1.组件化基本分层
-
组件化的分层思想是三层
- 基础模块
- 通用模块
- 业务模块
模块的集成顺序是从下至上,从
基础模块
到通用模块
到业务模块
,但是依赖的顺序是从上到下,业务
依赖通用
,通用
依赖基础
一二层模块可以看做是同一个层次,也可以是两个层次,但是即便是同一个层次,也要划分为两个目录
2.业务模块
-
通常项目模块间的关系大致如下图
- 各个模块多少都有关系,模块间进行通讯,需要导入彼此
-
模块间进行组件化首先要解决模的问题就是模块间之间的耦合关系,基本的思路是建立中间层,各个模块之间通过中间层进行通讯。如下图
3.CTMediator 通讯中间框架介绍
- 框架地址 https://github.com/casatwy/CTMediator
-
代码结构
- 框架的基本思路是
- 将每个模块的业务调用隔离出一个独立Target(这个和OC的target-action模式没有任何关系)层,该层作为该模块通讯的入口,通俗的讲就是这个模块的声明文件,对该模块的方法调用入口进行一次封装
- Target
- 在代码封装过程可以进行一些业务层面的判断或者容错
- 框架代码思路
- 将方法的调用转换为方法的调用转换为不导入某模块的情况下去调用该模块的方法,避免模块间的导入
- 以类名,方法名字符串形式进行调用,通过方法签名 + NSInvocation 方式对方法调用进行封装,通过
performSelector
进行调用 - 对于参数,target, action等进行容错处理
4.增加分类
4.1总体介绍
尽管CTMediator的代码整体比较少,逻辑也不是很复杂,但是由于是避免对三方框架强耦合,也避免框架的代码发生过大的变化,另外增加分类也是为了进一步解耦合,总结一下大致为以下几点
- 避免项目或者模块对
CTMediator
框架进行强耦合 - 防止
CTMediator
框架代码出现比较大的升级或者停止维护等 - 对模块增加一层隔离,每个模块需要暴露给外部调用的都写在分类里,如果想调用该模块的方法,进导入该模块对应的分类即可。
- 比如登录模块A,有登录方法,注册方法,忘记密码方法,如果C在某个业务需要时需要登录,这时只需要引入A分类即可
4.2 CTMediator代码 和 demo介绍
下面我们针对在github给出的demo进行介绍
1. 项目整体介绍
-
项目代码结构
-
代码运行起来,是一个tableView
- 我们以
present image
这个操作
2. 这个功能是将 DemoModuleADetailViewController
modal出来,并需要往这个控制器里传递一个UIImage,DemoModuleADetailViewController 一下简称为模块A
-
模块A
声明暴露了一个imageView,用于赋值
@interface DemoModuleADetailViewController : UIViewController
@property (nonatomic, strong, readonly) UILabel *valueLabel;
@property (nonatomic, strong, readonly) UIImageView *imageView;
@end
-
模块A
的target 层Target_A
类对应的方法及其实现为如下,主要是对参数解析出来,并进行赋值
- (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;
}
- 方法名
Action_nativePresentImage
根据是根据目前CTM的规则拼接出来,方法名的规则是Action_
拼上方法名,nativePresentImage
是我们可以定义的方法的名字,这名字在分类的层面进行声明拼接,在demo是定义成了一个static string
.我的理解起名和如何定义字符串,只要项目内部约定好就行,大家都根据这规则就好。
3. 第二步解决了方法名的问题,然后我们整体看下方法的调用,从cell的点击一步一步
-
cell 点击,调用CTM分类方法
-
分类方法的实现
- 调用到CTM内部方法,进行类 和 方法名的转换,创建对象,内部应做了相应的注释,下一步对
performSelector:
进一步封装
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
// tagrt判空
if (targetName == nil || actionName == nil) {
return nil;
}
// 对swift的特殊标记
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// 拼接target-action的tagert,就是方法调用的类名的字符串,类名规则为Target_方法名
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
// 类名规则为Target_方法名
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
// 做了类对象的缓存,避免多次创建对象
NSObject *target = self.cachedTarget[targetClassString];
// 类对象
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// 处理方法名字 拼接规则为 Action_方法名
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
// 方法名转 SEL
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;
}
}
}
- 此部分主要是对参数进行容错,对
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
}
至此一个完整CTM调用就结束了,大致的流程为