Wrappres' Studio.

组件化之 Target-Action

字数统计: 1.3k阅读时长: 5 min
2020/10/05 Share

组件化之 Target-Action

iOS 中主要有三种组件化方式,URL Route、Target-Action、Protocol 匹配。这里主要讲第一种:Target-Action。
这类方式的大致思想是利用语言的特性 runtime,根据传入 target 和 action,动态的创建类去调用相应的方法。同时利用分类规范了传入的参数。

优缺点

优点

  • 利用 Category 可以明确声明接口,进行编译检查
  • 实现方式轻量,避免了注册操作

缺点

  • 过度依赖 runtime,无法应用在纯 Swift 项目中
  • 无法保证所使用的模块一定存在,target 模块在修改后,使用者只有在运行时才能发现错误
  • 使用 runtime 相关的接口调用任意类的任意方法,需要注意别被苹果的审核误伤。参考:Are performSelector and respondsToSelector banned by App Store?
  • 在 category 中仍然引入了字符串硬编码,内部使用字典传参,一定程度上也存在和 URL 路由相同的问题

代码示例

1
2
3
4
5
6
7
8
// 模块管理者,提供了动态调用 target-action 的基本功能
@interface Mediator : NSObject

+ (instancetype)sharedInstance;

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在 category 中定义新接口
@interface Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController;
@end

@implementation Mediator (ModuleActions)

- (UIViewController *)Mediator_editorViewController {
// 使用字符串硬编码,通过 runtime 动态创建 Target_Editor,并调用 Action_viewController:
UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];
return viewController;
}

@end

// 调用者通过 Mediator 的接口调用模块
UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 模块提供者提供 target-action 的调用方式
@interface Target_Editor : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end

@implementation Target_Editor

- (UIViewController *)Action_viewController:(NSDictionary *)params {
// 参数通过字典传递,无法保证类型安全
EditorViewController *viewController = [[EditorViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}

@end

CTMediator 的实现

CTMediator本质是利用Objective-C的语言特性Runtime,根据 URL 创建相应的类并调用方法,并利用分类对参数进行限制。

分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}

这里分类起到的作用主要是锦上添花

  • 规避了字符串的硬编码
  • 明确了模块的接口

核心方法

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget

分类内部主要调用的是performTarget方法,下面是它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}

NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];

// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = [self safeFetchCachedTarget:targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}

// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);

if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}

if (shouldCacheTarget) {
[self safeSetCachedTarget:target key:targetClassString];
}

if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
@synchronized (self) {
[self.cachedTarget removeObjectForKey:targetClassString];
}
return nil;
}
}
}

  1. 首先在cache里寻找是否有存在的target,如果没有则单独创建。
  2. 根据 targetaction调用- (id)safePerformAction:(SEL)action target:(NSObject )target params:(NSDictionary )params`方法

PS:其中这里有个两个机会可以处理无效的调用:1. 创建 Target 失败 2. 创建 Target 后调用 Action 失败

- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
const char* retType = [methodSig methodReturnType];

if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:&params atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}

if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:&params atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}

if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:&params atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}

if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:&params atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}

if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:&params atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
CATALOG
  1. 1. 组件化之 Target-Action
    1. 1.1. 优缺点
      1. 1.1.1. 优点
      2. 1.1.2. 缺点
    2. 1.2. 代码示例
    3. 1.3. CTMediator 的实现
      1. 1.3.1. 分类
      2. 1.3.2. 核心方法
        1. 1.3.2.1. - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
        2. 1.3.2.2. - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params