iOS组件化的分层思路
组件化是一种软件设计方法,它将一个大型应用程序拆分成多个独立且可重用的模块。这些模块可以分别开发、测试和维护,从而提高代码的复用性和可维护性。通常,组件化的分层思想大致分为以下三层:
基础模块:封装一些不与业务相关的模块,如工具类和分类。这一层的代码应当是完全独立于项目,可以在其他项目中直接使用而无需修改。
通用模块:实际上是“通用业务模块”,既包含通用代码,也包含与业务逻辑相关的通用部分,如通用UIButton组件等。
业务模块:具体业务逻辑的实现模块,需要结合实际项目进行划分。
模块的开发集成顺序为从下至上,即从基础模块到通用模块再到业务模块,而依赖顺序则是从上到下。
业务模块的设计
业务模块的设计是组件化过程中最难的一部分,需要综合考虑当前的合理性和未来的扩展性。在组件化之前,项目中的各个模块可能会有复杂的耦合关系。在进行组件化之后,需要通过建立“通讯中间层”来降低这些模块之间的耦合度。
CTMediator 通讯中间框架介绍
CTMediator是一个用于模块间通讯的中间层框架,通过runtime机制实现完全杜绝模块间的耦合。它的基本思路包括: • 每个模块隔离出一个独立的Target层,这个层是模块的声明文件。该层提供了外部调用该模块功能的接口,并对传入参数进行验证和错误处理。 • 使用字符串形式的类名和方法名,通过runtime机制在不导入某模块的情况下调用其方法,从而避免模块间的硬性依赖。
增加分类 - 解决中间层与模块间的耦合
为防止CTMediator框架发生意外变化及避免项目或模块对框架的强耦合,可以通过增加分类(Category)进行拓展。其好处包括:
防止框架变化:通过创建分类进行拓展,不直接修改CTMediator源码,避免因框架升级导致的问题。
减少强耦合:为每个模块独立出一个类,分类中的方法与Target层声明的方法对应,方便外部调用。这使一个完整模块包括 分类 --> Target层 ---> 模块源码层。
框架使用示例
以present image操作为例,模块A暴露了一个imageView��于传值。模块A的Target层实现了所需方法,如Action_nativePresentImage。最终,通过CTMediator框架的performSelector方法实现对目标方法的调用。 总结 • CTMediator通过runtime机制进行模块解耦。 • 增加分类进一步减少中间层与模块间的耦合。 • 模块化设计提高代码的可复用性和维护性。
基础模块与通用模块的分层方式
这部分可依需求调整: • 基础模块:封装不与业务相关的模块,如分类、工具类等。理想状态下,这一层不需修改即可在其他项目中使用。 • 通用模块:主要是通用业务模块,如公用组件、通用UIButton、瀑布流、时间计算NSDate等。此层应体现与业务挂钩的通用逻辑;如果完全独立于业务逻辑,则放入基础模块。
业务模块
组件化除了技术层面,更难的是业务模块设计,需要考虑整个项目的划分、当前分层的合理性及未来扩展性。频繁改动的模块需要合理设计前端接口及与其他模块的交互。
通常项目模块间的关系大致如下图,就是我们在进行组件化之前的项目,给个层次中间的关系,这里写的模块可以理解是类之间的关系。
各个模块都或多或少有关系,模块间进行通讯(即类之间的方法调用),需要进行#import导入头文件,是一种比较强的耦合关系。
模块化的组件间耦合问题
为了实现模块间的低耦合,首先需要解决模块间的耦合关系。模块越独立,系统的可维护性和灵活性就越高。基本思路是建立一个通讯中间层,各模块通过该中间层进行通讯,避免直接联系。
通讯中间层的问题
与中间层的强耦合:中间层需要导入所有模块,才能允许模块间通讯。
中间层过于庞大:如果A模块只想与C模块通讯,中间层却包含ABCD所有代码,这不合理。
排列组合问题:中间层设计为AB、AC、AD、BC、BD、CD等组合,会使设计复杂化。
CTMediator 通讯中间框架介绍 框架地址:github.com/casatwy/CTM… 代码结构图:
框架使用前提:已经对项目划分好合理的业务模块,单纯是对项目的业务进行分层,不考模块之间的通讯。
基本思路
项目划分:先对项目进行合理的业务模块划分,不考虑模块间通讯。
独立Target层:每个模块有一个独立的Target层,作为模块声明和入口,类似于.h文件。
入口封装:Target层封装模块提供给外部的功能,如登录、注册等,并进行业务判断或容错处理。
代码实现思路 • 杜绝耦合:通过runtime机制将方法调用转换为字符串形式,以类名和方法名(字符串)进行调用,避免模块间直接导入。 • 错误处理:框架内部对参数如target和action进行容错处理,以减少手误风险。
增加分类 - 减少中间层与模块间的耦合
设计目的
防止框架意外变化:创建分类进行拓展,不直接修改CTMediator源码,类似于使用AFN等三方框架时,通过独立类管理和封装。
减少强耦合:为每个模块独立出一个类,分类中的方法与Target层声明的方法一一对应。
实现方式 • 举例:在登录模块中,将登录、注册、忘记密码等功能方法分别在Target层写好声明和实现,然后复制到分类。 • 外部调用:外部只需导入这个分类即可使用相应功能,类似于常规分类的使用效果。 • 完整模块结构:一个完整的模块包括 分类 --> Target层 ---> 模块源码层。
CTMediator代码和demo介绍
下面我们针对在github给出的demo进行介绍
项目整体介绍 项目代码结构
代码运行起来,是一个tableView
我们以 present image 这个操作
这个功能是将 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类对应的方法及其实现为如下,主要是创建模块A控制器,解析传进来的参数解,并进行赋值控制器,并实现modal。
- (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.我的理解起名和如何定义字符串,只要项目内部约定好就行,大家都根据这规则就好。
第二步解决了方法名的问题,然后我们整体看下方法的调用,从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调用就结束了,大致的流程为