組件化的目的
“組件”指的是較大粒度的業(yè)務(wù)功能模塊;一個APP通常由多個業(yè)務(wù)模塊組成,或者是多個不同子業(yè)務(wù)線的APP通過模塊的形式在主應(yīng)用中進行集成;模塊之間通常會有通信、互相調(diào)用;實現(xiàn)組件化的目的,就是為了讓組件之前的互相調(diào)用可以更松散,消除組件之間由于互相依賴導(dǎo)致的耦合;
組件化的益處是可以使每個組件的開發(fā)過程都更加獨立,更好的提升單一組件的迭代速度;開發(fā)人員可以只關(guān)注自己負責的部分,不需要全量編譯整個工程,同時也提升了開發(fā)效率;
在不使用組件化的應(yīng)用中,我們看到的不同模塊間的相互調(diào)用通常如下所示:
#import "ModuleAViewController.h"
#import "ModuleBViewController.h"
#import "ModuleCViewController.h"
@implementation ModuleBViewController
- (void)gotoModuleAVC {
ModuleAViewController *moduleAVC = [[ModuleAViewController alloc] init];
//....參數(shù)傳遞
[self.navigationController pushViewController: moduleAVC animated:YES];
}
- (void) gotoModuleCVC {
ModuleCViewController *cVC = [[ModuleCViewController alloc] init];
//...參數(shù)傳遞
[self.navigationController pushViewController: cVC animated:YES];
}
@end
通過直接引用不同模塊的Controller完成調(diào)用,這種方式在較小的項目里并沒有什么問題;但是隨著應(yīng)用的擴展與版本迭代,APP會變得越來越大,這種方式很容易導(dǎo)致模塊之間互相依賴越來越多, 模塊之間無法清晰劃分、形成強耦合,不利于后期的持續(xù)擴展與維護,從而降低了APP整體的迭代速度;且這種方式還不利于有單獨業(yè)務(wù)線的開發(fā)模式,單個業(yè)務(wù)線上的開發(fā)需要編譯整個工程,影響開發(fā)效率與編譯速度;
iOS業(yè)內(nèi)當前也形成了多種不同的組件化方案;通過對比不同的組件化設(shè)計方案、以及優(yōu)缺點,可以加深對組件化實現(xiàn)的理解;增強我們自己的設(shè)計能力;
不同組件化方案的對比
前面提到過,組件化方案的主要目的就是解耦模塊之間的直接調(diào)用;計算機領(lǐng)域里有一句話沒有什么問題,是不能通過增加一個中間層來解決的;組件化方案的思路也類似,通過增加一個中間層,讓不同的模塊之間的互相依賴下沉到這個中間層;通過這個中間層進行通信,來解決模塊之間的直接耦合問題;
因此通常的做法就是增加一個“ModuleManager”的組件管理中間層,并通過這個中間層來管理所有的組件間通信;類似于以下的結(jié)構(gòu):

通過中間層的實施組件化的實踐做法有很多種,對應(yīng)于不同的組件化方案;
方案一:通過提前在“ ModuleManager”中注冊服務(wù)的方式,實現(xiàn)組件化;注冊的方式至少包括以下幾種:
- 通過“URL-Instance”或“URL-Class” 注冊實現(xiàn)組件化
- 通過“URL-Block”注冊實現(xiàn)組件化
- 通過“Protocol-Class”注冊實現(xiàn)組件化
這種做法會在每一個組件模塊內(nèi)部 增加一個與其他組件的通信類,通信類包含該組件需要與外部通信的所有回調(diào)接口(即組件自己提供的與外部通信的服務(wù)接口);然后通過“ ModuleManager”的中間層來管理所有的組件通信類,在應(yīng)用啟動時提前把這些通信類注冊到“ ModuleManager”內(nèi)部;
這里“ ModuleManager”要實現(xiàn)組件間的相互調(diào)用,需要解決以下兩個問題:
- 1、在“ ModuleManager”中如何發(fā)現(xiàn)這些組件通信類,并調(diào)用到通信類內(nèi)部的通信接口;
- 2、“ ModuleManager”提供什么形式的外部統(tǒng)一接口供跨組件通信調(diào)用;
對于第一個問題,具體的實現(xiàn)也存在多種不同的做法,一種比較簡單和直觀的做法是,在APP內(nèi)維護一個組件模塊的配置文件“ModulesConfig.json”,配置文件內(nèi)部記錄每個組件的通信類名稱信息;在應(yīng)用啟動時讀取配置文件,獲取出所有的組件通信類信息;
第二個問題關(guān)聯(lián)到在獲取到所有的組件通信類后,通過什么樣的方式把這些類注冊到“ ModuleManager”模塊中;注冊的方式,就決定了可以提供什么樣的與之對應(yīng)的回調(diào)接口;這里就與以上提到過的三種注冊方式相關(guān);
通過“URL-Instance” 注冊實現(xiàn)的組件化
這種方式通過注冊“URL與通信對象”之間的Map關(guān)聯(lián),并在“ ModuleManager”中存儲這個Map信息;在回調(diào)時通過URL在Map中獲取出通信對象,并在通信對象內(nèi)部根據(jù)指定的URL調(diào)用到對應(yīng)的接口,跳轉(zhuǎn)到對應(yīng)的組件內(nèi)頁面;注冊過程與調(diào)用過程的簡化實現(xiàn) 類似如下:
// 注冊的過程
// modulesConfigClass是 之前從“ModulesConfig.json”配置文件中讀取出來的組件通信類信息
// 遍歷每一個組件類,并在實例化通信對象后,通過URL方式的完成注冊
for (Class cls in [ModuleManager sharedInstance]. modulesConfigClass) {
//實例化通信對象
id impl = [[cls alloc] init];
// route的注冊;
// 通信類內(nèi)部需要實現(xiàn)registModuleRoutes方法,方法內(nèi)返回一個數(shù)組,數(shù)組內(nèi)記錄了所有需要注冊的URL的硬編碼
if ([impl respondsToSelector:@selector(registModuleRoutes)]) {
//遍歷所有的URL
[[impl registModuleRoutes] enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//遍歷所有的URL,并把url與通信實例impl進行綁定,存放在一個NSDictionary中
if ([obj isKindOfClass:[NSString class]]) {
[ModuleManager.routeService registRoute:obj forModule:impl];
}
}];
}
}
//調(diào)用的過程,通過傳入URL完成跨組件調(diào)用
- (BOOL)openRoute:(NSString *)urlStr withParams:(nullable NSDictionary *)userInfo {
...
//解析urlStr
...
//根據(jù)urlStr信息,在“URL與通信對象”的Map字典里獲取出實例對象
...
//通過獲取到的實例對象,調(diào)用對象的路由跳轉(zhuǎn)方法,并在通信類內(nèi)部根據(jù)URL判斷 完成對應(yīng)頁面的跳轉(zhuǎn);
//
...
}
這種方式需要提前注冊組件的通信實例對象,會有較大的內(nèi)存常駐,且大量對象的初始化對應(yīng)用的啟動速度也會存在一定的影響;并會存在較多的URL硬編碼,這部分編碼的聲明可以寫在組件通信類的內(nèi)部;可以在“URL與通信對象”Map字典里只做“URL與通信類”的關(guān)聯(lián),這可以減少內(nèi)存開銷,只要調(diào)用時在懶加載通信類的實例,這種方式就是“URL-Class”的注冊方式了;
通過“URL-Block” 注冊實現(xiàn)的組件化
“URL-Block”的方式與“URL-Instance”對比并沒有本質(zhì)上的差別;在應(yīng)用啟動時會注冊所有的“URL與Block的Map關(guān)聯(lián)”,回調(diào)時通過URL獲取出對應(yīng)的Block并執(zhí)行,完成跨組件通信;與URL-Instance相比,在注冊的內(nèi)存占用上相對來說更節(jié)儉(可以不用實例化組件通信類);
在 蘑菇街的組件化實踐 里有這種方式的具體實踐;簡化的實現(xiàn)方式類似如下:
// 注冊的過程,某一個組件內(nèi)部注冊的實現(xiàn)
+ (void)registerComponent {
[[ModuleManager sharedInstance] registerURLPattern:@"url://PageA" withCallBack:^(NSDictionary *param) {
PageAViewController *detailVC = [[ModuleAPageViewController alloc] init];
//完成傳參與跳轉(zhuǎn)
參數(shù)param解析與傳遞
頁面跳轉(zhuǎn)
}
//在ModuleManager內(nèi)部,注冊與調(diào)用的實現(xiàn)
- (void)registerURLPattern:(NSString *)urlPattern withCallBack:(componentBlock)blk {
[self.cache setObject:blk forKey:urlPattern];
}
//通過傳入URL完成回調(diào)
- (BOOL)openRoute:(NSString *)urlStr withParams:(nullable NSDictionary *)userInfo {
...
Block block = self.cache[urlStr]; //獲取出回調(diào)
block(userInfo); //執(zhí)行回調(diào)
...
}
以上兩種通過URL的方式注冊的回調(diào),還需要解決應(yīng)該怎么與調(diào)用方約定調(diào)用的參數(shù)傳遞的,回調(diào)的block里可以獲取出正確的參數(shù)并做解析使用;蘑菇街內(nèi)部為了解決這個問題,在內(nèi)部做了一個網(wǎng)頁統(tǒng)一管理所有的URL與參數(shù)傳遞的約定;
通過“Protocol-Class” 注冊實現(xiàn)的組件化
這種方式通過給每一個組件提供一個滿足通信協(xié)議(Protocol)的通信對象;并在應(yīng)用啟動時注冊“Protocol與通信對象”之間的Map關(guān)聯(lián);回調(diào)時在通過Protocol獲取出Map內(nèi)部的通信對象,并調(diào)用通信對象內(nèi)部對應(yīng)的協(xié)議方法完成跨組件間的跳轉(zhuǎn);注冊與調(diào)用的過程類似以下示例:
//注冊的過程,buildModules是之前讀取配置記錄下來的組件通信類
//遍歷每一個組件類,并在實例化通信對象后,通過Protocal方式的完成注冊
for (Class cls in [ModuleManager sharedInstance].buildModules) {
//實例化通信類
id impl = [[cls alloc] init];
// Protocol-Service的注冊
if ([impl respondsToSelector:@selector(registModuleServices)]) {
//通信類內(nèi)部需要實現(xiàn)registModuleServices方法,方法內(nèi)登記所有需要注冊的Protocol-Class的關(guān)系
// service的注冊,serviceArr接收組件內(nèi)所有的Protocol-Class注冊對象
NSArray<TYModuleServiceInfo *> *serviceArr;
if ([impl respondsToSelector:@selector(registModuleServices)]) {
serviceArr = [impl registModuleServices];
}
[serviceArr enumerateObjectsUsingBlock:^(ModuleServiceInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop) {
//遍歷所有的Protocol對象,并把Protocol和info對象的關(guān)聯(lián) 存放在一個NSDictionary中
NSString *protocolStr = NSStringFromProtocol(serviceInfo.protocol);
[self.servicesMapping setObject: info forKey:protocolStr];
}];
}
}
//調(diào)用的過程,以某一個組件間調(diào)用為例
//ModuleManager根據(jù)Protocol或取出通信對象,并調(diào)用通信對象內(nèi)部實現(xiàn)的通信協(xié)議方法,完成跨組件調(diào)用
id< ModuleADetailProtocol > impl = [ModuleManager serviceOfProtocol:@protocol(ModuleADetailProtocol)];
[impl gotoMoudleASubPageViewController:nil];
//serviceOfProtocol方法內(nèi)部的實現(xiàn)
- (nullable id)serviceOfProtocol:(Protocol *)protocol {
ModuleServiceInfo *info = self.servicesMapping[NSStringFromProtocol(protocol)];
Class cls = info.implClass;
id implInstance = nil;
implInstance = [self singleInstanceOfClass:cls];
return implInstance;
}
這種方式組件間的通信通過約定好的通信協(xié)議,并在組件的通信類中實現(xiàn)好協(xié)議方法;在跨組件調(diào)用時只需要根據(jù)對應(yīng)的協(xié)議名稱獲取出組件的通信對象,調(diào)用對應(yīng)的協(xié)議方法即可完成;
以上就是通過提前注冊的方式實現(xiàn)的組件化方案,基本原理都是在“ ModuleManager”中通過 提前注冊的方式讓服務(wù)可以被ModuleManager層發(fā)現(xiàn),之后根據(jù)不同的注冊方式調(diào)用對應(yīng)的組件通信方法,完成跨組件調(diào)用;
方案二:通過集成阿里的 BeeLive 實現(xiàn)組件化
BeeHive
是一個比較重量級的開源框架,關(guān)注點是方便APP實現(xiàn)模塊化編程;能達到組件化的效果,比較適合大型APP的接入與使用;每一個獨立的模塊還能接受到所有的AppDelegate代理事件;
BeeHive主要也是通過模塊注冊的方式實現(xiàn)模塊化,其中就包括“Protocol-Class”的形式;在應(yīng)用啟動時會完成所有模塊服務(wù)的注冊;
方案三:Casa提出的組件化方案
Casa的組件化方案相對而言是最簡單也是耦合性最低的,組件化的目的同時能很好的達到;
前面提到過的幾種方式都需要提前注冊組件的服務(wù)類,提前注冊的目的是為了可以根據(jù)注冊的Key(不管是URL還是Protocol)去發(fā)現(xiàn)對應(yīng)的服務(wù);然后在調(diào)起對應(yīng)的服務(wù)完成組件調(diào)用;Casa認為:在iOS領(lǐng)域,服務(wù)的發(fā)現(xiàn)不需要通過注冊的方式,通過運行時就能做到;因此Casa實現(xiàn)的組件化方案是基于runtime的方式調(diào)用到對應(yīng)的服務(wù)的;
這種方式的原理是通過封裝“Target-Action”層實現(xiàn)組件通信模塊,并通過中間層“CTMediator”完成跨組件間的調(diào)用;“Target”是對象,Action是對象內(nèi)的方法;通過在每個組件內(nèi)部封裝一個Target層,來提供組件的對外通信;
在“CTMediator”中通過調(diào)用“performTarget: action: pramas:”方法調(diào)用到組件的通信方法,完成跨組件調(diào)用;方法的內(nèi)部實現(xiàn)大概如下:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
。。。。
NSString *targetClassString = 由targetName轉(zhuǎn)化而來;
//初始化出對應(yīng)Target對象
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
// 初始化出Action方法
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (shouldCacheTarget) {
//是否執(zhí)行緩存處理
。。。
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params]; //調(diào)用到對應(yīng)方法
#pragma clang diagnostic pop
}
這里不同組件的調(diào)用方法如果都封裝在“CTMediator”類內(nèi)部的話,會導(dǎo)致這個類過于龐大和復(fù)雜,難以維護;解決方式是給每一個Target層封裝一個CTMediator的分類;達到不同組件模塊的接口分離;具體的實踐可以看這個demo
在這個組件化框架中還做了遠程調(diào)用與本地調(diào)用的路徑區(qū)分,方便處理一些遠程與本地調(diào)用發(fā)生異常是的可能存在的不同處理方式等;因為本質(zhì)上組件化是需要為遠程調(diào)用服務(wù)的;