關於 iOS 組件通訊的思考

最近這幾天一直在調研市場上,關於組件通訊這一塊的實施方案和技術選型,關於路由方式和target-action的方式,由於硬編碼問題,擔憂後續維護硬編碼可能會耗費大量精力,還有就是基於runtime的通訊方式編譯期難以檢查是否有錯,這可能會產生運行時問題,因此 Pass 掉了。咱們項目目前 VC 之間經過路由方式進行跳轉,實際內部就是經過字符串反射出 Class 進行實例化跳轉,提供給 JS 的接口也是基於 runtime 進行了插件化,原理是差很少的,都是經過硬編碼在運行時拿到真實類型,再去調用。雖然這兩種方案都能進行模塊間的解耦,可是在實踐過程當中,咱們發現,在進行迴歸測試的時候,由於同事解決代碼衝突,確實發生過路由丟失、插件丟失的狀況,因此此次我直接調研了基於 Protocol 構建 Service 的方式(下面 Service 指 Protocol ),以及總結了一下本身的見解。ios

下面主要分析下兩個框架,這兩個框架是典型的基於 Service 構建的組件通訊,內部有着不少實用技巧,透過這兩個框架,咱們去探究下 Service 構建組件通訊的原理,目前我知道的,高德、天貓還有有贊的一系列App都是基於此方式(可能還有其餘我還未接觸到)。git

阿里:《BeeHive》github

有贊:《Bifrost》緩存

基本原理

關於組件化的介紹網上文章很是多,講的也很詳細,並不是本篇重點。本篇主要分析上述兩個框架在組件通訊的優劣,以及一些我的的思考,供技術選型使用。安全

《Bifrost》有贊將它比喻爲彩虹橋,對組件間進行鏈接通訊。代碼至關清晰、簡單。有讚的思路大概是這樣:bash

  • 1:每個業務組件,都定義一個Module和一個Service,Module用來實現對外提供的一些功能,Service用來定義組件對外暴漏的接口,旨在對外提供服務。
  • 2:再經過一個管理類,在+load方法內將他們之間的映射關係註冊到字典裏。
  • 3:app啓動的時候,將全部Module進行實例化,實際全部Module皆爲單例,支持同步、異步初始化,支持加載優先級。

這樣其餘模塊想要獲取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》和它的思路實際上大致一致,代碼相對多些,功能也相對細些,大概思路以下:多線程

  • 1:從源碼上來看,BeeHive認爲每一個須要對其餘組件提供接口的類,均可以註冊一個Service,旨在哪裏須要對外提供服務,哪裏進行註冊,相對靈活。例如組件A的某個類須要提供一個接口給組件B,那麼組件A的這個類須要對組件B提供一個Service(定義接口),再將這個Service和這個類註冊到BeeHive中。這樣B組件或者其餘組件只須要引用Service便可。BeeHive將全部Service抽離處理放到一塊兒讓其餘組件引用。
  • 2:BeeHive經過多種方式用來註冊Module和Service的映射關係,不論是哪一種方式最後都會經過管理類單例註冊到字典中。
  • 3:組件間接口調用的時候,會經過管理類找到註冊的字典,再將註冊的Service爲key,獲取到對應的Module實例,Module實例支持單例和多例的初始化形式,在獲取的過程當中,還支持將其緩存到字典,這樣拿到實例就能夠直接調用了,從源碼來看有經過遞歸鎖保證在多線程訪問的狀況下,按序訪問數據安全。代碼大體流程以下:
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];
}
複製代碼
  • 4:BeeHive還有一些解耦AppDelegate的邏輯,這裏暫不展開。

對比選型

總結一下,大致上看這兩個框架思路差很少,可是有些小細節須要再梳理下(如下Module所有表示爲Service的具體實現類):架構

Module劃分

《Bifrost》基於外觀模式,組件間的調用關係所有都有外觀類來實現,一個外觀類對應一個Service,也就是說一個組件一個Service,有贊認爲這樣一來,組件間的複雜關係由外觀角色來實現,下降了系統的耦合度。它將全部的外觀類,也就是Module類都設置爲了單例。app

《BeeHive》就我從源碼分析來看偏向於主張哪一個類有接口須要被其餘組件使用,哪一個類註冊一個Service,這個類能夠是單例,也能夠是多例,可是我以爲靈活一點爲每一個組件定義一個外觀類也能夠實現,否則可能Service文件會過多,維護困難。

這一塊我的認爲二者思路基本一致,相比之下《BeeHive》更靈活。

Module註冊

《Bifrost》註冊所有在+load方法中,每個Module均要實現其+load方法並對Service進行註冊,以達到這種映射關係。

+ (void)load {
    [Bifrost registerService:@protocol(xxxServiceProtocol) withModule:self.class];
}
複製代碼

相比之下《BeeHive》註冊有多種方式,最新穎的是經過__attribute()函數在編譯期將這種映射關係添加到 Mach-O 的數據段,在 App 啓動的時候將其取出註冊到字典中,具體實現都在 BHAnnotation 中。

Module管理

《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 協議而且實現了其中的方法,若是是的話咱們天然就能夠調用這些方法了。

ref:

《BeeHive,一次 iOS 模塊化解耦實踐》

《有贊移動 iOS 組件化(模塊化)架構設計實踐》

相關文章
相關標籤/搜索