一、源码分析

CTMediator.h文件

  1. #import <UIKit/UIKit.h>
  2. extern NSString * _Nonnull const kCTMediatorParamsKeySwiftTargetModuleName;
  3. @interface CTMediator : NSObject
  4. + (instancetype _Nonnull)sharedInstance;
  5. // 远程App调用入口
  6. - (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
  7. // 本地组件调用入口
  8. - (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
  9. - (void)releaseCachedTargetWithFullTargetName:(NSString * _Nullable)fullTargetName;
  10. @end
  • + (instancetype)sharedInstance;:单例,返回CTMediator对象
  • performActionWithUrl:这个方法主要是用于远程APP调用,比如从A应用传递一个URL到B应用,在B应用的openURL方法中去处理url
  • performTarget : 本地组件调用,使用RunTime处理targetaction,shouldCacheTarget是否对传入的target进行缓存
  • releaseCachedTargetWithTargetName:把传入的target从缓存中删除

CTMediator.m文件

这个方法主要是针对远程APP的互相调起,通过openURL实现APP之间的跳转,通过URL进行数据传递
image.png
一个完整的URL就像上图一样,上面的代码中,优先从URL中获取到query中的数据,然后进行遍历然后把对应的参数的key和value添加到字典中,然后从URL中取出actionName,也就是要调用的方法名,最后通过performTarget方法去实现方法的调用,根据返回值处理回调

  1. - (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
  2. {
  3. NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
  4. NSString *urlString = [url query];
  5. for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
  6. NSArray *elts = [param componentsSeparatedByString:@"="];
  7. if([elts count] < 2) continue;
  8. [params setObject:[elts lastObject] forKey:[elts firstObject]];
  9. }
  10. // 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。
  11. NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
  12. if ([actionName hasPrefix:@"native"]) {
  13. return @(NO);
  14. }
  15. // 这个demo针对URL的路由处理非常简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。如果需要拓展,可以在这个方法调用之前加入完整的路由逻辑
  16. id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
  17. if (completion) {
  18. if (result) {
  19. completion(@{@"result":result});
  20. } else {
  21. completion(nil);
  22. }
  23. }
  24. return result;
  25. }

根据传递的targetName在缓存中查找,没有找到就通过NSClassFromString获取这个类,如果target==nil进行错误处理,如果传入的shouldCacheTargetYES就把target添加到集合中缓存起来,然后判断target是否可以响应传进来的方法,不能响应错误处理,可以响应就调用safePerformAction这个方法

  1. - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
  2. {
  3. NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
  4. // generate target
  5. NSString *targetClassString = nil;
  6. if (swiftModuleName.length > 0) {
  7. targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
  8. } else {
  9. targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
  10. }
  11. NSObject *target = self.cachedTarget[targetClassString];
  12. if (target == nil) {
  13. Class targetClass = NSClassFromString(targetClassString);
  14. target = [[targetClass alloc] init];
  15. }
  16. // generate action
  17. NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
  18. SEL action = NSSelectorFromString(actionString);
  19. if (target == nil) {
  20. // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
  21. [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
  22. return nil;
  23. }
  24. if (shouldCacheTarget) {
  25. self.cachedTarget[targetClassString] = target;
  26. }
  27. if ([target respondsToSelector:action]) {
  28. return [self safePerformAction:action target:target params:params];
  29. } else {
  30. // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
  31. SEL action = NSSelectorFromString(@"notFound:");
  32. if ([target respondsToSelector:action]) {
  33. return [self safePerformAction:action target:target params:params];
  34. } else {
  35. // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
  36. [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
  37. [self.cachedTarget removeObjectForKey:targetClassString];
  38. return nil;
  39. }
  40. }
  41. }
  • targetName就是调用接口的Object
  • actionName就是调用方法的SEL
  • params是参数,
  • shouldCacheTarget代表是否需要缓存,如果需要缓存就把target存起来,
    • Key是targetClassString,
    • Value是target。

这段代码主要是判断返回值类型,如果是void,NSInteger,BOOL,CGFloat,NSUInteger就进行特殊处理,不是的话就直接返回performSelector的返回值类型

  1. - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
  2. {
  3. NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
  4. if(methodSig == nil) {
  5. return nil;
  6. }
  7. const char* retType = [methodSig methodReturnType];
  8. if (strcmp(retType, @encode(void)) == 0) {
  9. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
  10. [invocation setArgument:&params atIndex:2];
  11. [invocation setSelector:action];
  12. [invocation setTarget:target];
  13. [invocation invoke];
  14. return nil;
  15. }
  16. if (strcmp(retType, @encode(NSInteger)) == 0) {
  17. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
  18. [invocation setArgument:&params atIndex:2];
  19. [invocation setSelector:action];
  20. [invocation setTarget:target];
  21. [invocation invoke];
  22. NSInteger result = 0;
  23. [invocation getReturnValue:&result];
  24. return @(result);
  25. }
  26. if (strcmp(retType, @encode(BOOL)) == 0) {
  27. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
  28. [invocation setArgument:&params atIndex:2];
  29. [invocation setSelector:action];
  30. [invocation setTarget:target];
  31. [invocation invoke];
  32. BOOL result = 0;
  33. [invocation getReturnValue:&result];
  34. return @(result);
  35. }
  36. if (strcmp(retType, @encode(CGFloat)) == 0) {
  37. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
  38. [invocation setArgument:&params atIndex:2];
  39. [invocation setSelector:action];
  40. [invocation setTarget:target];
  41. [invocation invoke];
  42. CGFloat result = 0;
  43. [invocation getReturnValue:&result];
  44. return @(result);
  45. }
  46. if (strcmp(retType, @encode(NSUInteger)) == 0) {
  47. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
  48. [invocation setArgument:&params atIndex:2];
  49. [invocation setSelector:action];
  50. [invocation setTarget:target];
  51. [invocation invoke];
  52. NSUInteger result = 0;
  53. [invocation getReturnValue:&result];
  54. return @(result);
  55. }
  56. #pragma clang diagnostic push
  57. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  58. return [target performSelector:action withObject:params];
  59. #pragma clang diagnostic pop
  60. }

二、应用实践

CTMediator的分类

增加一个CTMediator的分类,在分类里面去关联上面提到的中间类,此处的关联其实也不需要导入文件,而是以字符串的形式传递类名和方法名,再通过调用CTMediator中的performTarget方法实现函数调用

  1. @implementation CTMediator (CTMediatorModuleAActions)
  2. - (UIViewController *)CTMediator_viewControllerForDetail
  3. {
  4. UIViewController *viewController = [self performTarget:kCTMediatorTargetA
  5. action:kCTMediatorActionNativeFetchDetailViewController
  6. params:@{@"key":@"value"}
  7. shouldCacheTarget:NO
  8. ];
  9. if ([viewController isKindOfClass:[UIViewController class]]) {
  10. // view controller 交付出去之后,可以由外界选择是push还是present
  11. return viewController;
  12. } else {
  13. // 这里处理异常场景,具体如何处理取决于产品
  14. return [[UIViewController alloc] init];
  15. }
  16. }
  17. - (void)CTMediator_presentImage:(UIImage *)image
  18. {
  19. if (image) {
  20. [self performTarget:kCTMediatorTargetA
  21. action:kCTMediatorActionNativePresentImage
  22. params:@{@"image":image}
  23. shouldCacheTarget:NO];
  24. } else {
  25. // 这里处理image为nil的场景,如何处理取决于产品
  26. [self performTarget:kCTMediatorTargetA
  27. action:kCTMediatorActionNativeNoImage
  28. params:@{@"image":[UIImage imageNamed:@"noImage"]}
  29. shouldCacheTarget:NO];
  30. }
  31. }
  32. @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也不用纠结到底应该传哪些参数。

  1. if (indexPath.row == 0) {
  2. UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
  3. // 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
  4. [self presentViewController:viewController animated:YES completion:nil];
  5. }
  6. if (indexPath.row == 1) {
  7. UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
  8. [self.navigationController pushViewController:viewController animated:YES];
  9. }
  10. if (indexPath.row == 2) {
  11. // 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了
  12. [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
  13. }
  14. if (indexPath.row == 3) {
  15. // 这种场景下,参数有问题,因此需要在流程中做好处理
  16. [[CTMediator sharedInstance] CTMediator_presentImage:nil];
  17. }
  18. if (indexPath.row == 4) {
  19. [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
  20. // 做你想做的事
  21. NSLog(@"%@", info);
  22. }];
  23. }

target中间类

target-action所在的模块,也就是提供服务的模块,这也是单独的repo,但无需被其他人依赖,其他人通过category调用到这里的功能

  1. @implementation Target_A
  2. - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
  3. {
  4. // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
  5. DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
  6. viewController.valueLabel.text = params[@"key"];
  7. return viewController;
  8. }
  9. - (id)Action_nativePresentImage:(NSDictionary *)params
  10. {
  11. DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
  12. viewController.valueLabel.text = @"this is image";
  13. viewController.imageView.image = params[@"image"];
  14. [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
  15. return nil;
  16. }
  17. @end

三、方案架构图

这幅图是组件化方案的一个简化版架构描述,主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务,与蘑菇街方案正好相反。

  1. --------------------------------------
  2. | [CTMediator sharedInstance] |
  3. | |
  4. | openUrl: <<<<<<<<< (AppDelegate) <<<< Call From Other App With URL
  5. | |
  6. | | |
  7. | | |
  8. | |/ |
  9. | |
  10. | parseUrl |
  11. | |
  12. | | |
  13. | | |
  14. .................................|...............................
  15. | | |
  16. | | |
  17. | |/ |
  18. | |
  19. | performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Call From Native Module
  20. | |
  21. | | |
  22. | | |
  23. | | |
  24. | |/ |
  25. | |
  26. | ------------- |
  27. | | | |
  28. | | runtime | |
  29. | | | |
  30. | ------------- |
  31. | . . |
  32. ---------------.---------.------------
  33. . .
  34. . .
  35. . .
  36. . .
  37. . .
  38. . .
  39. . .
  40. . .
  41. -------------------.----------- ----------.---------------------
  42. | . | | . |
  43. | . | | . |
  44. | . | | . |
  45. | . | | . |
  46. | | | |
  47. | Target | | Target |
  48. | | | |
  49. | / | \ | | / | \ |
  50. | / | \ | | / | \ |
  51. | | | |
  52. | Action Action Action ... | | Action Action Action ... |
  53. | | | |
  54. | | | |
  55. | | | |
  56. |Business A | | Business B |
  57. ------------------------------- --------------------------------

image.png

参考