先说模块化可能给项目带来的改变:
- 代码提交更规范,分工更为明确,质量提高
- 编译加快
在原模式中,需要 150s 左右整个编译完毕,然后开发人员才可以开始调试。而现在组件化之后,某个业务组件只需要 10s ~ 20s 左右即可开工 - 结合 MVVM
更加细化的单元测试,提高代码质量,保证 App 稳定性 - 回滚更方便
面对发生业务或者 UI 变回之前版本的情况,以前我们都是 checkout 出之前的代码。而现在组件化了之后,我们只需要使用旧版本的业务组件 Pod 库,或者在旧版本的基础上再发一个 Pod 库便可。 - 上面都是忽悠你来看的,别当真 🤣
模块化抽离
最近一直在调研模块化的相关知识,基本掌握了“初级封装抽离”的水平,正在迷茫之际,遇大神指点迷津,探索出了后面的进阶路线,心中默默感谢大神一刻钟...
1)初级封装抽离:
主要工作就是把 App 之间重用的 Util、Category、网络层和本地存储等抽成了 Pod 库,由于三方库自带一定的解耦性,对后期的组件化开发也比较有帮助。另一方面工作比如Chart,ChartSocket这些功能在各个 App 之间重用的却不会过于耦合,所以拆分难度也不会太高。
这一级的抽离相对简单,难点倒是对 cocopods 等工具的使用,目前的我组件化学习就只到这个水平,大家共同学习!
相关组件化工具的使用参考:《使用 CocoaPods 对公有库开源和私有库组件》https://juejin.cn/post/6844903581250748430
都是自己摸着石头过河,有什么不对的地方,大家探讨哈~
2)中级解耦抽离:
以 Analytics 统计功能为例,Analytics 是依赖 UMengAnalytics 来做统计的,用于收集数据的方法处理不好极易发生耦合,如既依赖了 User,还依赖了 currentServerId等。
应对 Analytics 这类情况,网上资料有几种方法来解耦:
- 1.把它依赖的代码先做成一个 Pod 库,然后转而依赖 Pod 库。有点像是“依赖下沉”。
- 2.使用 category 的方式把依赖改成组合的方式。
- 3.使用一个 block 或 delegate(协议)把这部分职责丢出去。
- 4.直接 copy 代码,其实我首先想到的就是这个 😂,copy 代码这个事情看起来很不优雅,但是它的好处就是快。对于一些不重要的工具方法,也可以直接 copy 到内部来用。
对于解耦,网上类似的资料还有利用中间件 Mediator的方式:
应对上面的情景,最直接的方法就是增加一个中间件,各个模块跳转通过中间件来管理。这样,所有模块只依赖这个中间件。
但是中间件怎么去调用其他模块那?好吧,中间件又会依赖所有模块。好像除了增加代码的复杂度,并没有真正解决任何问题。
有没有一种方法,可以完美的解决这个依赖关系那?
我们希望做到:每个模块之间互相不依赖,并且每个模块可以脱离工程由不同的人编写、单独编译调试。
下面的方案通过对中间件的改造,很好的解决了这个问题,解决后的模块间依赖关系如下:
实现方案 demo 源码地址: https://github.com/zcsoft/ZC_CTMediator,搞来学习吧
目录结构:
所有模块的引用关系如图:
由于 demo 中只是从 ViewController.h.m 中跳转到 DemoModule 模块,所以只需要 ViewController.h.m 依赖 CTMediator,CTMediator 到 DemoModule 模块的调用是使用运行时完成了(图片中的蓝线),在代码中不需要相护依赖。
也就是说,如果一个模块不需要跳转到其他模块,就不需要依赖 CTMediator。
完整的内部调用关系图:
响应过程:
1.ViewController 中判断Cell选中 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; -> 2.CTMediator+CTMediatorModuleAActions 中图片加载响应方法 - (void)CTMediator_presentImage:(UIImage *)image; -> 3.CTMediator 中本地组件调用入口 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params; -> 4.Target_A - (id)Action_nativePresentImage:(NSDictionary *)params; 完成跳转。
事件响应断点:
Demo 中各个文件功用说明:
<1>CTMediator.h.m 功能: 指定目标(target,类名)+动作(action,方法名),并提供一个字典类型的参数。
CTMediator.h.m 会判断 target-action 是否可以调用,如果可以,则调用。由于这一功能是通过 runtime 动态实现的,所以在 CTMediator.h.m 的实现中,不会依赖任何其他模块,也不需要知道 target-action 的具体功能,只要 target-action 存在,就会被执行 target-action 具体的功能由 DemoModule 自己负责)。
CTMediator.h 里实际提供了两个方法,分别处理 url 方式的调用和 target-action 方式的调用,其中,如果使用 url 方式,会自动把 url 转换成 target-action。
<2>CTMediator+CTMediatorModuleAActions.h.m 功能:CTMediator 的扩展,用于管理跳转到 DemoModule 模块的动作。其他模块想要跳转到 DemoModule 模块时,通过调用这个类的方法来实现。
但是这个类中,并不真正去做跳转的动作,它只是对 CTMediator.h.m类的封装,这样用户就不需要关心使用CTMediator.h.m跳转到DemoModule模块时具体需要的target名称和action名称了。
<3>‘CTMediator.h.m’+‘CTMediator+CTMediatorModuleAActions.h.m’ 共同组成了一个面相 DemoModule 的跳转,并且它不会在代码上依赖 DemoModule,DemoModule 是否提供了相应的跳转功能,只体现在运行时是否能够正常跳转。
至此,CTMediator 这个中间层实现了完全的独立,其他模块不需要预先注册,CTMediator也不需要知道其他模块的实现细节。唯一的关联就是需要在 ‘CTMediator+CTMediatorModuleAActions.h.m’ 中写明正确的 target+action 和正确的参数,而且这些 action 和参数只依赖于 Target_A.h.m。
action 和参数的正确性只会在运行时检查,如果 target 或 action 不存在,可以在 ‘CTMediator.h.m’ 中进行相应的处理。既:CTMediator 不需要依赖任何模块就可以编译运行。
<4>Target_A.h.m 提供了跳转到 DemoModule 模块的对外接口,与 CTMediator+CTMediatorModuleAActions.h.m 相互对应,可以说它只用来为 CTMediator+CTMediatorModuleAActions.h.m 提供服务,所以在实现 CTMediator+CTMediatorModuleAActions.h.m时只需要参考 TargetA.h.m 即可,足够简单以至于并不需要文档来辅助描述。其他模块想跳转到这个模块时,不能直接通过 Target_A.h.m 实现,而是要通过 CTMediator+CTMediatorModuleAActions.h.m 来完成。
这样,就实现了模块间相互不依赖,并且只有需要跳转到其他模块的地方,才需要依赖 CTMediator。
<5>DemoModuleADetailViewController.h.m DemoModule 模块的主视图,这个例子中,会从 ViewController.h.m 跳转到这个模块。
<6>AppDelegate.h.m APP 入口,从应用外通过 Scheme 跳入程序时会经过这个类。
<7>ViewController.h.m APP 主视图,需要在这里跳转到 DemoModule 模块。
3)高级初始化抽离:
AppDelegate 充斥着各种初始化和第三方的注册,这些初始化会被各个业务组件使用,而且第三方库基本都需要注册一个 AppKey ,特别是一些第三方的库需要在 application: didFinishLaunchingWithOptions: 时初始化。
面对这种高难度的耦合场景,我想到了一个基于 runtime 的 AOP 解决方案。
关于AOP的简单介绍参考: 《基于 Aspects 简单展示 AOP 面向切面编程(中英文)》https://juejin.cn/post/6844903560837087246
原理就是利用 runtime,不需要在 AppDelegate 中添加任何代码,就可以捕获 App 生命周期,具体的解决方案还有待探讨。
这里引用《iOS App组件化开发实践》的解决方案,通过创建一个 PBBasicProviderModule 弱业务组件:
- 它通过依赖YTXModule来捕捉App生命周期。
- 它来负责初始化自己的和第三方的东西。
- 所有业务组件都可以依赖这个弱业务组件。
- 它来保证所有东西一定是是初始化完毕的。
- 它来统一管理。
- 它来暴露一些类和功能给业务组件使用。