最近這幾天一直在調研市場上,關於組件通訊這一塊的實施方案和技術選型,關於路由
方式和target-action
的方式,由於硬編碼
問題,擔憂後續維護硬編碼可能會耗費大量精力,還有就是基於runtime
的通訊方式編譯期難以檢查是否有錯,這可能會產生運行時問題,因此 Pass 掉了。咱們項目目前 VC 之間經過路由方式進行跳轉,實際內部就是經過字符串反射出 Class 進行實例化跳轉,提供給 JS 的接口也是基於 runtime 進行了插件化,原理是差很少的,都是經過硬編碼在運行時拿到真實類型,再去調用。雖然這兩種方案都能進行模塊間的解耦,可是在實踐過程當中,咱們發現,在進行迴歸測試的時候,由於同事解決代碼衝突,確實發生過路由丟失、插件丟失的狀況,因此此次我直接調研了基於 Protocol
構建 Service
的方式(下面 Service 指 Protocol ),以及總結了一下本身的見解。ios
下面主要分析下兩個框架,這兩個框架是典型的基於 Service
構建的組件通訊,內部有着不少實用技巧,透過這兩個框架,咱們去探究下 Service
構建組件通訊的原理,目前我知道的,高德、天貓還有有贊
的一系列App都是基於此方式(可能還有其餘我還未接觸到)。git
阿里:《BeeHive》github
有贊:《Bifrost》。緩存
關於組件化的介紹網上文章很是多,講的也很詳細,並不是本篇重點。本篇主要分析上述兩個框架在組件通訊的優劣,以及一些我的的思考,供技術選型使用。安全
《Bifrost》有贊將它比喻爲彩虹橋,對組件間進行鏈接通訊。代碼至關清晰、簡單。有讚的思路大概是這樣:bash
+load
方法內將他們之間的映射關係註冊到字典裏。單例
,支持同步、異步初始化,支持加載優先級。這樣其餘模塊想要獲取Module實例,只須要經過它的Service,將Service做爲key,去管理類中註冊的字典,便可拿到,從而實現了組件間依賴解除,大體調用流程以下:markdown
id<xxxService> module = [[Bifrost moduleByService:@protocol(xxxService)] doSomething:xxx]; 複製代碼
+ (id<BifrostModuleProtocol> _Nullable)moduleByService:(Protocol*_Nonnull)serviceProtocol { // 映射String NSString *protocolStr = NSStringFromProtocol(serviceProtocol); ... // moduleDict 以前註冊的字典取Class Class class = BFInstance.moduleDict[protocolStr]; // 單例,此時已是在啓動的時候初始化好的了 id instance = [class sharedInstance]; return instance; } 複製代碼
《BeeHive》和它的思路實際上大致一致,代碼相對多些,功能也相對細些,大概思路以下:多線程
id<xxxServiceProtocol> module = [[BeeHive shareInstance] createService:@protocol(xxxServiceProtocol)];
複製代碼
- (id)createService:(Protocol *)service { return [self createService:service withServiceName:nil]; } - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName { return [self createService:service withServiceName:serviceName shouldCache:YES]; } 複製代碼
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache { ... NSString *serviceStr = serviceName; // 支持緩存,先去緩存中查找,存在返回,不存在繼續往下走 if (shouldCache) { id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr]; if (protocolImpl) { return protocolImpl; } } // 去管理類的字典中找module類名字符串並轉爲Class NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)]; if (serviceImpl.length > 0) { Class implClass = NSClassFromString(serviceImpl); } // 若是實現了singleton if ([[implClass class] respondsToSelector:@selector(singleton)]) { if ([[implClass class] singleton]) { if ([[implClass class] respondsToSelector:@selector(shareInstance)]) // 實現了shareInstance就設置爲單例 implInstance = [[implClass class] shareInstance]; else implInstance = [[implClass alloc] init]; // 設置了緩存那就存儲一下 if (shouldCache) { [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr]; return implInstance; } else { return implInstance; } } } // 未實現singleton直接返回爲多例 return [[implClass alloc] init]; } 複製代碼
總結一下,大致上看這兩個框架思路差很少,可是有些小細節須要再梳理下(如下Module所有表示爲Service的具體實現類
):架構
《Bifrost》基於外觀模式
,組件間的調用關係所有都有外觀類來實現,一個外觀類對應一個Service,也就是說一個組件一個Service,有贊認爲這樣一來,組件間的複雜關係由外觀角色來實現,下降了系統的耦合度。它將全部的外觀類,也就是Module類都設置爲了單例。app
《BeeHive》就我從源碼分析來看偏向於主張哪一個類有接口須要被其餘組件使用,哪一個類註冊一個Service,這個類能夠是單例,也能夠是多例,可是我以爲靈活一點爲每一個組件定義一個外觀類也能夠實現,否則可能Service文件會過多,維護困難。
這一塊我的認爲二者思路基本一致,相比之下《BeeHive》更靈活。
《Bifrost》註冊所有在+load
方法中,每個Module均要實現其+load方法並對Service進行註冊,以達到這種映射關係。
+ (void)load {
[Bifrost registerService:@protocol(xxxServiceProtocol) withModule:self.class];
}
複製代碼
相比之下《BeeHive》註冊有多種方式,最新穎的是經過__attribute()
函數在編譯期將這種映射關係添加到 Mach-O 的數據段,在 App 啓動的時候將其取出註冊到字典中,具體實現都在 BHAnnotation 中。
《Bifrost》在 App 啓動的時候,在 AppDelegate 的 willFinishLaunchingWithOptions
中,將全部 Module,按照順序進行初始化,且所有爲單例。有贊在實踐的過程當中組件最多在20幾個,因此這些單例不會帶來內存問題。初始化支持異步。《Bifrost》在組件間調用的時候實際上拿到的實例已是被初始化好的單例了。
+ (void)setupAllModules { NSArray *modules = [self allRegisteredModules]; for (Class<BifrostModuleProtocol> moduleClass in modules) { ...省略一些代碼 if (setupSync) { [[moduleClass sharedInstance] setup]; } else { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[moduleClass sharedInstance] setup]; }); } } } 複製代碼
《BeeHive》在 App 啓動的時候,並未將全部 Module 實例化,而是將其類名及對應的 Service 名添加到管理類的字典中,在組件間真正要實施通信的時候,根據 Service 名稱,去字典中取 Module 類名,纔去進行實例化,實例化的過程當中支持將其設置爲單例或者多例。
- (void)registerService:(Protocol *)service implClass:(Class)implClass { ... NSString *key = NSStringFromProtocol(service); NSString *value = NSStringFromClass(implClass); if (key.length > 0 && value.length > 0) { [self.lock lock]; // 實際上只是將string存儲了起來並未將其實例化 [self.allServicesDict addEntriesFromDictionary:@{key:value}]; [self.lock unlock]; } } 複製代碼
在管理這一塊我的感受他們之間有着本質區別,《Bifrost》將全部 Module 設置爲單例,在實踐中我發現這樣配置使用起來確實很是的方便,經過 Service 直接獲取單例便可,尤爲在須要 Module 存儲某些狀態時。可是這樣作也發現了一個問題,由於 Module 的定位是整個組件全部對外暴漏接口的包裝層,但我每每由於一些業務場景,須要 Module 持有那個具體的實現類,這時會發現被單例持有的這個類內存釋放會比較麻煩,繞點彎也能夠解決,但總以爲不那麼美觀。因此這裏我相對來講偏向於《BeeHive》對組件的外觀類添加多例的實現,須要的時候進行初始化,用完即釋放。
《BeeHive》在傳參處理上,未看到對 model 傳遞的處理,若是咱們須要將一個 model 從組件 A 傳遞到組件 B,至少在 BeeHive 的 Demo 裏,若是想要傳遞整個 model,須要將 model 全部字段都以參數的形式傳遞給組件 B 使用,這樣會讓接口顯得很是的長,也不夠直觀。若是組件 B 能夠直接拿到 model,那麼組件 B 將會很輕鬆的知道這個接口傳遞的參數來源於哪,具體是作什麼的,也會側面增強業務關聯性,另外還能夠經過點語法來獲取參數值,這其實將很是利於讀寫。《Bifrost》就提供了一個很好的思路,它爲 model 也構建了 Service,代碼編寫在 Module 所在的那個 Service 中,以下所示:
@interface GoodsModel : NSObject<GoodsProtocol>
@property(nonatomic, strong) NSString *goodsId;
@property(nonatomic, strong) NSString *name;
@property(nonatomic, assign) CGFloat price;
@property(nonatomic, assign) NSInteger inventory;
@end
複製代碼
#pragma mark - Model Protocols @protocol GoodsProtocol <NSObject> - (NSString*)goodsId; - (NSString*)name; - (CGFloat)price; - (NSInteger)inventory; @end 複製代碼
使用起來也很方便:
id<GoodsProtocol> goods = [BFModule(GoodsModuleService) goodsById:item.goodsId];
複製代碼
BFModule宏定義展開:
#define BFModule(service_protocol) ((id<service_protocol>)[Bifrost moduleByService:@protocol(service_protocol)]) 複製代碼
總的來講,在《Bifrost》的基礎上,Module管理這塊,融匯一下《BeeHive》的註冊方式,支持多例,在使用時建立用完釋放等思想會不會更好些。
額外的再說下基於 Protocol
的方式最主要的優點,就是出問題編譯期就能報錯,編譯器幫咱們檢查了是否有文件缺失,是否有引用缺失,我想這也是不少公司採用這種方式的最主要緣由。兩個模塊,經過 id <xxxServiceProtocol> xxx = ...
便可拿到其中一個模塊的實例,而不須要對模塊的頭文件引用,從而達到模塊間編譯隔離和模塊間通訊。
接觸少的同窗可能會以爲這有點繞,這實際上和咱們經常使用的代理原理一致,當咱們編寫一個工具類對外提供一個代理的時候,你會關心調用你的這個工具類具體是哪個類嗎?答案固然是不會的,咱們只須要關心調用方是否遵循了 xxxServiceProtocol
協議而且實現了其中的方法,若是是的話咱們天然就能夠調用這些方法了。