元件化工具BeeHive(一):事件分發

wangzzzzz發表於2018-06-12

前言

BeeHive是阿里開源的一個元件化框架工具,其內部是使用Spring框架Service的理念來實現模組解耦的,實際上就是使用protocol-class的方案。另外,在元件化的基礎上,BeeHive還增加了一個事件分發的功能來配合使用。

目錄

  • 1. 概覽
  • 2. 事件分發的作用
  • 3. 事件分發的配置過程
  • 4. 註冊Module的幾種方式
  • 5. 事件分發的內部實現
  • 6. 事件種類

1. 概覽

官方文件中的架構圖:

元件化工具BeeHive(一):事件分發

從上圖可以看出,BeeHive的工作分為兩部分:

  1. 事件分發 BeeHive本身會監聽一些系統事件和應用事件,比如App生命週期、推送、handoff等,當事件發生時,BeeHive將其分發給各個模組,然後各個業務模組就可以在自己的Module類中呼叫各自的響應方法。

  2. 元件化 這部分是指在元件化的情況下,實現模組間呼叫,也就是說,各個模組是相互解耦的,BeeHive使用protocol-class的方案實現這一點。

2. 事件分發的作用

當一個事件被觸發時,其對應的響應方法需要被執行,比如介面更新、資料儲存等。在這個過程中,會涉及到響應方法的呼叫和實現這兩部分。

首先需要確定的是響應方法的實現都是由模組來完成的(不屬於現有模組的響應方法可以看做是屬於一個全域性模組),針對呼叫響應方法的位置,這裡就有兩種呼叫方式,一個是直接在事件觸發點呼叫(通常是在AppDelegate),另一個是通過BeeHive將事件分發給各個模組,在具體模組的Module類中呼叫。

為了對比這兩種呼叫方法的差別,下面以一個例子來說明:

一個場景

使用者在spotlight搜尋一個關鍵字testA,點選搜尋結果,app需要跳轉到模組A中的一個介面;
搜尋關鍵字testB,點選搜尋結果,app需要跳到模組B中的一個介面。
複製程式碼

具體操作如下:

元件化工具BeeHive(一):事件分發

在這個場景中,當使用者在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),整個註冊過程會分為三步

  1. 儲存需要註冊的類名 巨集BeeHiveMod的作用就是儲存類名,它會在專案mach-o檔案的segment:__DATA中新增一個名為BeehiveMods的section,並將字串"ModuleAModule"新增到這個section中。

  2. 取出儲存的類名 從專案mach-o檔案的(__DATA,BeehiveMods)中,獲取所有的類名。

  3. 註冊 使用[步驟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" ")))
複製程式碼

在編譯的時,前處理器會根據分段標誌將被定義的字串進行分段,然後每一段都會和巨集的引數對比,如果相同就會被替換,空格、括號、運算子和##等都屬於一種分段標誌。 ##一般被用在被替換段和其他段直接接觸的情況下,比如替換變數名的一部分,上述巨集定義中,就是使用兩個##將一個變數名隔離成三段,kname_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內部包含了兩個地址,分別為010001ea48010001ead2,這兩個地址的其中一個就是指向字串"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

複製程式碼

可以看到010001ea48010001ead2這兩個地址分別在上述輸出中的第三行和倒數第二行,對應的字串分別為"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(一):事件分發

需要注意的是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類來負責事件分發,其主要步驟是:

  1. 註冊 儲存Module類的物件和事件對應的響應方法
  2. 觸發 通過事件型別,獲取需要響應的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中。(這裡就儲存好了BHModulesBHModuleInfos這兩個屬性。)

最後,呼叫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],其中moduleInstanceseletor表示事件對應的Module類物件和響應方法,它們分別可以由引數target和引數selectorStr來指定,由於在本例中並沒有傳入這兩個引數,只傳入了BHMContinueUserActivityEvent作為引數eventType的值,所以它們需要根據引數eventTypeself.BHModulesByEventself.BHSelectorByEvent這兩個屬性中獲取,這裡的self指的是BHModuleManager,它們都是在註冊時就設定好了。

6. 事件型別

官方的系統事件流程圖:

元件化工具BeeHive(一):事件分發

上圖中的事件包含了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
    
};
複製程式碼

相關文章