组件化的简短总结及 BeeHive 源码解析

iOS 模块化组件化是前两年比较流行的话题。当时懒得写相关的东西。现在趁比较有空。对几种方式做一个最最简单的总结。

其实在谈及组件化之前应该先搞清楚一个概念。什么是组件?我认为组件应该可以即插即拔的同类功能集合。它可以在各个项目中使用,并且和业务是不相关或者若相关的。比如网络库、本地存储、基本控件,这些都可以叫做组件。相对的,和业务强相关的功能合集,较难在各个项目中复用的,我更愿意将其归类为模块。

因此,对于我们一般所说的组件化,其实更应该叫模块化。

组件化方式

一般来说,组件化的方式有三种:

  1. key-block
  2. protocol-class
  3. target-action

key-block

key-block 是最简单最容易想到的组件化方式。在模块加载的时候以键值对的形式将 key 和 block 注册在一个全局的注册表中。其中 key 通常是一个 URL:

1
2
3
4
5
6
7
8
9
10
11
12
// 注册
+ (void)load {
[[Mediator sharedInstance] registerUrl:@"myapp://gotoregister" withHandler:^(NSDictionary *dic) {
RegisterViewController *registerVC = [[RegisterViewController alloc] initWithSomeParam:dic["someParam"]];
[[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:registerVC animated:YES];
}];
}

// 调用
+ (void)gotoRegister {
[[Mediator sharedInstance] openUrl:@"myapp://gotoregister" withparam:@{@"someParam": 123}];
}

主要存在三个问题:

  1. 注册的 key 和调用的 key 需要输入多遍,可能出现输入错误或者修改错误,在编译的时候不易察觉。可以使用 define 定义字符串。不过这样虽然可以减少写错的风险,但还是会产生硬编码。
  2. 每一个模块间调用的方法都会保存一个 block 实例。当模块很多,或者模块中提供的调用方法很多的时候,会保存大量 block 对象实例。
  3. 调用注册方法的时候使用的是 NSDictionary,参数及其类型对于调用者来说是未知的。需要一套文档记录每个方法的参数和类型。

protocol-class

protocol-class 的方式可以说是上面 key-block 的改进,代表框架有阿里出品的 BeeHive。

针对上面的问题1编译的时候无法校验 key 是否写错的问题,可以通过校验 protocol 是否存在,在编译期抛出问题;针对上面的问题2,保存大量 block 实例的问题,使用保存 class 的的形式代替,可以减少大量 block 实例的保存;针对上面的问题3,调用者不知道调用参数和类型的问题,可以在 protocol 中定义方法,让 class 实现,调用者知道 protocol 就可以获取模块间通信的方法,进而知道调用的参数和类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 协议
@protocol RegisterProtocol
+ (void)navigateToRegisterVCWithString:(NSString *)str;
@end

// 实现类
@interface RegisterViewController : UIViewController<RegisterProtocl>
@end

@implementation RegisterViewController
+ (void)navigateToRegisterVC {
...具体的实现
}
@end

// 注册
+ (void)load {
[[Mediator sharedInstance] registerProtocol:@protocol(RegisterProocol) forInstance:[[self alloc] init]];
}

// 调用
id<RegisterProtocol> registerVC = [[Mediator sharedInstance] instanceForProtocol:@protocol(RegisterProtocol)];
[registerVC navigateToRegisterVCWithString:@"someParam"];

这在 key-block 的基础上已经做了很大的改进。但是仍有改进的空间:

  1. 比如调用者直接通过 Mediator 拿到 Class 的实例,再通过实例调用方法。这样相当于调用者和被调用者直接接触,没有办法做统一处理,比如组件不存在时的统一处理。
  2. 仍然需要提前注册

针对问题1,可以尝试用一个 wrapper 把 class 再包裹一层做统一处理。不过这样就更麻烦了。没有太大必要。

target -action

target-action 通过 runtime 调用被调用的方法,这样可以免去提前注册的问题;同时,将调用的统一处理逻辑放在了 Mediator 中,这样可以解决没法统一处理的问题。

但是所有的处理方法都写在 Mediator 中那 Mediator 就会显得太臃肿了。因此,可以将不同模块的方法写在 Mediator 不同的分类中。

1
2
3
4
5
6
7
8
// Mediator
@implementation Mediator (RegisterModule)
- (void)navigateToRegisterVCWithId:(NSString *)id {
Class cls = NSClassFromString(@"RegisterViewController");
UIViewController *registerVC = [cls performSelector:NSSelectorFromString(@"createVCWithId:") withObject:@{@"id":id}];
[self.navigationController pushViewController:registerVC];
}
@end

这样调用者只需要引入 Mediator+RegisterModule.h 并且执行 [Mediator sharedInstance] navigateToRegisterVCWithId:@"xxx"] 即可完成调用。对于调用者来说,引入了方法头文件后,同样不需要关注参数和类型,也不需要提前注册。同时还可以通过 Mediator 进行统一处理。

但是它在调用被调用者的时候仍然会产生硬编码。不过由于该模块的 Mediator 分类是由被调用者维护的。因此,在被调用方法变化时,被调用者需要维护 Mediator 分类。

BeeHive 源码解析

Beehive 是阿里开源的一款基于 protocol-class 方式的组件化方案。它具体的思想已经在上面说明了,这里我们可以对它进行简单的解析。

BeeHive 分为两部分:

  • Module:负责模块的注册以及事件分发
  • Service:负责模块间的通信以及调用

模块注册

Beehive 中的模块是一个实现了 BHModuleProtocol 协议的类,用于管理模块的各个生命周期事件处理。Beehive 中模块注册有三种方式:

  1. 加载 plist 中配置静态注册
  2. +load 方法中动态注册
  3. 使用注解动态注册

plist 静态注册

plist 注册在 AppDelegate 的 application:didFinishLaunchingWithOptions: 方法中进行。模块的注册通过 BHModuleManager 完成。

plist 中 module 的形式如下,包含了 ModuleClassModuleLevel,ModulePriority

BHModuleManager 会将 plist 中的各个 ModuleClass 读取保存到 ModuleInfos 数组中,并在其中按照 level 和 priority 排序。随后实例化每一个 module,添加到 BHModules 模块示例数组中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/// 加载本地的 Modules 列表
- (void)loadLocalModules
{

NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
return;
}

NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];

NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy;
[self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]];
}];
[modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (!moduleInfoByClass[[obj objectForKey:kModuleInfoNameKey]]) {
[self.BHModuleInfos addObject:obj];
}
}];
}

// 实例化模块并添加到 BHModules 数组中
- (void)registedAllModules
{
/// 按照优先级对模块列表中的模块排序
[self.BHModuleInfos sortUsingComparator:^NSComparisonResult(NSDictionary *module1, NSDictionary *module2) {
NSNumber *module1Level = (NSNumber *)[module1 objectForKey:kModuleInfoLevelKey];
NSNumber *module2Level = (NSNumber *)[module2 objectForKey:kModuleInfoLevelKey];
if (module1Level.integerValue != module2Level.integerValue) {
return module1Level.integerValue > module2Level.integerValue;
} else {
NSNumber *module1Priority = (NSNumber *)[module1 objectForKey:kModuleInfoPriorityKey];
NSNumber *module2Priority = (NSNumber *)[module2 objectForKey:kModuleInfoPriorityKey];
return module1Priority.integerValue < module2Priority.integerValue;
}
}];

NSMutableArray *tmpArray = [NSMutableArray array];

//module init
[self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary *module, NSUInteger idx, BOOL * _Nonnull stop) {

NSString *classStr = [module objectForKey:kModuleInfoNameKey];

Class moduleClass = NSClassFromString(classStr);
BOOL hasInstantiated = ((NSNumber *)[module objectForKey:kModuleInfoHasInstantiatedKey]).boolValue;
if (NSStringFromClass(moduleClass) && !hasInstantiated) {
id<BHModuleProtocol> moduleInstance = [[moduleClass alloc] init];
[tmpArray addObject:moduleInstance];
}

}];

[self.BHModules removeAllObjects];

/// 把class添加到 BHModules 中
[self.BHModules addObjectsFromArray:tmpArray];

[self registerAllSystemEvents];
}

+load 方法注册

Beehive 为 +load 方法注册添加了一个宏定义。只需要把这个宏定义插入到实现文件中的任意位置即可:

1
2
3
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}

先不用管这个异步的定义方法。在 +load 方法中直接调用了 registerDynamicModule: 方法注册自己,兜兜转转来到了下面的方法。代码还是非常直接易懂的,和 plist 一样是一个实例化的过程,并且添加到 BHModules 数组中,再按照优先级和 level 排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- (void)addModuleFromObject:(id)object
shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
{
Class class;
NSString *moduleName = nil;

if (object) {
class = object;
moduleName = NSStringFromClass(class);
} else {
return ;
}

...

if ([class conformsToProtocol:@protocol(BHModuleProtocol)]) {
NSMutableDictionary *moduleInfo = [NSMutableDictionary dictionary];

BOOL responseBasicLevel = [class instancesRespondToSelector:@selector(basicModuleLevel)];

int levelInt = 1;

if (responseBasicLevel) {
levelInt = 0;
}

[moduleInfo setObject:@(levelInt) forKey:kModuleInfoLevelKey];
if (moduleName) {
[moduleInfo setObject:moduleName forKey:kModuleInfoNameKey];
}

[self.BHModuleInfos addObject:moduleInfo];

id<BHModuleProtocol> moduleInstance = [[class alloc] init];
[self.BHModules addObject:moduleInstance];
[moduleInfo setObject:@(YES) forKey:kModuleInfoHasInstantiatedKey];
[self.BHModules sortUsingComparator:^NSComparisonResult(id<BHModuleProtocol> moduleInstance1, id<BHModuleProtocol> moduleInstance2) {
NSNumber *module1Level = @(BHModuleNormal);
NSNumber *module2Level = @(BHModuleNormal);
if ([moduleInstance1 respondsToSelector:@selector(basicModuleLevel)]) {
module1Level = @(BHModuleBasic);
}
if ([moduleInstance2 respondsToSelector:@selector(basicModuleLevel)]) {
module2Level = @(BHModuleBasic);
}
if (module1Level.integerValue != module2Level.integerValue) {
return module1Level.integerValue > module2Level.integerValue;
} else {
NSInteger module1Priority = 0;
NSInteger module2Priority = 0;
if ([moduleInstance1 respondsToSelector:@selector(modulePriority)]) {
module1Priority = [moduleInstance1 modulePriority];
}
if ([moduleInstance2 respondsToSelector:@selector(modulePriority)]) {
module2Priority = [moduleInstance2 modulePriority];
}
return module1Priority < module2Priority;
}
}];
...
}
}

注解注册

注解注册是一个比较新奇的东西。它的使用方式如下,在类拓展前通过 @BeehiveMod() 进行注册:

1
2
3
@BeeHiveMod(ShopModule)
@interface ShopModule() <BHModuleProtocol>
@end

那么它是如何进行的呢?看宏代码:

1
2
3
4
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

可以发现,上面的 @BeeHiveMod(ShopModule) 展开后就会变为:

1
@class BeeHive; char * kShopModule_mod __attribute((used, section("__DATA,"BeehiveMods"))) = ""ShopModule""

其中 @class 的提前声明 BeeHive 其实没什么用处,应该只是为了写起来更像注解才这么做的。宏的主要作用是声明了一个全局变量 kShopModule_mod,并且把这个全局变量存到了 Mach-o 的自己创建的 BeehiveMods 段中。

为什么要存到特定的段呢?因为存到特定的段中就能在程序加载的时候找到这个段,然后自行加载注册。具体逻辑在 BHAnnotation 中:

1
2
3
4
5
/// dyld 加载 lib 后的回调
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}

上面代码给 dyld 添加回调,在加载完镜像后会执行 dyld_callback 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
/// 拿到所有的 BeehiveMods 中的 module 名
NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
/// 将 module 名注册
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);

if (cls) {
[[BHModuleManager sharedManager] registerDynamicModule:cls];
}
}
}
...
}

NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif

unsigned long counter = size/sizeof(void*);
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;

BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}

return configs;
}

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 中声明的方法了。