为什么使用 BeeHive
在狼人杀项目时,iOS 团队有 9 个人协作开发,如果没有一个好的模块化方案支撑,诸如代码冲突 / 逻辑复用等问题将占用不少开发时间,势必面临效率问题。当时的解决方案,就使用了类似 BeeHive 的模块化方案进行团队开发。
业务模块化的好处:
- 维护便利,划分业务模块,业务逻辑在一个模块内。
- 代码复用,相似度高的模块可以低成本迁移到其它项目。
- 易于扩展,业务解耦使得新功能的开发负担小。
什么时候应进行模块化
一套框架肯定有学习和使用成本,这里涉及一个产出和投入的问题。那么如何具体的衡量是否使用呢?
个人愚见,有以下情况是值得去思考做模块化的:
- 业务规模,一是业务代码本身的体量大,二是业务发展速度,业务增速快,有必要考虑行模块化,适应日后复杂情形。
- 团队规模,团队人数也是业务规模的体现,除代码提交的冲突增加,沟通成本也直线上升,需要更具效率的协同。
模块化的标准是什么
目前业界并没有形成标准共识,一定之规。
而一定要量化标准的话,个人的粗浅实践认为是:
业务模块数<=研发人员数*3
如果模块化造成了负效果,违背我们让业务跑得更好更快的初衷,那就是本末倒置了。
BeeHive 是什么
BeeHive 是阿里巴巴开源的一款模块化框架,按照官方的介绍:
BeeHive
是用于iOS
的App
模块化编程的框架实现方案,吸收了Spring
框架Service
的理念来实现模块间的API
耦合。
架构图:
与上面的架构图所对应的,BeeHive 重点关注的有两个部分:
- 业务模块
- 模块如何注册
- 模块如何分发系统事件&应用事件
- 服务
- 服务如何注册
- 服务如何被模块调用
BeeHive 如何实现模块化
BeeHive 模块注册
具体进行业务开发时, 所有业务模块 Module 统一交由 BeeHive 来进行管理,如何将 Module 注册到 BeeHive 是关键。
BeeHive 提供了 3 种方式来进行注册:
- 动态注册
- 静态注册
- Annotation
BeeHive 框架在模块解耦方面,比较特别的是 Annotaition 方式。
动态注册
动态注册使用
动态注册一个 NewModule 的代码如下:
1 |
|
动态注册实现
动态注册使用需要利用宏 BH_EXPORT_MODULE
:
1 |
|
可以发现,上面的注册时机是固定在 load 方法的。
对于模块的动态注册,调用实现如下:
最后会走到 -[BHModuleManager addModuleFromObject:shouldTriggerInitEvent:]
实现:
1 | - (void)addModuleFromObject:(id)object |
静态注册
静态注册使用
首先,静态注册要在配置中 指定 plist 文件名 :
1 | [BHContext shareInstance].moduleConfigName = @"BHModule"; |
然后在 Plist 文件里面配置好 module 对应的信息:
- moduleClass: 模块类的类名。
- moduleLevel: 如果不去设置 Level 默认是 BHModuleNormal。 只会有两种 Level ,BHModuleBasic = 0 ,BHModuleNormal = 1。
- modulePriority: 对应 BHModuleProtocol 的同名方法,moduleLevel 相等情况下比较 modulePriority 。
静态注册实现
重点关注 2 个方法:
-[BHModuleManager loadLoacalModules]
从 plist 文件扫描出来对应模块字典信息:
1 | - (void)loadLocalModules |
-[BHModuleManager registedAllModules]
根据模块信息,实例化静态注册的模块类:
1 | - (void)registedAllModules |
在经历registedAllModules方法之后,所有注册的module都生成了对应的实例对象。
PS:如果对模块顺序有要求时要注意,动态注册的模块,整体的顺序会排在静态注册的前面,因为 load 方法的时机在更前面。
Annotation 注册模块
Annotation 注册使用
模块如果使用 Annotaiton 方式注册,代码非常的简单:
1 |
|
Annotation 注册实现
查看 BeeHiveMod() 的宏定义:
1 |
|
其中的 BeeHiveDATA 也是一个宏:
1 |
直接展开变成:
1 | @class BeeHive; char * kYourModule_mod __attribute((used, section("__DATA,"BeehiveMods" "))) = ""YourModule""; |
Annotation 相关扩展
编译属性 __attribute__
这里的关键是 __attribute__
,日常开发中不少场景也有它的身影,它的使用格式也很简单:
1 | __attribute__((属性列表)) |
例如作为 方法过期的警告 对已废弃方法使用:
实现效果:
查看相关的 官方解释:
An attribute specifier is of the form
__attribute__ ((attribute-list))
. An attribute list is a possibly empty comma-separated sequence of attributes, where each attribute is one of the following:
- Empty. Empty attributes are ignored.
- An attribute name (which may be an identifier such as
unused
, or a reserved word such asconst
).- An attribute name followed by a parenthesized list of parameters for the attribute. These parameters take one of the following forms:
- An identifier. For example,
mode
attributes use this form.- An identifier followed by a comma and a non-empty comma-separated list of expressions. For example,
format
attributes use this form.- A possibly empty comma-separated list of expressions. For example,
format_arg
attributes use this form with the list being a single integer constant expression, andalias
attributes use this form with the list being a single string constant.
大意是说的 __attribute__ ((attribute-list))
使用的 attribute-list 属性列表有三种情况:
- 为空,空的会被忽略。
- 单纯的属性名,例如
unused
这样的:__attribute__((unused))
。 - 属性名带参数使用:
- 特定的标记,例如 :
__attribute__((mode(DI)))
- 特定的标记,可能是逗号分割的表达式。例如 :
__attribute__((format(printf, a, b)))
- 一个可能为空的逗号分隔的表达式列表。例如:
__attribute__((noreturn, format(printf, 1, 2)) );
- 特定的标记,例如 :
而在 BeeHive 当中使用到的代码则是:
1 | __attribute((used, section("__DATA,"BeehiveMods" "))) |
其实就是分别使用了 2 个关键字,一个关键字是 used,另外一个则是 section() 。
used
used 具体描述见 gun 官方文档 :
used
This attribute, attached to a variable with static storage, means that the variable must be emitted even if it appears that the variable is not referenced.
When applied to a static data member of a C++ class template, the attribute also means that the member is instantiated if the class itself is instantiated.
假如 used 比较陌生的话,那 unused 的警告,作为 iOS 开发者我们应该很熟悉了。
对于 __attribute((used))
它的作用与 unused 相反, 是向编译器说明这段代码有用,即使在没有用到的情况下编译器也不会警告。
代码如果没有加 used 变量会被编译器给优化了。
section
要了解 section ,要知道 Mach-O 的构成:
Mach-O 主要由 3 部分组成:
- Mach-O 头(Mach Header):Mach Header 描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息。
- 加载命令(Load Commands):Load Commands 描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。
- 数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。
作为与 Mach-O 文件格式有关的 Segment 的定义,可以在 /usr/include/mach-o/loader.h 中看到:
1 | #define SEG_PAGEZERO "__PAGEZERO" /* the pagezero segment which has no */ |
而关于 section 的结构也可以看到:
1 | struct section { /* for 32-bit architectures */ |
明白 section 的定义,就很容易理解__attribute((section("__DATA,"BeehiveMods" ")))
的意思,就是将数据放进 segname 为 __DATA
, sectname 为 BeehiveMods
的 section 中。
读取 section
在 BeeHive 中既然把数据存进 section,自然也需要将 section 存储的数据取出来。
这里的关键在于 BHReadConfiguration
方法:
1 | NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp) |
利用 constructor 完成模块加载
可以看到代码中有一个 initProphet
函数的代码如下:
1 | __attribute__((constructor)) |
其中的玄机就在 __attribute__((constructor))
,它到作用是系统在执行 main()
函数之前调用该函数 。
官方解释如下:
constructor
destructorThe
constructor
attribute causes the function to be called automatically before execution entersmain ()
. Similarly, thedestructor
attribute causes the function to be called automatically aftermain ()
has completed orexit ()
has been called. Functions with these attributes are useful for initializing data that will be used implicitly during the execution of the program.These attributes are not currently implemented for Objective-C.
感兴趣的可以去看看相关文档和资料 attribute((constructor))用法解析 ,这里不再展开。
而 _dyld_register_func_for_add_image()
则表示每一次镜像的加载,都会触发传入的回调函数。
在 BeeHive 的上述代码里,每次 App 启动都会触发 dyld_callback
回调函数:
1 | static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) |
回调函数的主要作用,就是每次启动,去注册模块和服务的数据信息。
BeeHive 模块分发事件
将模块注册到 BeeHive 后,就可以接收到事件:
- UIApplicationDelegate 系统事件
- BeeHive 模块事件
- 业务自定义事件
BHModuleProtocol 定义事件
上述的 3 种事件,都在 BHModuleProtocol 当中定义了方法,Module 类可根据需求去实现:
1 | @protocol BHModuleProtocol <NSObject> |
模块事件分发实现
BHAppDelegate 替换
在 main 函数入口,要替换 AppDelegate 为 BHAppDelegate 或者 BHAppDelegate 的子类,是使用 BeeHive 的一个前提:
1 | int main(int argc, char * argv[]) { |
这样才能让业务模块收到 BeeHive 分发对应的事件。
BHAppDelegate 实现监听
如下代码,在 BHAppDelegate 中,触发 系统事件 时,由 BHModuleManager 分发 BeeHive 事件到模块当中:
1 | @implementation BHAppDelegate |
可以看到,上述监听都收拢到了 -[BHModuleManager triggerEvent:]
调用当中。
BHModuleManager 事件分发逻辑
除了 eventType 为 BHMInitEvent / BHMTearDownEvent 时特殊,其它的处理方法都一样。
defalut 默认的模块方法执行实现流程如下图:
BHMInitEvent
BeeHive 模块 初始化 事件:
1 | - (void)handleModulesInitEventForTarget:(id<BHModuleProtocol>)target |
BHMTearDownEvent
BeeHive 模块 销毁 事件:
1 | - (void)handleModulesTearDownEventForTarget:(id<BHModuleProtocol>)target |
BHMTearDownEvent 事件特别的是,按优先级从低到高销毁。
业务自定义事件
BeeHive 还提供了自定义事件的方式来进行分发。
自定义事件分发
1 | -(void)someMethod |
-[BHModuleManager tiggerCustomEvent:]
方法的实现为:
1 | - (void)tiggerCustomEvent:(NSInteger)eventType |
判断 eventType 后,直接透传给 BHModuleManager 进行处理。
自定义事件 - 手动注册
如果定义好的枚举值 BHMDidCustomEvent 无法满足需求,也使用满足条件(大于1000)的事件值:
1 | //Module |
具体实现:
1 | - (void)registerCustomEvent:(NSInteger)eventType |
BeeHive 模块间调用 Service
BeeHive 的模块间通过 Service 调用,来协同其它模块完成功能。
Service 是什么
这里的 Service 实际上指 Protocol,且都需要遵循 BHServiceProtocol ,官方例子示例 HomeServiceProtocol
:
1 | @protocol HomeServiceProtocol <NSObject, BHServiceProtocol> |
Protocol 的好处是,不同的模块,不会再依赖某一个类实例,而是 依赖接口
,对于使用者屏蔽具体实现。
Service 注册
ServiceProtocol 必须注册,才能由 BHServiceManager 管理,从而被不同模块使用。与模块注册相同,也有 3 种注册方式:
- 代码动态注册
- Plist 文件静态注册
- Annoatation 注册
Service 动态注册
在需要的地方直接利用代码注册 Service ,
1 |
|
官方的示例是在模块事件的 modInit
中进行注册。
动态注册实现
最终的实现为:
1 | - (void)registerService:(Protocol *)service implClass:(Class)implClass |
主要作用就是协议名和对应类名放到 allServicesDict 保存,等待创建使用。
Serivce 静态注册
通过 BHContext
配置注册 Service 的 Plist 文件:
1 | //Service 静态配置文件 |
如图,在 Plist 文件中,配置对应的 ServiceProtocol 与实现类名:
这就完成 BeeHive 服务注册了。
静态注册实现
最终的实现为:
1 | - (void)registerLocalServices |
Serivce Annotation 注册
类似业务模块 Annoatation 注册,Service 注册使用 BeeHiveService()
宏:
1 | @BeeHiveService(HomeServiceProtocol,BHViewController) |
Annotation 注册实现
展开 BeeHiveService
宏:
1 |
|
上述 HomeServiceProtocol
注册示例最终展开为:
1 |
|
本质实现,也与模块 Annoatation 注册一样,将数据存储在 segment 中。然后在 App 启动时,通过回调函数 dyld_callback 注册:
最后会走进 -[BHServcieManager registerService:implClass:]
的方法,和上面服务动态注册的方法一样,添加到BHServcieManager
的 allServicesDict
字典当中。
使用 Service
通过 Service 调用其它模块非常简单:
1 |
|
Service 调用实现
Service 的调用过程如下:
最终调用 -[BHServiceManager createService:withServiceName:shouldCache:]
的实现:
1 | - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache { |
其它
本文重点在 BeeHive 的模块和服务时如何完成解耦,如何完成交互协作,关于 BeeHive 的其它细节不会再赘述。
关于如何使用,官方文档介绍已比较详细,感兴趣可直接去看看 BeeHive 。
总结
作为业务模块化框架,BeeHive 并不是唯一的解决方案,但以 BeeHive 框架为代表的基于面向协议思想的 服务注册 方案,符合开闭原则,让使用者针对接口编程,而不是针对实现编程。且服务注册方案对于代码自动补全和编译时检查都有效,调用简单方便,对业务开发人员来说,工作体感会更好。
BeeHive 劣势在于确实没有做到完全无依赖,客观存在维护公共协议的成本。但只能说仁者见仁,智者见智。