逆水行舟,不进则退
这段时间处于项目空档期,别提有多开心了(如果让老大看到我这样估计我会死的很惨),开心并不是因为懒,而是为终于有了可以自由翱翔的时间
最近看了一些组件化方面的文章,感触良多,今天主要是基于CTMediator
组件化方案进行分享,大家如果觉得我理解的不对可以留言或者直接去看Caca写的 iOS应用架构谈 组件化方案
一:CTMediator源码
源码里面代码不是特别的多,大概就是200多行
先看一下.h文件
#import <UIKit/UIKit.h>
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
+ (instancetype)sharedInstance;
:单例,返回CTMediator
对象
performActionWithUrl
:这个方法主要是用于远程APP调用,比如从A应用传递一个URL到B应用,在B应用的openURL
方法中去处理url
performTarget
: 本地组件调用,使用RunTime处理target和action,shouldCacheTarget
是否对传入的target进行缓存
releaseCachedTargetWithTargetName
:把传入的target从缓存中删除
接下来去.m文件中看看具体是怎么实现的
sharedInstance
,这里就不多说了
- (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;
}
这个方法主要是针对远程APP的互相调起,通过openURL实现APP之间的跳转,通过URL进行数据传递
一个完整的URL就像上图一样,上面的代码中,优先从URL中获取到query中的数据,然后进行遍历然后把对应的参数的key和value添加到字典中,然后从URL中取出actionName,也就是要调用的方法名,最后通过
performTarget
方法去实现方法的调用,根据返回值处理回调
- (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在缓存中查找,没有找到就通过NSClassFromString获取这个类,如果tatget==nil进行错误处理,如果传入的shouldCacheTarget
为YES就把target添加到集合中缓存起来,然后判断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
}
这段代码主要是判断返回值类型,如果是void
,NSInteger
,BOOL
,CGFloat
,NSUInteger
就进行特殊处理,不是的话就直接返回performSelector
的返回值类型
二:使用CTMediator实战
CTMediator
笔者用的是cocopods进行的组件化管理,我这边用的是framework进行的
首页是一个单独的模块,按照原来的开发方式,如果把这个模块从项目中删除,肯定就会报错,因为项目中有几个地方是对首页进行引用的,如何才能做到删除它而对项目不产生影响呢?下面开始介绍我做了哪些操作:
- 抽取出主工程,包括:工具类,三方框架,常用的一些配置等,有了这些作为支撑,才可以开始子模块的开发和测试
- 创建framework,把首页功能封装在framework里面,通过一个中间类
Target_HomeVCAction
来操作首页功能,包括实例化,和外界参数的传递,只有中间类是可以供外部调用的 - 增加一个
CTMediator
的分类,在分类里面去关联上面提到的中间类,此处的关联其实也不需要导入文件,而是以字符串的形式传递类名和方法名,再通过调用CTMediator
中的performTarget
方法实现函数调用
分类里面的实现
NSString * const kCTMediatorTargetA = @"HomeVCAction";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail:(NSDictionary *)dict
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:dict
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
在整个过程中只有一处对CTMediator
分类的引用,如果传递的参数错误或者找不到类,可以在CTMediator
中进行统一处理,如果需要修改代码可以回到自己的framework中进行修改,修改完成后只需要把framework更新一下就可以了,,项目一天天的变的庞大起来,每次编译都会耗费很长的时间,对自己也是一种折磨,这样做可以大大的减少项目的编译时间了
这种通过Target-Action的组件化方案,我个人觉得挺好的,只是多了一些硬编码,但是方便各模块传值,使用URL路由跳转的话,传递对象就没那么简单了
大家有意见欢迎提出,帮助别人成长的同时,也是对自己的一次锤炼