组件化开发第一步要解决的就是解耦以及不同module之间的通信问题,这个时候CTMediator就进入了大家的视野,它使用的解耦方式是非常典型的
Target-Action
,使用字符串(类名、方法名)来获取对象和方法,然后调用,简单粗暴又高效,它省去了其他方式例如protocol、URL路由等需要建立和维护注册表的麻烦,但相应的增加了很多字符串的硬编码,但个人认为利大于弊。
核心原理
FOUNDATION_EXPORT Class _Nullable NSClassFromString(NSString *aClassName);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
- (id)performSelector:(SEL)aSelector withObject:(id)object;
利用OC动态语言runtime的特性,使用👆上面的前两个方法获取到类和方法,最后一个函数可以使用实例调用获取到的方法。这里不得不感叹动态语言的便利性,swift由于静态的本质,至今没有什么好的解耦方式,因为静态语言的类型是在编译时就确定的,注定了它无法像上面一样使用字符串就能获取到,所以现在iOS常用的解耦框架都是OC编写的,swift工程只能混编OC来使用这些框架,一声叹息啊……
源码解析
1. 头文件
extern NSString * const kCTMediatorParamsKeySwiftTargetModuleName;
@interface CTMediator : NSObject
+ (instancetype)sharedInstance;
// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;
@end
- kCTMediatorParamsKeySwiftTargetModuleName 这个是swift功能使用的,swift工程内获取Target需要带上module的名称
- sharedInstance 这个没啥说的,单例
- performActionWithUrl 远程url调用方法,主要也是解析url之后调用👇下面的target方法
- performTarget 使用字符串来调用target对应的方法,params是action的参数,shouldCacheTarget是否缓存,这个缓存是个极其简单的字典存储,并没有实现自动内存管理,不建议开启这个缓存
- releaseCachedTargetWithTargetName 移除缓存的target,与上一个方法的是否缓存对应
上面最重要的还是performTarget这个方法,接下来我们详细去读一下它的实现
2. 实现
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.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
首先是获取Target,前面的就是一系列拼接Target名称的方法,拼接后是Modeule.Target_yourTarget
这个格式,OC工程不需要Module,直接是Target_yourTarget
。所以这里算是个默认约定,你使用CTMediator获取的Target类,必须以Target_
这个前缀开头。
然后先去读缓存,缓存里并没有这个Target对象的话,就使用NSClassFromString
获取类,然后初始化出target对象。swift类记得继承NSObject,不然无法使用OC方法动态获取⚠️
我们来看一个这份缓存的实现,我前面也吐槽过了,这个缓存太过简单了
#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
if (_cachedTarget == nil) {
_cachedTarget = [[NSMutableDictionary alloc] init];
}
return _cachedTarget;
}
cachedTarget
就是一个NSMutableDictionary
可变的字典😂,以上面拼接出来的target名称作为键值,当你肆无忌惮的开启缓存,然后你的target又特别多,target对象里面又包含很多内容,你想想你的内存会怎样,字典并不会帮助你使用LRU(最近最少使用原则)这样的原则释放内存,你自己也不可能把握好释放target的时机,所以这个不要随便缓存,我后面会提出解决方案的,继续看下去
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
接下来和之前获取Target类似,拼接出方法名,格式是Action_yourAction
,所以你的方法也必须使用Action_
作为前缀,然后swift的类,记得方法前声明@objc
。
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
接下来是异常处理,如果没有获取到对应的Target,进入这个处理;在下面就是如果获取到target对象了,根据你的设置进行缓存。没有获取到Target可能的原因有:Target不存在、名称错误、Module错误等等
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];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
终于到了激动人心的target对象调用action方法的环节!首先使用respondsToSelector:
方法判断target是否响应这个sel,如果不响应也就是target对象并没有实现这个方法,进入异常处理;可以响应的话,那我们就去调用它,获取返回值,作者实现了safePerformAction:
来实现调用,我们来看看这个方法的实现
- (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);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms 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:¶ms 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:¶ms 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
}
这个方法比较长,但其实前面就是重复的代码,来实现返回类型是基本数据类型的调用,使用的是NSInvocation
来完成调用;最后使用performSelector
实现返回值是对象的调用。
最后performSelector方法前后的#pragma clang diagnostic
是消除内存警告的预处理命令,这个和编译器有关,这里不深入说明。
讲到这里差不多就把这个框架的核心方法讲完了,之前url调用的方法也是将url解析之后调用performTarget
完成的;还有那个releaseCached
方法只是移除那个缓存字典里面对应的数据,需要自己手动调用,时机很难把握,这也是这个框架做的不太好的地方,提供了缓存却没有实现内存管理的逻辑😢
由于我的项目是swift项目,我上面也说了CTMediator没有实现内存管理逻辑,所以我使用swift实现了一个使用LRU(最近最少使用原则)自动管理内存的解耦工具,调用方法也使用swift的语言特性进行了一些优化,使用更加简单,👉# AAMediator,这个工具完全和CTMediator兼容,正在使用CTMediator并且使用swift的同学可以尝试一下我这个工具,pod可以直接集成,代码也很简单易懂,大家可以去读一下源码,欢迎大家提出问题和交流👏