1. Target-Action
这种方案是基于 OC
的runtime
、category
特性动态获取模块,例如通过 NSClassFromString
获取类并创建实例,通过 performSelector + NSInvocation
动态调用方法。
这种方案的代表框架:CTMediator,实现原理可以看这篇文章,具体实践可以看这篇文章
本篇文章主要就是通过上面提到的两篇文章学习使用并记录一下CTMediator这个框架,然后分析一下其在组件化实施中的优点和缺点。
2. CTMediator的实现
我们先来看一下该方案实现的架构图,如下图所示:
我们以 Module_A
为例,打算将 Module_A
抽离为单独的组件,在使用CTMediator
此种方案时,需要先给 CTMediator
添加一个相应的 CTMediator+A
的分类,这个分类里面包含了Module_A
对外提供的接口,以及负责具体实现的 Target
,并且使用performTarget:action
方法来调用Target
的方法,示例如下:
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionAViewController = @"getAViewController";
@implementation CTMediator (A)
- (UIViewController *)CTMediator_AViewController
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action: kCTMediatorActionAViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
我们可以看到上面的performTarget:action
方法是 CTMediator
类中的,这个并不影响我们继续学习,它其实是将 Target
字符串,用 NSClassFromString
反射方法,构建了 Target
对应类的实例对象,然后调用了 action
对应的方法。
我们继续往下看Module_A
对应的 Target_A
,这个 Target_A
对 Module_A
有单向引用,负责对于Module_A
暴露接口方法的具体实现,比如上面的那个方法,将会调用到Target_A
中的getAViewController
方法,如下:
#import "DemoModuleAViewController.h"
@implementation Target_A
- (UIViewController *)Action_getAViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleAViewController *viewController = [[DemoModuleAViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
从而实现了对Module_A
中方法的调用。
CTMediator
主要是利用了Category 分类
提供的特性,去调用组件对应的Catetory
中的的方法,进而再去调用Category分类
中指定的 Target 的 Action方法
,这一步是利用反射 + NSInvocation
调用方法,下面源码分析时会有介绍。
3. CTMediator 源码分析
CTMediator
代码很简短,也很容易理解,但这并不是我们要学习的重点,我们需要了解的是CTMediator
这种实现方案的思想,不同项目中对于组件之间通信的方案是不一样的,这个就需要在实践的时候具体去衡量了。
我们以上面提到的performTarget:action:params:
方法为切入点:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
// 支持 Swift
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// 创建 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];
}
// 获取 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];
}
// 去调用 target 实例对象的 action 方法
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;
}
}
}
上面有对于方法或者类找不到的容错处理,这里就不做介绍了,该方法主要是通过 NSClassFromString
将传入的target
转成类,并创建一个该类的对象,然后通过 NSSelectorFromString
将传入的 action
转成 方法,然后调用target
的 action
方法,调用的方法实现如下:
- (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:¶ms 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:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
...
}
就是获取 action
对应的方法签名
,然后使用NSInvocation
调用, 或者使用系统的performSelector:withObject:
调用方法。
4. 总结
优点:
- 利用
category
可以明确声明的接口,进行编译检查 - 实现方式属于轻量级
缺点:
- 需要给每一个模块创建对应的
target
和mediator
分类,模块化代码时较为繁琐 - 在
category
中仍然使用了字符串硬编码,内部使用字典传参 - 无法保证所调用的目标模块一定存在,运行时才能发现错误