一、源码分析
CTMediator.h文件
#import <UIKit/UIKit.h>extern NSString * _Nonnull const kCTMediatorParamsKeySwiftTargetModuleName;@interface CTMediator : NSObject+ (instancetype _Nonnull)sharedInstance;// 远程App调用入口- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;// 本地组件调用入口- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;- (void)releaseCachedTargetWithFullTargetName:(NSString * _Nullable)fullTargetName;@end
+ (instancetype)sharedInstance;:单例,返回CTMediator对象performActionWithUrl:这个方法主要是用于远程APP调用,比如从A应用传递一个URL到B应用,在B应用的openURL方法中去处理urlperformTarget: 本地组件调用,使用RunTime处理target和action,shouldCacheTarget是否对传入的target进行缓存releaseCachedTargetWithTargetName:把传入的target从缓存中删除
CTMediator.m文件
这个方法主要是针对远程APP的互相调起,通过openURL实现APP之间的跳转,通过URL进行数据传递
一个完整的URL就像上图一样,上面的代码中,优先从URL中获取到query中的数据,然后进行遍历然后把对应的参数的key和value添加到字典中,然后从URL中取出actionName,也就是要调用的方法名,最后通过performTarget方法去实现方法的调用,根据返回值处理回调
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion{NSMutableDictionary *params = [[NSMutableDictionary alloc] init];NSString *urlString = [url query];for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {NSArray *elts = [param componentsSeparatedByString:@"="];if([elts count] < 2) continue;[params setObject:[elts lastObject] forKey:[elts firstObject]];}// 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];if ([actionName hasPrefix:@"native"]) {return @(NO);}// 这个demo针对URL的路由处理非常简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。如果需要拓展,可以在这个方法调用之前加入完整的路由逻辑id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];if (completion) {if (result) {completion(@{@"result":result});} else {completion(nil);}}return result;}
根据传递的targetName在缓存中查找,没有找到就通过NSClassFromString获取这个类,如果target==nil进行错误处理,如果传入的shouldCacheTarget为YES就把target添加到集合中缓存起来,然后判断target是否可以响应传进来的方法,不能响应错误处理,可以响应就调用safePerformAction这个方法
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget{NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];// generate targetNSString *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];}// generate actionNSString *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.cachedTarget[targetClassString] = target;}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;}}}
targetName就是调用接口的Object,actionName就是调用方法的SEL,params是参数,shouldCacheTarget代表是否需要缓存,如果需要缓存就把target存起来,- Key是targetClassString,
- Value是target。
这段代码主要是判断返回值类型,如果是void,NSInteger,BOOL,CGFloat,NSUInteger就进行特殊处理,不是的话就直接返回performSelector的返回值类型
- (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}
二、应用实践
CTMediator的分类
增加一个CTMediator的分类,在分类里面去关联上面提到的中间类,此处的关联其实也不需要导入文件,而是以字符串的形式传递类名和方法名,再通过调用CTMediator中的performTarget方法实现函数调用
@implementation CTMediator (CTMediatorModuleAActions)- (UIViewController *)CTMediator_viewControllerForDetail{UIViewController *viewController = [self performTarget:kCTMediatorTargetAaction:kCTMediatorActionNativeFetchDetailViewControllerparams:@{@"key":@"value"}shouldCacheTarget:NO];if ([viewController isKindOfClass:[UIViewController class]]) {// view controller 交付出去之后,可以由外界选择是push还是presentreturn viewController;} else {// 这里处理异常场景,具体如何处理取决于产品return [[UIViewController alloc] init];}}- (void)CTMediator_presentImage:(UIImage *)image{if (image) {[self performTarget:kCTMediatorTargetAaction:kCTMediatorActionNativePresentImageparams:@{@"image":image}shouldCacheTarget:NO];} else {// 这里处理image为nil的场景,如何处理取决于产品[self performTarget:kCTMediatorTargetAaction:kCTMediatorActionNativeNoImageparams:@{@"image":[UIImage imageNamed:@"noImage"]}shouldCacheTarget:NO];}}@end
实际应用中,这是一个单独的
repo,所用需要调度其他模块的人,只需要依赖这个repo。这个repo由target-action维护者维护mediator这个repo维护了若干个针对mediator的category,每一个对应一个target,每个category里的方法对应了这个target下所有可能的调用场景,这样调用者在包含mediator的时候,自动获得了所有可用的target-action,无论是调用还是参数传递,都非常方便。
使用分类优点
- category本身就是一种组合模式,根据不同的分类提供不同的方法,此时每一个组件就是一个分类,因此把每个组件可以支持的调用用category封装是很合理的。
- 在category的方法中可以做到参数的验证,在架构中对于保证参数安全是很有必要的。当参数不对时,category就提供了补救的入口。
- category可以很轻松地做请求转发,如果不采用category,请求转发逻辑就非常难做了。
- category统一了所有的组件间调用入口,因此无论是在调试还是源码阅读上,都为工程师提供了极大的方便。
- 由于category统一了所有的调用入口,使得在跨模块调用时,对于param的
hardcode在整个App中的作用域仅存在于category中,在这种场景下的hardcode就已经变成和调用宏或者调用声明没有任何区别了,因此是可以接受的。 - 对Mediator的所有方法进行拆分,这样就可以不会导致Mediator这个类过于庞大了。
这里是业务方使用category调用时的场景,大家可以看到非常方便,不用去记URL也不用纠结到底应该传哪些参数。
if (indexPath.row == 0) {UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];// 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了[self presentViewController:viewController animated:YES completion:nil];}if (indexPath.row == 1) {UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];[self.navigationController pushViewController:viewController animated:YES];}if (indexPath.row == 2) {// 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了[[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];}if (indexPath.row == 3) {// 这种场景下,参数有问题,因此需要在流程中做好处理[[CTMediator sharedInstance] CTMediator_presentImage:nil];}if (indexPath.row == 4) {[[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {// 做你想做的事NSLog(@"%@", info);}];}
target中间类
target-action所在的模块,也就是提供服务的模块,这也是单独的repo,但无需被其他人依赖,其他人通过category调用到这里的功能
@implementation Target_A- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params{// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];viewController.valueLabel.text = params[@"key"];return viewController;}- (id)Action_nativePresentImage:(NSDictionary *)params{DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];viewController.valueLabel.text = @"this is image";viewController.imageView.image = params[@"image"];[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];return nil;}@end
三、方案架构图
这幅图是组件化方案的一个简化版架构描述,主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务,与蘑菇街方案正好相反。
--------------------------------------| [CTMediator sharedInstance] || || openUrl: <<<<<<<<< (AppDelegate) <<<< Call From Other App With URL| || | || | || |/ || || parseUrl || || | || | |.................................|...............................| | || | || |/ || || performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Call From Native Module| || | || | || | || |/ || || ------------- || | | || | runtime | || | | || ------------- || . . |---------------.---------.------------. .. .. .. .. .. .. .. .-------------------.----------- ----------.---------------------| . | | . || . | | . || . | | . || . | | . || | | || Target | | Target || | | || / | \ | | / | \ || / | \ | | / | \ || | | || Action Action Action ... | | Action Action Action ... || | | || | | || | | ||Business A | | Business B |------------------------------- --------------------------------
