前言
BeeHive是阿里开源的一个组件化框架工具,其内部是使用Spring框架Service的理念来实现模块解耦的,实际上就是使用protocol-class
的方案。另外,在组件化的基础上,BeeHive还增加了一个事件分发的功能来配合使用。
目录
- 1. 概览
- 2. 事件分发的作用
- 3. 事件分发的配置过程
- 4. 注册
Module
的几种方式 - 5. 事件分发的内部实现
- 6. 事件种类
1. 概览
官方文档中的架构图:
从上图可以看出,BeeHive的工作分为两部分:
事件分发
BeeHive本身会监听一些系统事件和应用事件,比如App生命周期、推送、handoff等,当事件发生时,BeeHive将其分发给各个模块,然后各个业务模块就可以在自己的Module类中调用各自的响应方法。组件化
这部分是指在组件化的情况下,实现模块间调用,也就是说,各个模块是相互解耦的,BeeHive使用protocol-class
的方案实现这一点。
2. 事件分发的作用
当一个事件被触发时,其对应的响应方法需要被执行,比如界面更新、数据存储等。在这个过程中,会涉及到响应方法的调用和实现这两部分。
首先需要确定的是响应方法的实现都是由模块来完成的(不属于现有模块的响应方法可以看做是属于一个全局模块),针对调用响应方法的位置,这里就有两种调用方式,一个是直接在事件触发点调用(通常是在AppDelegate
),另一个是通过BeeHive将事件分发给各个模块,在具体模块的Module
类中调用。
为了对比这两种调用方法的差别,下面以一个例子来说明:
一个场景
用户在spotlight搜索一个关键字testA,点击搜索结果,app需要跳转到模块A中的一个界面;
搜索关键字testB,点击搜索结果,app需要跳到模块B中的一个界面。
具体操作如下:
在这个场景中,当用户在spotlight中点击一个搜索结果后,会触发一个handoff事件,这个事件会被AppDelegate
接受到,可以把AppDelegate
当做是事件的触发点。
这个事件期望的响应是,根据点击的搜索结果,跳转到对应的模块界面中。
2.1. 直接调用
直接调用事件的响应方法,代码如下:(完整项目代码可以查看BeeHive_demo1)
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler{
if ([userActivity.activityType isEqualToString:@"com.company.app.moduleA.one"]) {
id<ModuleAServiceProtocol> moduleAService = [[BeeHive shareInstance] createService:@protocol(ModuleAServiceProtocol)];
[moduleAService pushToModuleAOneViewController];
}else if ([userActivity.activityType isEqualToString:@"com.company.app.moduleB.one"]) {
id<ModuleBServiceProtocol> moduleBService = [[BeeHive shareInstance] createService:@protocol(ModuleBServiceProtocol)];
[moduleBService pushToModuleBOneViewController];
}
return YES;
}
上述代码中BeeHive的createService:
方法是用来获取对应的模块句柄(下文会具体讲到),使用这个句柄可以直接调用模块的响应方法,跳转到模块对应的界面。
根据userActivity
的类型来判断app是跳转到moduleA的界面还是moduleB的界面。
直接调用存在的问题
在事件触发点处(
AppDelegate
)直接调用模块的响应方法,会将事件的响应代码全都堆积在触发点处,当事件的类别越来越多,这个地方会存在许多判断语句,不便于阅读和维护;
更重要的是会导致触发点对各个模块产生依赖,继而会影响触发点的稳定性,只要模块稍有改动,触发点也要跟着变动。在执行事件的响应方法的过程中,会涉及到响应方法的调用逻辑和实现逻辑这两部分。响应方法的实现逻辑通常是在模块中完成的,采用第一种方式,响应方法的调用逻辑会在触发点处完成,整个事件的处理过程会被分割在触发点和模块这两部分中,当需要对事件的响应逻辑做出变动时,则需要在这两部分同时做出改变。
事件触发点一般是位在主工程中,在大型项目中,模块和主工程一般是分开开发的,并且可能是由不同的开发者开发的,一个事件最好不要同时涉及到这两部分,因为这样会导致这两部分存在某种耦合,不易于维护。
2.2. 事件分发
在使用BeeHive之后,BeeHive会监听这些事件,事件触发后,它会遍历已注册模块对应的Module
类,然后调用这些类对应的事件响应方法,这样,BeeHive就将一个事件分发给了所有的Module
类。
换句话说,就是给每一个需要响应事件的模块都新建一个对应的Module
类,在这个Module
类中完成对响应方法的调用,这个Module
类和模块将由同一个开发者创建和维护。
这样,一个事件的整个响应过程就都是由模块负责,主工程只需要负责事件分发。事件的处理被隔离在模块内部,即便以后需要修改事件的响应逻辑,也只需要改动模块,主工程不需要任何改动。
3. 事件分发的配置过程
使用BeeHive实现事件分发,共需要三步,下面还是以上文的场景来作为例子进行讲解:(完整项目代码可以查看BeeHive_demo2)
- 初始化
BeeHive内部有一个类BHAppDelegate
,它的作用就是监听事件的触发,实际项目中的AppDelegate
需要继承这个类。
//AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
[BeeHive shareInstance].enableException = YES;
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
[super application:application didFinishLaunchingWithOptions:launchOptions];
...
...
return YES;
}
- 创建并注册
Module
类
每一个需要响应事件的模块,都需要新建一个对应的Module
类,且Module
类需要遵守协议BHModuleProtocol
,然后使用BeeHive提供的方法将这个Module
类注册到BeeHive中,这样,BeeHive才能将事件转发给这个模块。
#import "ModuleAModule.h"
#import "BHService.h"
//注册
@BeeHiveMod(ModuleAModule)
@interface ModuleAModule() <BHModuleProtocol>
@end
@implementation ModuleAModule
...
...
@end
本例中使用宏BeeHiveMod
来注册ModuleAModule
类,代码如下:
@BeeHiveMod(ModuleAModule)
- 调用响应方法
在Module
类中,调用事件对应的响应方法,每个Module
类只应该处理和本模块相关的事件。
在[步骤2]创建的Module
类中添加响应方法:
//handoff事件响应
- (void)modContinueUserActivity:(BHContext *)context{
NSUserActivity *userActivity = context.userActivityItem.userActivity;
if ([userActivity.activityType isEqualToString:@"com.company.app.moduleA.one"]) {
id<ModuleAServiceProtocol> moduleAService = [[BeeHive shareInstance] createService:@protocol(ModuleAServiceProtocol)];
[moduleAService pushToModuleAOneViewController];
}
}
4. 注册Module
的几种方式
BeeHive提供了四种方式来注册成为Module
类,其内部实现大多数都是间接调用:
[[BHModuleManager sharedManager] registerDynamicModule:moduleClass]
4.1. 方式一
通过BeeHive
类的类方法+ (void)registerDynamicModule:(Class) moduleClass;
其方法实现是直接将消息转发给BHModuleManager
类:
+ (void)registerDynamicModule:(Class)moduleClass
{
[[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
4.2. 方式二
使用在协议BHModuleProtocol
中定义的宏BH_EXPORT_MODULE
,其定义为:
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
这种方式是对第一种方式的调用,同时还增加了对-[async]
方法的定义,返回YES
表示这个Module
类将会被异步加载,用来优化启动时间。Module
类都需要遵守这个协议BHModuleProtocol
,所以可以在其内部直接使用这个宏。
4.3. 方式三
使用在BHAnnotation
类中定义的宏BeeHiveMod
,这个宏需要一个类名作为参数,其用法如下:
@BeeHiveMod(ModuleAModule)
只需要这一句代码,ModuleAModule
类就会被注册成功。
如何实现?
使用这个宏@BeeHiveMod(ModuleAModule)
,整个注册过程会分为三步
存储需要注册的类名
宏BeeHiveMod
的作用就是存储类名,它会在项目mach-o文件的segment:__DATA
中添加一个名为BeehiveMods
的section,并将字符串"ModuleAModule"
添加到这个section中。取出存储的类名
从项目mach-o文件的(__DATA,BeehiveMods)
中,获取所有的类名。注册
使用[步骤2]的类名,调用BHModuleManager
类的registerDynamicModule:
方法来注册。
4.3.1. 存储类名
宏BeeHiveMod
的定义
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
在编译的时,预处理器会根据分段标志将被定义的字符串进行分段,然后每一段都会和宏的参数对比,如果相同就会被替换,空格、括号、运算符和##
等都属于一种分段标志。
##
一般被用在被替换段和其他段直接接触的情况下,比如替换变量名的一部分,上述宏定义中,就是使用两个##
将一个变量名隔离成三段,k
,name
和_mod
,然后使用参数替换name
那一段。
上述宏定义中,还用到了#
符号,#
后面跟着的必须是宏的参数,它的作用是将参数的值符号化,也就是用一对引号""
将参数包围起来。
另外,需要注意的是在使用#
进行符号化的时候,其前面和后面的引号的数量必须是偶数,否则,预处理器不会替换和符号化参数。
下面使用预处理命令来验证一下:
新建一个macro.c
文件,将上述宏定义写入,并调用
//macro.c
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA, "#sectname" ")))
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
@BeeHiveMod(ModuleAModule)
在终端输入如下预处理处理命令,预处理阶段会进行宏解析
clang -E macro.c
输出的关键部分
@class BeeHive; char * kModuleAModule_mod __attribute((used, section("__DATA, ""BeehiveMods"" "))) = """ModuleAModule""";
上述输出中,__attribute((used, section("__DATA, ""BeehiveMods"" ")))
表示在项目的mach-o文件的名字为__DATA
的segment中添加一个名字为BeehiveMods
的section,并将其值设置为字符串"ModuleAModule"
。
下面使用otool
命令来验证一下
首先找到本文项目BeeHive-demo2生成的mach-o文件,在终端执行如下命令来输出这个mach-o文件的所有segment和section:
otool -l BeeHive-demo2
下面是这个命令的部分输出:
......
Section
sectname BeehiveMods
segname __DATA
addr 0x000000010002b3b8
size 0x0000000000000010
offset 177080
align 2^3 (8)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
......
在mach-o文件中确实存在BeehiveMods
这个section,接下来,继续验证这个section中的值是否是字符串"ModuleAModule"
。
使用下来命令查看section的内容:
otool -s __DATA BeehiveMods BeeHive-demo2
__DATA
表示segment,BeehiveMods
表示section,其输出为:
BeeHive-demo2:
Contents of (__DATA,BeehiveMods) section
000000010002b3b8 0001ea48 00000001 0001ead2 00000001
上述输出表示,section内部包含了两个地址,分别为010001ea48
和010001ead2
,这两个地址的其中一个就是指向字符串"ModuleAModule"
。
为了找到这两个地址指向的具体值,可以使用下列命名查看mach-o文件中segment为__TEXT
,section为__cstring
的内容
otool -V -s __TEXT __cstring BeeHive-demo2
截取输出的开头部分
BeeHive-demo2:
Contents of (__TEXT,__cstring) section
000000010001ea48 ModuleBModule
000000010001ea56 com.company.app.moduleB.one
000000010001ea72 hash
000000010001ea77 TQ,R
000000010001ea7c superclass
000000010001ea87 T#,R
000000010001ea8c description
000000010001ea98 T@\"NSString\",R,C
000000010001eaa9 debugDescription
000000010001eaba moduleA
000000010001eac2 moduleB
000000010001eaca moduleC
000000010001ead2 ModuleAModule
000000010001eae0 com.company.app.moduleA.one
可以看到010001ea48
和010001ead2
这两个地址分别在上述输出中的第三行和倒数第二行,对应的字符串分别为"ModuleBModule"
和"ModuleAModule"
。
这样,类名就被存储在mach-o文件的section中了。
4.3.2. 取出类名
BeeHive在文件BHAnnotation.m
中注册了一个函数dyld_callback
,代码如下:
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}
当一个函数被__attribute__((constructor))
修饰时,表示这个函数是这个image的初始化函数,在image被加载时,首先会调用这个函数。(image指的是mach-o和动态共享库,在工程运行时,可以使用lldb命令image list
查看这个工程中加载的所有image。)
上述代码表示initProphet
函数被指定为mach-o的初始化函数,当dyld(动态链接器)加载mach-o时,执行initProphet
函数,其执行时机在man函数和类的load
方法之前。
当_dyld_register_func_for_add_image(dyld_callback);
被执行时,如果已经加载了image,则每存在一个已经加载的image就执行一次dyld_callback
函数,在此之后,每当有一个新的image被加载时,也会执行一次dyld_callback
函数。
(dyld_callback
函数在image的初始化函数之前被调用,mach-o是第一个被加载的image,调用顺序是:load mach-o -> initProphet -> dyld_callback -> load other_image -> dyld_callback -> other_image_initializers -> ......)
所以,当程序启动时,会多次调用dyld_callback
函数。
在dyld_callback
函数中,使用下列函数来获取[步骤2]中存储的类名
extern uint8_t *getsectiondata(
const struct mach_header_64 *mhp,
const char *segname,
const char *sectname,
unsigned long *size);
segname的值为__DATA
,sectname的值为BeehiveMods
。
4.4.3. 注册
在dyld_callback
函数中,调用BHModuleManager
的注册方法,并传入上文中回去的类名
[[BHModuleManager sharedManager] registerDynamicModule:cls];
4.4. 方式四
使用plist文件注册,首先需要指定plist文件的路径,使用如下代码来指定路径:
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";
这一句代码一般是在初始化BeeHive时调用
plist文件的格式:
需要注意的是BeeHive.bundle必须添加到项目的主工程的target上,因为BeeHive内部是在[NSBundle mainBundle]
的目录下寻找BeeHive.bundle。
当使用cocoapods来加载BeeHive时,默认情况下,BeeHive.bundle是存在于BeeHive.framework中,这个时候使用[NSBundle mainBundle]
时获取不到BeeHive.bundle的,解决办法是改用[NSBundle bundleForClass:self.class]
或将BeeHive.bundle添加到项目的主工程的target上。
5. 事件分发的内部实现
在BeeHive中,使用BHModuleManager
类来负责事件分发,其主要步骤是:
- 注册
存储Module
类的对象和事件对应的响应方法 - 触发
通过事件类型,获取需要响应的Module
类对象和对应的响应方法,使用performSelector:withObject:
执行响应方法。
5.1. 注册
BHModuleManager
使用四个实例属性来存储Module
类的对象和对应的响应方法
@property(nonatomic, strong) NSMutableArray<NSDictionary *> *BHModuleInfos;
@property(nonatomic, strong) NSMutableArray *BHModules;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSMutableArray<id<BHModuleProtocol>> *> *BHModulesByEvent;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSString *> *BHSelectorByEvent;
BHModules
属性存储了所有注册的Module
类的对象。
BHModuleInfos
属性保存了对应Module
类的一些状态,比如kModuleInfoHasInstantiatedKey
表示Module
类是否已创建,每一个Module
类对应其内部的一个字典。
BHModulesByEvent
属性表示每当一个事件触发时,哪些Module
类需要响应这个事件。它是一个字典类型,以事件类型eventType
作为key,响应这个事件的Module
类的对象组成的数组作为value。
BHSelectorByEvent
属性存储了事件类型和响应方法名的映射关系,它是一个字典类型,以事件类型eventType
作为key,事件的响应方法的字符串名作为value。
相对比较重要的是后面两个属性,当事件触发时,会使用这两个属性。
这四个属性的值都是在注册Module
类时设置的,从上文可知,注册Module
类时,其内部是调用BHModuleManager
类的registerDynamicModule:
方法。
在registerDynamicModule:
方法的内部,首先将传入的Module
类参数实例化得到一个对象moduleInstance
,然后将其添加到BHModules
中,并创建一个对应的字典添加到BHModuleInfos
中。(这里就存储好了BHModules
和BHModuleInfos
这两个属性。)
最后,调用registerEventsByModuleInstance:
来给moduleInstance
对象注册响应事件。
- (void)registerEventsByModuleInstance:(id<BHModuleProtocol>)moduleInstance
{
NSArray<NSNumber *> *events = self.BHSelectorByEvent.allKeys;
[events enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self registerEvent:obj.integerValue withModuleInstance:moduleInstance andSelectorStr:self.BHSelectorByEvent[obj]];
}];
}
BHSelectorByEvent
属性存储了所有事件类型和响应方法名的映射关系,遍历这个属性的所有事件类型,通过事件类型拿到BHModulesByEvent
属性对应的响应方法列表,然后将moduleInstance
添加到事件的响应方法列表中。(这里就存储好了BHModulesByEvent
属性)
如果需要添加自定义事件类型,也就是在BHSelectorByEvent
属性中添加一个映射关系,可以使用方法registerCustomEvent:withModuleInstance:andSelectorStr:
。
5.2. 触发
使用BeeHive时,事件是如何被触发的?
这里还是以handoff为例,查看BeeHive中的BHAnnotation
类的handoff代理方法:
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
{
if([UIDevice currentDevice].systemVersion.floatValue >= 8.0f){
//保存参数
[[BeeHive shareInstance].context.userActivityItem setUserActivity: userActivity];
[[BeeHive shareInstance].context.userActivityItem setRestorationHandler: restorationHandler];
//分发事件
[[BHModuleManager sharedManager] triggerEvent:BHMContinueUserActivityEvent];
}
return YES;
}
可以看出,事件是通过BHModuleManager
类的triggerEvent:
方法分发出去的。triggerEvent:
方法接受了一个参数BHMContinueUserActivityEvent
,用来表示事件的类型。
查看triggerEvent:
的内部实现,发现最终会调用BHModuleManager
类的方法handleModuleEvent:forTarget:withSeletorStr:andCustomParam:
:
- (void)handleModuleEvent:(NSInteger)eventType
forTarget:(id<BHModuleProtocol>)target
withSeletorStr:(NSString *)selectorStr
andCustomParam:(NSDictionary *)customParam
{
BHContext *context = [BHContext shareInstance].copy;
context.customParam = customParam;
context.customEvent = eventType;
if (!selectorStr.length) {
selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
}
SEL seletor = NSSelectorFromString(selectorStr);
if (!seletor) {
selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
seletor = NSSelectorFromString(selectorStr);
}
NSArray<id<BHModuleProtocol>> *moduleInstances;
if (target) {
moduleInstances = @[target];
} else {
moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
}
[moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
}
}];
}
这个方法的目的是执行[moduleInstance performSelector:seletor withObject:context]
,其中moduleInstance
和seletor
表示事件对应的Module
类对象和响应方法,它们分别可以由参数target
和参数selectorStr
来指定,由于在本例中并没有传入这两个参数,只传入了BHMContinueUserActivityEvent
作为参数eventType
的值,所以它们需要根据参数eventType
从self.BHModulesByEvent
和self.BHSelectorByEvent
这两个属性中获取,这里的self
指的是BHModuleManager
,它们都是在注册时就设置好了。
6. 事件类型
官方的系统事件流程图:
上图中的事件包含了Application生命周期事件和BeeHive自己扩展的三个事件,ModSetup、ModInit、ModSplash
。除了这些事件,BeeHive还监听了推送、3D-Touch等相关的事件。
其完整的事件如下:
typedef NS_ENUM(NSInteger, BHModuleEventType)
{
//通用事件
BHMSetupEvent = 0,
BHMInitEvent,
BHMTearDownEvent,
BHMSplashEvent,
//3D-Touch
BHMQuickActionEvent,
//生命周期
BHMWillResignActiveEvent,
BHMDidEnterBackgroundEvent,
BHMWillEnterForegroundEvent,
BHMDidBecomeActiveEvent,
BHMWillTerminateEvent,
//未使用
BHMUnmountEvent,
BHMOpenURLEvent,
BHMDidReceiveMemoryWarningEvent,
//推送相关事件
BHMDidFailToRegisterForRemoteNotificationsEvent,
BHMDidRegisterForRemoteNotificationsEvent,
BHMDidReceiveRemoteNotificationEvent,
BHMDidReceiveLocalNotificationEvent,
BHMWillPresentNotificationEvent,
BHMDidReceiveNotificationResponseEvent,
//handoff和相关事件
BHMWillContinueUserActivityEvent,
BHMContinueUserActivityEvent,
BHMDidFailToContinueUserActivityEvent,
BHMDidUpdateUserActivityEvent,
//watchApp请求事件
BHMHandleWatchKitExtensionRequestEvent,
//自定义事件
BHMDidCustomEvent = 1000
};