BeeHive 1.6.0 原始碼閱讀探討

JacobLJ發表於2020-04-03

因業務需要,故而有此閱讀一舉

基本類功能概念

  • 框架內部類總覽

    BeeHive 1.6.0 原始碼閱讀探討

  • BHModule

    • 監聽系統級別事件並執行事件的類(即用於處理APP生命週期事件及相關係統層的類)
    • BHModule 需要遵守 BHModuleProtocol(系統級別事件介面定義在 BHModuleProtocol)
    • 自行通過alloc init方式自行初始化的模組時不在BHModuleManager下被管理
  • BHContext

    • 管理應用環境、全域性配置、事件觸發上下文物件(即事件方法的上下文引數
    • 還包括 BHShortcutItemBHOpenURLItemBHNotificationsItemBHUserActivityItemBHWatchItem
  • BHModuleManager:負責【註冊】模組 BHModule,並管理

    • BHModules 陣列:
      • 存放已註冊的 BHModule 例項(每一種 class 的 BHModule 例項只能有一個,註冊時會進行過濾操作,isKindOfClass 則不再計入該例項)
      • 排序
        • 先按 basicModuleLeve (從小到大排,basic < normal)
        • 如果前面相等,通過 modulePriority 排(從大到小)
    • BHModuleInfos 字典陣列:註冊模組時建立的模組資訊字典,字典包括模組class、模組level、是否已Instantiated等資訊
    • BHSelectorByEvent 事件表字典:管理預設系統事件型別和自定義事件型別
    • BHModulesByEvent 事件-響應例項陣列表字典:將能夠響應相同型別事件的模組例項按 BHModules 陣列的排序方法存放至對應陣列中
  • BHServiceManager:管理各個已【註冊】Protocol

Module的註冊( 按啟動時執行的順序 )

1.Annotation方式註冊

  • 功能對應類:BHAnnotation
  1. 通過使用巨集 BeeHiveMod(類名) 將類名字串,在編譯階段寫入到指定名稱的記憶體section中

BeeHive 1.6.0 原始碼閱讀探討

在 ShopModule 中使用該巨集,這樣就能夠在編譯階段將 ShopModule這個類名寫入到 __DATA 段中並以 BeehiveMods 作為key標識位置

BeeHive 1.6.0 原始碼閱讀探討

巨集編譯後的結果是

BeeHive 1.6.0 原始碼閱讀探討

  1. 通過__attribute__((constructor))這個巨集修飾方法void initProphet() {...},讓該方法在main函式【前】被呼叫用

BeeHive 1.6.0 原始碼閱讀探討

_dyld_register_func_for_add_image 表示每一次映象的載入都會觸發傳入的dyld_callback回撥函式

弊端是dyld_callback會回撥多次

BeeHive 1.6.0 原始碼閱讀探討

dyld_callback被觸發時,內部會呼叫BHReadConfiguration方法

BeeHive 1.6.0 原始碼閱讀探討

  1. 從 Mach-O section 中讀取之前寫入的類名字元資料(組合成字串陣列)

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

  1. 遍歷類名字串陣列,通過 BHModuleManager 註冊該類,[[BHModuleManager sharedManager] registerDynamicModule:cls];

BeeHive 1.6.0 原始碼閱讀探討

2.Load方式註冊

通過使用巨集 BH_EXPORT_MODULE(isAsync) 方式,在類被載入記憶體時呼叫+load方法註冊,實際內部呼叫的相同的方法[[BHModuleManager sharedManager] registerDynamicModule:cls];

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

3.讀取本地Plist檔案

BeeHive 例項首次設定其 BHContext類的屬性context時,會觸發本地plist的讀取,載入對一個的模組字典資訊,存放至 BHModuleManagerBHModuleInfos 陣列中,然後註冊模組

BeeHive 1.6.0 原始碼閱讀探討

從demo上看,觸發時機就是在delegate中了

BeeHive 1.6.0 原始碼閱讀探討

Services的註冊 (方式與Annotation的一樣)

1.Annotation方式註冊

1、編譯巨集

BeeHive 1.6.0 原始碼閱讀探討

2、使用方式

BeeHive 1.6.0 原始碼閱讀探討

3、編譯後結果

BeeHive 1.6.0 原始碼閱讀探討

4、從__DATA段中的BeehiveServices中讀取內容結果,邏輯與Module的annotation方式是一摸一樣的,區別是Module在記憶體段中存放的是一個字串陣列,而service存放的是一個字典陣列

BeeHive 1.6.0 原始碼閱讀探討

5、逐個讀取,然後註冊

BeeHive 1.6.0 原始碼閱讀探討

2.Load方式註冊

目前的框架庫中,service的註冊方式是沒有了load方式註冊的。至於為啥不提供了,我猜應該就是為了突出Module和Service這兩層概念的區別吧

【Module】

1、Module主要是用來處理系統及APP宣告週期等級別的基本操作單位,更類似於平時開發的三方庫的抽象管理類,以及APP宣告週期的抽象管理類,還有一些其他的更底層及迭代週期基本不變的那種管理類

2、根據第一點,所以Module出現是必須約定已經遵守了框架提供的一個協議BHModuleProtocol,從該協議定義介面來看(都是APPdelegate的代理轉發介面),更加突出第一點的要求,就是Module屬於處理系統層級的管理類

3、既然Module是底層型的管理類,那麼它具備了load方式的註冊方法來看也是合理的,畢竟底層的管理類就會有類載入時就要做一些其他操作的需要(這是從使用方向上的分析,比如Swizzle等行為)

【Service】

1、從註冊時提供的內容看,不僅要提供對應的類,還要提供其使用的範圍的介面protocol,明顯就是一個業務層型的Module版,由於業務的迭代快特點,故而關聯的protocol也是需要使用者自己來維護才是合理的

2、由於service定位在業務上的操作管理類,則對load形式的需求明顯就與Module型的類相提並論了,故而不提供load的註冊方式也在情理中

3、還沒想到?、當然硬是要實現也是可以的,模仿Module的load方式即可

3.讀取本地Plist檔案

同Module的一樣,觸發時機都是首次對BeeHive這個單例設定context屬性時

BeeHive 1.6.0 原始碼閱讀探討

service與module的註冊內容區別是,service(類)與protocol(介面)配對註冊的,而module的註冊只需要知道類名即可,這也是這兩類元件使用上的區別

Module註冊過程分析

1.Annotation方式註冊

  1. 從記憶體段中獲取到所有module名的字串資料組後,遍歷陣列,然後逐個通過[[BHModuleManager sharedManager] registerDynamicModule:cls];註冊

BeeHive 1.6.0 原始碼閱讀探討

  1. [BHModuleManager sharedManager]通過單例形式使用

BeeHive 1.6.0 原始碼閱讀探討

  1. registerDynamicModule:註冊入口邏輯

BeeHive 1.6.0 原始碼閱讀探討

  1. - (void)addModuleFromObject:(id)object shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent 進入真正的註冊流程

引數防空及引數轉存層

BeeHive 1.6.0 原始碼閱讀探討

module必須遵守對應的協議

BeeHive 1.6.0 原始碼閱讀探討

確定遵守協議,進入module類資訊標記及初始化流程

BeeHive 1.6.0 原始碼閱讀探討

補述:

  • 第1點:

    • 系統事件處理的module(也就是BHModule類)的層級按目前劃分normal和basic,未響應方法basicModuleLevel的為normal(值為1)層級,否則為basic層級(值為1),但暫時還無法從給出的demo中區分出這兩種層級的劃分是作用是什麼,但可以明確的是,這兩種層級的劃分對系統事件的響應順序是有影響的,後續會進一步書名層級對module的響應排序
  • 第2點:

    • self.BHModuleInfos是一個模組資訊字典的陣列,存放每一module的類名稱字串、層級的值、是否已初始化等資訊。至於是否已初始化這一標識,從annotation方式註冊類來看,都會被標記成已初始化狀態 * 這個self.BHModuleInfos是沒有進一步排序的【是有排序的,只是放到了plist方式註冊時觸發而已】,與self.BHModules不同。self.BHModuleInfos是資訊的儲存列表而已,它也不會重複加入相同的module,因為前面已經有處理module的唯一性邏輯了
  • 第3點:

    • 就是將初始化的module例項加入至self.BHModules列表中

對self.BHModules例項列表進行層級排序

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

1、先按 basicModuleLeve (從小到大排,basic (0) < normal (1))

2、如果前面相等,通過modulePriority排(從大到小)

假設:

moduleA(basic)、
moduleB(normal)、
moduleC(basic)、
moduleD(modulePriority=1)、
moduleE(modulePriority=3)、
moduleF(modulePriority=2)

則,排序結果為:

moduleA、moduleC、moduleB、moduleE、moduleF、moduleD
複製程式碼

將模組例項註冊至對應的事件處理列表集中,也就是說,將剛初始化的module例項按相應的系統事件進行歸類,這樣也是方便於後續如果觸發一個系統事件,能夠快速直接將訊息轉發至所有響應該事件的module例項中

BeeHive 1.6.0 原始碼閱讀探討

將在下面第5小點詳細說明這個邏輯

setup事件、init事件、splash事件的觸發。當然annotation的註冊方式預設不執行這些方法

BeeHive 1.6.0 原始碼閱讀探討

  1. 詳細說說[self registerEventsByModuleInstance:moduleInstance];處理邏輯

BeeHive 1.6.0 原始碼閱讀探討

self.BHSelectorByEvent標識一個系統事件的字典表

BeeHive 1.6.0 原始碼閱讀探討

歸類module並進行排序操作

BeeHive 1.6.0 原始碼閱讀探討

  1. 詳細說說前面的三種事件setup事件、init事件、splash事件的觸發邏輯
  • InitEvent

BeeHive 1.6.0 原始碼閱讀探討

  • setup和splash的事件等其他event
    BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

  1. TearDownEvent

BeeHive 1.6.0 原始碼閱讀探討

  1. 主動移除module 方法-unRegisterDynamicModule:
    BeeHive 1.6.0 原始碼閱讀探討

2.Load方式註冊

使用對應的巨集

BeeHive 1.6.0 原始碼閱讀探討

巨集展開後結果

BeeHive 1.6.0 原始碼閱讀探討

從上可以知,該巨集是重寫了類的+ (void)load方法和實現了BHModuleProtocol的一個介面方法- (BOOL)async

load方發中呼叫的是BeeHive提供的便捷介面+ registerDynamicModule:,實際內部也就是呼叫BHModuleManager的方法[[BHModuleManager sharedManager] registerDynamicModule:moduleClass];

BeeHive 1.6.0 原始碼閱讀探討

後續的流程及跟Annotation的註冊module一樣了。

至於響應的那個非同步方法,在Annotation的註冊module的最後流程中有說過,就是註冊並管理該module後,會觸發一次該module的三個事件setup/init/splash,其中init事件時會根據module例項是否響應了-async方法的返回值作為是否非同步執行的判斷依據

3.讀取本地Plist檔案

觸發時機及位置

BeeHive 1.6.0 原始碼閱讀探討

bundle的Plist內容

BeeHive 1.6.0 原始碼閱讀探討

逐步說明一下內部處理的邏輯[[BHModuleManager sharedManager] loadLocalModules];

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

1、定位plist的路徑,防空處理

2、根據路徑獲取plist內容,也就是一個陣列

3、建立一個臨時空字典(目的是用來在第5步中進行過濾判斷)

4、將已有的module資訊字典陣列,將對應的類歸入至臨時字典中

原有self.BHModuleInfos中裝的內容如下,這些都是通過annotation或load方式註冊的模組資訊
@[
    @{
           @"moduleLevel" : 0,
           @"moduleClass" : @"ModuleA",
           @"moduleHasInstantiated" : YES,
        },
    @{
            @"moduleLevel" : 1,
            @"moduleClass" : @"ModuleB",
            @"moduleHasInstantiated" : YES,
        },
]
複製程式碼

加入至臨時字典後,字典中內容如下

    @{
        @"ModuleA" : 1,
        @"ModuleB" : 1,
    }
複製程式碼

5、過濾已有的模組內容,因為不允許處理系統事件的同名module存在,將對應的plist的module的資訊字典物件,加入到self.BHModuleInfos中。 【注意】

  • annotation和load方式註冊的module中,其對應的module資訊字典中的是否已註冊欄位moduleHasInstantiated對應的值預設是YES的。而此時從plist中獲取的module則是未建立的

  • plist中的module資訊,還有一個獨有的key值,即modulePriority,之前annotation和load方式註冊的module時的排序都是通過該module的響應的該方法返回值作為排序依據的,這裡則直接帶有該排序權重的值

  • BeeHive 1.6.0 原始碼閱讀探討

  • BeeHive 1.6.0 原始碼閱讀探討

6、至此我們知道,self.BHModuleInfos中可能同時具備了annotation和load方式註冊的module的資訊,及plist中獲得的未初始化過的module資訊

繼續說說[[BHModuleManager sharedManager] registedAllModules];邏輯

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

1、self.BHModuleInfos根據元素Module字典的兩個key值進行排序

  • 分別是moduleLevel,modulePriority。實際上就是前面annotation方式註冊流程中年說過的level層(basic或normal),以及priority(數值大小)排序。

  • 這裡需要注意的點是,前面annotation和load方式註冊的Module資訊字典中是沒有modulePriority這個key的(但有這個值的排序,只不多是通過響應方法來獲取)。

  • 【從這點來看,我認為annotation和load一般都類為level層級的Module,而plist的都為priority層級。這是我的猜測,只不多目前這個框架已不更新多年瞭如果要繼續考究的話,只能靠自己了?】

2、將未初始化的Module進行初始化,這裡指的也就是從plist方式註冊的Module了,然後加入到臨時陣列中

3、將新初始化的Module例項陣列併入至self.BHModules陣列中

4、最後就是重複一次Module例項按事件類別進行歸類了,這個邏輯在前面的annotation註冊分析中也已經說過了

【至此可以清楚瞭解到BHModuleManager的內部內容都有啥了】

BeeHive 1.6.0 原始碼閱讀探討

Service註冊過程分析

1.Annotation方式註冊

1、從記憶體段讀取完 class-protocol 字典陣列後,即遍歷,然後逐個註冊

BeeHive 1.6.0 原始碼閱讀探討

2、class-protocol資訊儲存管理即為註冊

BeeHive 1.6.0 原始碼閱讀探討

3、儲存資訊時通過lock的方式防止多執行緒寫入問題

  • 在Module的annotation中是沒有的相關的執行緒問題的,為什麼這裡需要呢?

  • 答案就是:正因為這兩者從概念上負責的範圍層級不同造成的,Module是偏底層的,基本是程式啟動時機會按各種依賴做好排序,並且分工明確。而Service則不同,它就是負責業務層的,可以同時具備多個Service例項也可以在執行過程中臨時註冊一個新的service,所以基於這種情況,出現執行緒問題就不足為奇了。

  • 既然有執行緒問題,那麼lock的方式使用合理了嗎?(後續再討論?)

4、註冊邏輯是簡單的,這裡的事件觸發都是在業務執行時發生的

2.Load方式註冊

不存在load的註冊方式

3.讀取本地Plist檔案

1、觸發註冊本地service

BeeHive 1.6.0 原始碼閱讀探討

2、讀取plist內容並註冊(即儲存class-protocol字典)

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

3、有重複可能的小問題

  • 這裡的方式沒有做防重處理,因為annotation和plist中是會存在class-protocol一樣的情況的,其實如果是完全一樣的話問題也是不大的,即使是重複,字典的讀取時間複雜度也是O(1),但如果是classX-protocolX其中一個不一致則還會會有嚴重問題的

4、enableException 開啟問題

  • BHServiceManager內部是有一個enableException的開關的,從上面程式碼截圖可以看出,這是用於異常的提示的開關,這點但從BHServiceManager內看,我覺得無可厚非,但是縱觀整個框架的看的話,為什麼BHModuleManager中沒有呢?這點就有點厚此薄彼的意思了,而且也對後續的異常處理層的擴充套件造成障礙,畢竟你看在只用於BHServiceManager內部的enableException開關介面竟然放到了BeeHive這個總管理類上來,從使用角度看,定義在總覽的表頭,應該表示用於全域性的異常開關吧,可以目前的框架情況來看,只是區域性使用而已。

  • BeeHive 1.6.0 原始碼閱讀探討

  • BeeHive 1.6.0 原始碼閱讀探討

【至此可以清楚瞭解到BHServiceManager的內部內容都有啥了】

BeeHive 1.6.0 原始碼閱讀探討

Module的使用流程介紹

BeeHive 1.6.0 原始碼閱讀探討
1、很明顯,Module的使用基本都提現在了AppDelegate的生命週期函式中了,也就是系統層級事件的統一排程,觸發對應的單個或多個Module執行對應的行為

2、從這裡也可以看出,雖然從層面上做到了應用代理的解耦,但畢竟重寫了整個AppDelegate的生命週期函式,就這一點的耦合性終究是無法避免的

3、具體的觸發事件邏輯,在Module的Annotation註冊流程分析中有說,這裡就不再複述了

Service的使用流程介紹

1、我們知道,BHServiceManager中並沒有管理對應的service例項,只是單純的管理serviceClass-protocol這個鍵值字典而已,所以每一次需要使用對應的service例項是,就需要根據對應的protocol獲取到對應的class然後建立該例項

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

BeeHive 1.6.0 原始碼閱讀探討

2、建立流程分析

BeeHive 1.6.0 原始碼閱讀探討

  • 第1:

    • 如果沒有傳入對應的protocol字串,則從Protocol class中轉出其字串名稱
    • 檢查是否已註冊,否則拋異常,也就是說service的使用時先註冊後建立使用
  • 第2:

    • 根據方法引數shouldCache判斷是否從快取中讀取。而這個快取是在上下文單例[BHContext shareInstance] 只用維護的,這個也是一個字典,key:Protocol的字串名,value:service例項。如果是通過快取讀取,則快取中如果有該protocol對應的service例項則直接返回該例項

    • 問題來了,為啥是全域性上下文context單例來維護這個service例項表呢?不僅如此,全域性上下文context單例也同時維護一個module名稱表,但並未啟用而已

    • BeeHive 1.6.0 原始碼閱讀探討

    • 我的猜測是:因為上下文是全域性通用的,同時也是service間、module間以及service與module間的引數橋樑,很明顯業務的層間的使用時複雜多變的,而將service例項表通過context來維護,明顯是為後續各種互動提供便利,同樣這一點也是耦合的原點,將來框架對業務的影響或者反過來業務對框架的影響都是無法預測的。

  • 第3:邏輯意圖比較明顯,如果用文字來說顯得太冗餘了,自己看圖吧

    • BeeHive 1.6.0 原始碼閱讀探討

    • service也有單例的概念,當然單例同樣是可以被context快取的

  • 其他問題

    • 程式碼風格不一致
    • -serviceImplClass方法感覺是多餘的,protocol 從class轉string又從string轉class

BHAppDelegate

  • 讓專案的 AppDelegate 類繼承自 BHAppDelegate
  • BHAppDelegate 直接過載所有 AppDelegate 的方法
  • 在過載的方法裡,通過 BHModuleManager 將該代理方法轉發至,監聽該事件的 BHModule 例項中

問題分析

應用代理解耦

對應操作物件是 BHModuleManager - BHModule

  • 雖然響應應用代理的模組具備代理事件呼叫順序可控,但順序不明顯,需要一個一個類檢查其排序等級

  • 模組間呼叫解耦 對應操作物件是 BHServiceManager - 遵守BHServiceProtocol的物件

  • url-ruouter解耦 對應操作物件是 BHRouter - BHServiceManager - 遵守BHServiceProtocol的物件

方向

可借鑑使用地方

1、在 AppDelegate 中啟動時觸發的三方庫,通過用module方式進行封裝

  • 增加debug難度,因為原來直觀的程式碼邏輯被分散
  • 如果三方庫間是具有信心的耦合互動的話,不容易實現。也就是BeeHive的module間互動是相對獨立的

參考

相關文章