iOS 模块化组件化是前两年比较流行的话题。当时懒得写相关的东西。现在趁比较有空。对几种方式做一个最最简单的总结。
其实在谈及组件化之前应该先搞清楚一个概念。什么是组件?我认为组件应该可以即插即拔的同类功能集合。它可以在各个项目中使用,并且和业务是不相关或者若相关的。比如网络库、本地存储、基本控件,这些都可以叫做组件。相对的,和业务强相关的功能合集,较难在各个项目中复用的,我更愿意将其归类为模块。
因此,对于我们一般所说的组件化,其实更应该叫模块化。
组件化方式
一般来说,组件化的方式有三种:
- key-block
- protocol-class
- target-action
key-block
key-block 是最简单最容易想到的组件化方式。在模块加载的时候以键值对的形式将 key 和 block 注册在一个全局的注册表中。其中 key 通常是一个 URL:
1 | // 注册 |
主要存在三个问题:
- 注册的 key 和调用的 key 需要输入多遍,可能出现输入错误或者修改错误,在编译的时候不易察觉。可以使用 define 定义字符串。不过这样虽然可以减少写错的风险,但还是会产生硬编码。
- 每一个模块间调用的方法都会保存一个 block 实例。当模块很多,或者模块中提供的调用方法很多的时候,会保存大量 block 对象实例。
- 调用注册方法的时候使用的是 NSDictionary,参数及其类型对于调用者来说是未知的。需要一套文档记录每个方法的参数和类型。
protocol-class
protocol-class 的方式可以说是上面 key-block 的改进,代表框架有阿里出品的 BeeHive。
针对上面的问题1编译的时候无法校验 key 是否写错的问题,可以通过校验 protocol 是否存在,在编译期抛出问题;针对上面的问题2,保存大量 block 实例的问题,使用保存 class 的的形式代替,可以减少大量 block 实例的保存;针对上面的问题3,调用者不知道调用参数和类型的问题,可以在 protocol 中定义方法,让 class 实现,调用者知道 protocol 就可以获取模块间通信的方法,进而知道调用的参数和类型。
1 | // 协议 |
这在 key-block 的基础上已经做了很大的改进。但是仍有改进的空间:
- 比如调用者直接通过 Mediator 拿到 Class 的实例,再通过实例调用方法。这样相当于调用者和被调用者直接接触,没有办法做统一处理,比如组件不存在时的统一处理。
- 仍然需要提前注册
针对问题1,可以尝试用一个 wrapper 把 class 再包裹一层做统一处理。不过这样就更麻烦了。没有太大必要。
target -action
target-action 通过 runtime 调用被调用的方法,这样可以免去提前注册的问题;同时,将调用的统一处理逻辑放在了 Mediator 中,这样可以解决没法统一处理的问题。
但是所有的处理方法都写在 Mediator 中那 Mediator 就会显得太臃肿了。因此,可以将不同模块的方法写在 Mediator 不同的分类中。
1 | // Mediator |
这样调用者只需要引入 Mediator+RegisterModule.h
并且执行 [Mediator sharedInstance] navigateToRegisterVCWithId:@"xxx"]
即可完成调用。对于调用者来说,引入了方法头文件后,同样不需要关注参数和类型,也不需要提前注册。同时还可以通过 Mediator 进行统一处理。
但是它在调用被调用者的时候仍然会产生硬编码。不过由于该模块的 Mediator 分类是由被调用者维护的。因此,在被调用方法变化时,被调用者需要维护 Mediator 分类。
BeeHive 源码解析
Beehive 是阿里开源的一款基于 protocol-class 方式的组件化方案。它具体的思想已经在上面说明了,这里我们可以对它进行简单的解析。
BeeHive 分为两部分:
- Module:负责模块的注册以及事件分发
- Service:负责模块间的通信以及调用
模块注册
Beehive 中的模块是一个实现了 BHModuleProtocol
协议的类,用于管理模块的各个生命周期事件处理。Beehive 中模块注册有三种方式:
- 加载 plist 中配置静态注册
- +load 方法中动态注册
- 使用注解动态注册
plist 静态注册
plist 注册在 AppDelegate 的 application:didFinishLaunchingWithOptions:
方法中进行。模块的注册通过 BHModuleManager
完成。
plist 中 module 的形式如下,包含了 ModuleClass
,ModuleLevel
,ModulePriority
:
BHModuleManager
会将 plist 中的各个 ModuleClass 读取保存到 ModuleInfos
数组中,并在其中按照 level 和 priority 排序。随后实例化每一个 module,添加到 BHModules
模块示例数组中:
1 | /// 加载本地的 Modules 列表 |
+load 方法注册
Beehive 为 +load 方法注册添加了一个宏定义。只需要把这个宏定义插入到实现文件中的任意位置即可:
1 |
|
先不用管这个异步的定义方法。在 +load 方法中直接调用了 registerDynamicModule:
方法注册自己,兜兜转转来到了下面的方法。代码还是非常直接易懂的,和 plist 一样是一个实例化的过程,并且添加到 BHModules
数组中,再按照优先级和 level 排序:
1 | - (void)addModuleFromObject:(id)object |
注解注册
注解注册是一个比较新奇的东西。它的使用方式如下,在类拓展前通过 @BeehiveMod()
进行注册:
1 | @BeeHiveMod(ShopModule) |
那么它是如何进行的呢?看宏代码:
1 |
|
可以发现,上面的 @BeeHiveMod(ShopModule)
展开后就会变为:
1 | @class BeeHive; char * kShopModule_mod __attribute((used, section("__DATA,"BeehiveMods"))) = ""ShopModule"" |
其中 @class
的提前声明 BeeHive
其实没什么用处,应该只是为了写起来更像注解才这么做的。宏的主要作用是声明了一个全局变量 kShopModule_mod
,并且把这个全局变量存到了 Mach-o 的自己创建的 BeehiveMods
段中。
为什么要存到特定的段呢?因为存到特定的段中就能在程序加载的时候找到这个段,然后自行加载注册。具体逻辑在 BHAnnotation
中:
1 | /// dyld 加载 lib 后的回调 |
上面代码给 dyld 添加回调,在加载完镜像后会执行 dyld_callback
方法。
1 | static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) |
dyld 的回调会把段信息传入,就可以在回调函数中解析得到所有的模块信息,并且动态注册了。
注册事件
对于 protocol-class 来说,最重要的是 protocol 和 class 的映射,也就是 Beehive 中 service 的部分。那么为什么还要专门弄出来一个 Module 类呢?为了让模块更抽象,可以监听到外部传来的事件做相应的处理。
因此,在每个模块注册成功的时候还会为每个模块注册事件通知。它的实现方式其实就是一个观察者模式。预先设定了各种可能触发的 event。在注册模块的时候,会遍历所有的 event,在模块中找是否实现了该 event 的方法。对于有 module 实现的 sel,会把 sel 存到 BHSelectorByEvent
中去。再把 module 存到 BHModuleByEvent
字典中 sel 对应的数组中,并把 modules 按照优先级排序。
这样,就可以在任意地方调用方法 [[BHModuleManager sharedManager] triggerEvent:xxxEvent];
,触发 event。
给每个模块注册事件,可以很好地减少 AppDelegate 中的代码量,AppDelegate 中的代码被分散到了各个模块中。
为什么要有优先级呢?因为可能存在模块 A,B,A 依赖于 B,但是如果 A 先初始化,那么就会产生问题。
Service 注册与使用
Service 负责模块之间的通信。Service 必须要满足 BHServiceProtocol
协议。一般我们会将 ViewController 注册为 Service,当然使用专门的一个 Service 类也是可以的。
Service 的注册和 Module 的注册是类似的。都可以通过 plist 静态注册,动态注册,以及注解注册。这里就不详细说明了。
使用方式如下:
1 | id<HomeServiceProtocol> homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)]; |
通过 createService:
传入 protocol 就可以拿到 service 的实例。调用 HomeServiceProtocol
中声明的方法了。