前言
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)
3.1. 初始化
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;
}
複製程式碼
3.2. 建立並註冊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)
複製程式碼
3.3. 呼叫響應方法
在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
};
複製程式碼