一、源码分析
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 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];
}
// 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.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:kCTMediatorTargetA
action:kCTMediatorActionNativeFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
- (void)CTMediator_presentImage:(UIImage *)image
{
if (image) {
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativePresentImage
params:@{@"image":image}
shouldCacheTarget:NO];
} else {
// 这里处理image为nil的场景,如何处理取决于产品
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeNoImage
params:@{@"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 |
------------------------------- --------------------------------