關(guān)于組件化的探討已經(jīng)有不少了拔恰,綜合比較了各種方案后因谎,我傾向于使用面向接口的方式進(jìn)行組件化。
這是一篇從代碼層面講解模塊解耦的文章颜懊,會(huì)全方位地展示如何實(shí)踐面向接口的思想财岔,盡量全面地探討在模塊管理和解耦的過(guò)程中,需要考慮到的各種問(wèn)題河爹,并且給出實(shí)際的解決方案匠璧,以及對(duì)應(yīng)的模塊管理開(kāi)源工具:ZIKRouter。你也可以根據(jù)本文的內(nèi)容改造自己現(xiàn)有的方案咸这,即使你的項(xiàng)目不進(jìn)行組件化夷恍,也可以參考本文進(jìn)行代碼解耦。
文章主要內(nèi)容:
- 如何衡量模塊解耦的程度
- 對(duì)比不同方案的優(yōu)劣
- 在編譯時(shí)進(jìn)行靜態(tài)路由檢查媳维,避免使用不存在的模塊
- 如何進(jìn)行模塊解耦酿雪,包括模塊重用、模塊適配侄刽、模塊間通信指黎、子模塊交互
- 模塊的接口和依賴(lài)管理
- 管理界面跳轉(zhuǎn)邏輯
什么是組件化
將模塊單獨(dú)抽離、分層州丹,并制定模塊間通信的方式袋励,從而實(shí)現(xiàn)解耦,以及適應(yīng)團(tuán)隊(duì)開(kāi)發(fā)当叭。
為什么需要組件化
主要有4個(gè)原因:
- 模塊間解耦
- 模塊重用
- 提高團(tuán)隊(duì)協(xié)作開(kāi)發(fā)效率
- 單元測(cè)試
當(dāng)項(xiàng)目越來(lái)越大的時(shí)候茬故,各個(gè)模塊之間如果是直接互相引用,就會(huì)產(chǎn)生許多耦合蚁鳖,導(dǎo)致接口濫用磺芭,當(dāng)某天需要進(jìn)行修改時(shí),就會(huì)牽一發(fā)而動(dòng)全身醉箕,難以維護(hù)钾腺。
問(wèn)題主要體現(xiàn)在:
- 修改某個(gè)模塊的功能時(shí),需要修改許多其他模塊的代碼讥裤,因?yàn)檫@個(gè)模塊被其他模塊引用
- 模塊對(duì)外的接口不明確放棒,外部甚至?xí){(diào)用不應(yīng)暴露的私有接口,修改時(shí)會(huì)耗費(fèi)大量時(shí)間
- 修改的模塊涉及范圍較廣己英,很容易影響其他團(tuán)隊(duì)成員的開(kāi)發(fā)间螟,產(chǎn)生代碼沖突
- 當(dāng)需要抽離模塊到其他地方重用時(shí),會(huì)發(fā)現(xiàn)耦合導(dǎo)致根本無(wú)法單獨(dú)抽離
- 模塊間的耦合導(dǎo)致接口和依賴(lài)混亂,難以編寫(xiě)單元測(cè)試
所以需要減少模塊之間的耦合厢破,用更規(guī)范的方式進(jìn)行模塊間交互荣瑟。這就是組件化,也可以叫做模塊化摩泪。
你的項(xiàng)目是否需要組件化
組件化也不是必須的笆焰,有些情況下并不需要組件化:
- 項(xiàng)目較小,模塊間交互簡(jiǎn)單见坑,耦合少
- 模塊沒(méi)有被多個(gè)外部模塊引用嚷掠,只是一個(gè)單獨(dú)的小模塊
- 模塊不需要重用,代碼也很少被修改
- 團(tuán)隊(duì)規(guī)模很小
- 不需要編寫(xiě)單元測(cè)試
組件化也是有一定成本的荞驴,你需要花時(shí)間設(shè)計(jì)接口不皆,分離代碼,所以并不是所有的模塊都需要組件化戴尸。
不過(guò)粟焊,當(dāng)你發(fā)現(xiàn)這幾個(gè)跡象時(shí),就需要考慮組件化了:
- 模塊邏輯復(fù)雜孙蒙,多個(gè)模塊間頻繁互相引用
- 項(xiàng)目規(guī)模逐漸變大项棠,修改代碼變得越來(lái)越困難
- 團(tuán)隊(duì)人數(shù)變多,提交的代碼經(jīng)常和其他成員沖突
- 項(xiàng)目編譯耗時(shí)較大
- 模塊的單元測(cè)試經(jīng)常由于其他模塊的修改而失敗
組件化方案的8條指標(biāo)
決定了要開(kāi)始組件化之路后挎峦,就需要思考我們的目標(biāo)了香追。一個(gè)組件化方案需要達(dá)到怎樣的效果呢?我在這里給出8個(gè)理想情況下的指標(biāo):
- 模塊間沒(méi)有直接耦合坦胶,一個(gè)模塊內(nèi)部的修改不會(huì)影響到另一個(gè)模塊
- 模塊可以被單獨(dú)編譯
- 模塊間能夠清晰地進(jìn)行數(shù)據(jù)傳遞
- 模塊可以隨時(shí)被另一個(gè)提供了相同功能的模塊替換
- 模塊的對(duì)外接口容易查找和維護(hù)
- 當(dāng)模塊的接口改變時(shí)透典,使用此模塊的外部代碼能夠被高效地重構(gòu)
- 盡量用最少的修改和代碼,讓現(xiàn)有的項(xiàng)目實(shí)現(xiàn)模塊化
- 支持 Objective-C 和 Swift顿苇,以及混編
前4條用于衡量一個(gè)模塊是否真正解耦峭咒,后4條用于衡量在項(xiàng)目實(shí)踐中的易用程度。最后一條必須支持 Swift纪岁,是因?yàn)?Swift 是一個(gè)必然的趨勢(shì)凑队,如果你的方案不支持 Swift,說(shuō)明這個(gè)方案在將來(lái)的某個(gè)時(shí)刻必定要改進(jìn)改變幔翰,而到時(shí)候所有基于這個(gè)方案實(shí)現(xiàn)的模塊都會(huì)受到影響漩氨。
基于這8個(gè)指標(biāo),我們就能在一定程度上對(duì)我們的方案做出衡量了遗增。
方案對(duì)比
現(xiàn)在主要有3種組件化方案:URL 路由叫惊、target-action、protocol 匹配做修。
接下來(lái)我們就比較一下這幾種組件化方案霍狰,看看它們各有什么優(yōu)缺點(diǎn)抡草。必須要先說(shuō)明的是,沒(méi)有一個(gè)完美的方案能滿(mǎn)足所有場(chǎng)景下的需求蚓耽,需要根據(jù)每個(gè)項(xiàng)目的需求選擇最適合的方案渠牲。
URL 路由
目前 iOS 上絕大部分的路由工具都是基于 URL 匹配的旋炒,或者是根據(jù)命名約定步悠,用 runtime 方法進(jìn)行動(dòng)態(tài)調(diào)用。
這些動(dòng)態(tài)化的方案的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單瘫镇,缺點(diǎn)是需要維護(hù)字符串表鼎兽,或者依賴(lài)于命名約定,無(wú)法在編譯時(shí)暴露出所有問(wèn)題铣除,需要在運(yùn)行時(shí)才能發(fā)現(xiàn)錯(cuò)誤谚咬。
代碼示例:
// 注冊(cè)某個(gè)URL
[URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {
UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];
return editorViewController;
}];
// 調(diào)用路由
[URLRouter openURL:@"app://editor/?debug=true" completion:^(NSDictionary *info) {
}];
URL router 的優(yōu)點(diǎn):
- 極高的動(dòng)態(tài)性,適合經(jīng)常開(kāi)展運(yùn)營(yíng)活動(dòng)的 app尚粘,例如電商
- 方便地統(tǒng)一管理多平臺(tái)的路由規(guī)則
- 易于適配 URL Scheme
URL router 的缺點(diǎn):
- 傳參方式有限择卦,并且無(wú)法利用編譯器進(jìn)行參數(shù)類(lèi)型檢查,因此所有的參數(shù)都只能從字符串中轉(zhuǎn)換而來(lái)
- 只適用于界面模塊郎嫁,不適用于通用模塊
- 不能使用 designated initializer 聲明必需參數(shù)
- 要讓 view controller 支持 url秉继,需要為其新增初始化方法,因此需要對(duì)模塊做出修改
- 不支持 storyboard
- 無(wú)法明確聲明模塊提供的接口泽铛,只能依賴(lài)于接口文檔尚辑,重構(gòu)時(shí)無(wú)法確保修改正確
- 依賴(lài)于字符串硬編碼,難以管理
- 無(wú)法保證所使用的模塊一定存在
- 解耦能力有限盔腔,url 的”注冊(cè)”杠茬、”實(shí)現(xiàn)”、”使用”必須用相同的字符規(guī)則弛随,一旦任何一方做出修改都會(huì)導(dǎo)致其他方的代碼失效瓢喉,并且重構(gòu)難度大
字符串解耦的問(wèn)題
如果用上面的8個(gè)指標(biāo)來(lái)衡量,URL 路由只能滿(mǎn)足”支持模塊單獨(dú)編譯”舀透、”支持 OC 和 Swift”兩條栓票。它的解耦程度非常一般。
所有基于字符串的解耦方案其實(shí)都可以說(shuō)是偽解耦盐杂,它們只是放棄了編譯依賴(lài)逗载,但是當(dāng)代碼變化之后,即便能夠編譯運(yùn)行链烈,邏輯仍然是錯(cuò)誤的厉斟。
例如修改了模塊定義時(shí)的 URL:
// 注冊(cè)某個(gè)URL
[URLRouter registerURL:@"app://editorView" handler:^(NSDictionary *userInfo) {
...
}];
那么調(diào)用者的 URL 也必須修改,代碼仍然是有耦合的强衡,只不過(guò)此時(shí)編譯器無(wú)法檢查而已擦秽。這會(huì)導(dǎo)致維護(hù)更加困難,一旦 URL 中的參數(shù)有了增減,或者決定替換為另一個(gè)模塊感挥,參數(shù)命名有了變化缩搅,幾乎沒(méi)有高效的方式來(lái)重構(gòu)代碼〈ビ祝可以使用宏定義來(lái)管理字符串硼瓣,不過(guò)這要求所有模塊都使用同一個(gè)頭文件,并且也無(wú)法解決參數(shù)類(lèi)型和數(shù)量變化的問(wèn)題置谦。
URL 路由適合用來(lái)做遠(yuǎn)程模塊的網(wǎng)絡(luò)協(xié)議交互堂鲤,而在管理本地模塊時(shí),最大的甚至是唯一的優(yōu)勢(shì)媒峡,就是適合經(jīng)澄疗埽跨多端運(yùn)營(yíng)活動(dòng)的 app,因?yàn)榭梢杂蛇\(yùn)營(yíng)人員統(tǒng)一管理多平臺(tái)的路由規(guī)則谅阿。
代表框架
改進(jìn):避免字符串管理
改進(jìn) URL 路由的方式半哟,就是避免使用字符串,通過(guò)接口管理模塊签餐。
參數(shù)可以通過(guò) protocol 直接傳遞寓涨,能夠利用編譯器檢查參數(shù)類(lèi)型,并且在 ZIKRouter 中贱田,能通過(guò)路由聲明和編譯檢查缅茉,保證所使用的模塊一定存在。在為模塊創(chuàng)建路由時(shí)男摧,也無(wú)需修改模塊的代碼蔬墩。
但是必須要承認(rèn)的是,盡管 URL 路由缺點(diǎn)多多耗拓,但它在跨平臺(tái)路由管理上的確是最適合的方案拇颅。因此 ZIKRouter 也對(duì) URL 路由做出了支持,在用 protocol 管理的同時(shí)乔询,可以通過(guò)字符串匹配 router樟插,也能和其他 URL router 框架對(duì)接。
Target-Action 方案
有一些模塊管理工具基于 Objective-C 的 runtime竿刁、category 特性動(dòng)態(tài)獲取模塊黄锤。例如通過(guò)NSClassFromString
獲取類(lèi)并創(chuàng)建實(shí)例,通過(guò)performSelector:
NSInvocation
動(dòng)態(tài)調(diào)用方法食拜。
例如基于 target-action 模式的設(shè)計(jì)鸵熟,大致是利用 category 為路由工具添加新接口,在接口中通過(guò)字符串獲取對(duì)應(yīng)的類(lèi)负甸,再用 runtime 創(chuàng)建實(shí)例流强,動(dòng)態(tài)調(diào)用實(shí)例的方法痹届。
示例代碼:
// 模塊管理者,提供了動(dòng)態(tài)調(diào)用 target-action 的基本功能
@interface Mediator : NSObject
+ (instancetype)sharedInstance;
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
@end
// 在 category 中定義新接口
@interface Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController;
@end
@implementation Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController {
// 使用字符串硬編碼打月,通過(guò) runtime 動(dòng)態(tài)創(chuàng)建 Target_Editor队腐,并調(diào)用 Action_viewController:
UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];
return viewController;
}
@end
// 調(diào)用者通過(guò) Mediator 的接口調(diào)用模塊
UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
// 模塊提供者提供 target-action 的調(diào)用方式
@interface Target_Editor : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
@implementation Target_Editor
- (UIViewController *)Action_viewController:(NSDictionary *)params {
// 參數(shù)通過(guò)字典傳遞,無(wú)法保證類(lèi)型安全
EditorViewController *viewController = [[EditorViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
優(yōu)點(diǎn):
- 利用 category 可以明確聲明接口奏篙,進(jìn)行編譯檢查
- 實(shí)現(xiàn)方式輕量
缺點(diǎn):
- 需要在 mediator 和 target 中重新添加每一個(gè)接口柴淘,模塊化時(shí)代碼較為繁瑣
- 在 category 中仍然引入了字符串硬編碼,內(nèi)部使用字典傳參报破,一定程度上也存在和 URL 路由相同的問(wèn)題
- 無(wú)法保證所使用的模塊一定存在悠就,target 模塊在修改后千绪,使用者只有在運(yùn)行時(shí)才能發(fā)現(xiàn)錯(cuò)誤
- 過(guò)于依賴(lài) runtime 特性充易,無(wú)法應(yīng)用到純 Swift 上。在 Swift 中擴(kuò)展 mediator 時(shí)荸型,無(wú)法使用純 Swift 類(lèi)型的參數(shù)
- 可能會(huì)創(chuàng)建過(guò)多的 target 類(lèi)
- 使用 runtime 相關(guān)的接口調(diào)用任意類(lèi)的任意方法盹靴,需要注意別被蘋(píng)果的審核誤傷。參考:Are performSelector and respondsToSelector banned by App Store?
字典傳參的問(wèn)題
字典傳參時(shí)無(wú)法保證參數(shù)的數(shù)量和類(lèi)型瑞妇,只能依賴(lài)調(diào)用約定稿静,就和字符串傳參一樣,一旦某一方做出修改辕狰,另一方也必須修改改备。
相比于 URL 路由,target-action 通過(guò) category 的接口把字符串管理的問(wèn)題縮小到了 mediator 內(nèi)部蔓倍,不過(guò)并沒(méi)有完全消除悬钳,而且在其他方面仍然有很多改進(jìn)空間。上面的8個(gè)指標(biāo)中其實(shí)只能滿(mǎn)足第2個(gè)”支持模塊單獨(dú)編譯”偶翅,另外在和接口相關(guān)的第3默勾、5、6點(diǎn)上聚谁,比 URL 路由要有改善母剥。
代表框架
CTMediator源碼分析
通過(guò)分類(lèi)中調(diào)用的performTarget來(lái)到CTMediator中的具體實(shí)現(xiàn),即performTarget:action:params:shouldCacheTarget:形导,主要是通過(guò)傳入的name环疼,找到對(duì)應(yīng)的target 和 action
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
//在swift中使用時(shí),需要傳入對(duì)應(yīng)項(xiàng)目的target名稱(chēng)朵耕,否則會(huì)找不到視圖控制器
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target 生成target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
//swift中target文件名拼接
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
//OC中target文件名拼接
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
//緩存中查找target
NSObject *target = [self safeFetchCachedTarget:targetClassString];
//緩存中沒(méi)有target
if (target == nil) {
//通過(guò)字符串獲取對(duì)應(yīng)的類(lèi)
Class targetClass = NSClassFromString(targetClassString);
//創(chuàng)建實(shí)例
target = [[targetClass alloc] init];
}
// generate action 生成action方法名稱(chēng)
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
//通過(guò)方法名字符串獲取對(duì)應(yīng)的sel
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這里是處理無(wú)響應(yīng)請(qǐng)求的地方之一炫隶,這個(gè)demo做得比較簡(jiǎn)單,如果沒(méi)有可以響應(yīng)的target憔披,就直接return了等限。實(shí)際開(kāi)發(fā)過(guò)程中是可以事先給一個(gè)固定的target專(zhuān)門(mén)用于在這個(gè)時(shí)候頂上爸吮,然后處理這種請(qǐng)求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
//是否需要緩存
if (shouldCacheTarget) {
[self safeSetCachedTarget:target key:targetClassString];
}
//是否響應(yīng)sel
if ([target respondsToSelector:action]) {
//動(dòng)態(tài)調(diào)用方法
return [self safePerformAction:action target:target params:params];
} else {
// 這里是處理無(wú)響應(yīng)請(qǐng)求的地方,如果無(wú)響應(yīng)望门,則嘗試調(diào)用對(duì)應(yīng)target的notFound方法統(tǒng)一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里也是處理無(wú)響應(yīng)請(qǐng)求的地方形娇,在notFound都沒(méi)有的時(shí)候,這個(gè)demo是直接return了筹误。實(shí)際開(kāi)發(fā)過(guò)程中桐早,可以用前面提到的固定的target頂上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
@synchronized (self) {
[self.cachedTarget removeObjectForKey:targetClassString];
}
return nil;
}
}
}
- 進(jìn)入safePerformAction:target:params:實(shí)現(xiàn)厨剪,主要是通過(guò)invocation進(jìn)行參數(shù)傳遞+消息轉(zhuǎn)發(fā)
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
//獲取方法簽名
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
//獲取方法簽名中的返回類(lèi)型哄酝,然后根據(jù)返回值完成參數(shù)傳遞
const char* retType = [methodSig methodReturnType];
//void類(lèi)型
if (strcmp(retType, @encode(void)) == 0) {
...
}
//...省略其他類(lèi)型的判斷
}
改進(jìn):避免字典傳參
Target-Action 方案最大的優(yōu)點(diǎn)就是整個(gè)方案實(shí)現(xiàn)輕量,并且也一定程度上明確了模塊的接口祷膳。只是這些接口都需要通過(guò) Target-Action 封裝一次陶衅,并且每個(gè)模塊都要?jiǎng)?chuàng)建一個(gè) target 類(lèi),既然如此直晨,直接用 protocol 進(jìn)行接口管理會(huì)更加簡(jiǎn)單搀军。
ZIKRouter 避免使用 runtime 獲取和調(diào)用模塊,因此可以適配 OC 和 swift勇皇。同時(shí)罩句,基于 protocol 匹配的方式,避免引入字符串硬編碼敛摘,能夠更好地管理模塊门烂,也避免了字典傳參。
protocol class
protocol匹配的實(shí)現(xiàn)思路
是:
1兄淫、將
protocol
和對(duì)應(yīng)的類(lèi)
進(jìn)行字典匹配
2屯远、通過(guò)用
protocol
獲取class
,在動(dòng)態(tài)創(chuàng)建實(shí)例
protocol比較典型的三方框架就是阿里的BeeHive拖叙。BeeHive
借鑒了Spring Service氓润、Apache DSO的架構(gòu)理念,采用AOP+擴(kuò)展App生命周期API
形式薯鳍,將業(yè)務(wù)功能
咖气、基礎(chǔ)功能
模塊以模塊方式以解決大型應(yīng)用中的復(fù)雜問(wèn)題,并讓模塊之間以Service形式調(diào)用
挖滤,將復(fù)雜問(wèn)題切分崩溪,以AOP方式模塊化服務(wù)。
BeeHive 核心思想
1斩松、各個(gè)模塊間調(diào)用從直接調(diào)用對(duì)應(yīng)模塊伶唯,變成調(diào)用
Service
的形式,避免了直接依賴(lài)惧盹。2乳幸、App生命周期的分發(fā)瞪讼,將耦合在
AppDelegate
中邏輯拆分,每個(gè)模塊以微應(yīng)用的形式獨(dú)立存在粹断。
示例如下:
//******** 1符欠、注冊(cè)
[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];
//******** 2、使用
#import "BHService.h"
id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
優(yōu)點(diǎn)
1瓶埋、利用接口調(diào)用希柿,實(shí)現(xiàn)了參數(shù)傳遞時(shí)的類(lèi)型安全
2、直接使用模塊的protocol接口养筒,無(wú)需再重復(fù)封裝
缺點(diǎn)
1曾撤、用框架來(lái)創(chuàng)建所有對(duì)象,創(chuàng)建方式不同晕粪,即不支持外部傳入?yún)?shù)
2挤悉、用
OC runtime
創(chuàng)建對(duì)象,不支持swift3兵多、只做了
protocol
和class
的匹配尖啡,不支持更復(fù)雜的創(chuàng)建方式 和依賴(lài)注入4、無(wú)法保證所使用的protocol 一定存在對(duì)應(yīng)的模塊剩膘,也無(wú)法直接判斷某個(gè)protocol是否能用于獲取模塊
除了BeeHive
,還有Swinject
BeeHive 模塊注冊(cè)
在BeeHive
主要是通過(guò)BHModuleManager
來(lái)管理各個(gè)模塊的盆顾。BHModuleManager
中只會(huì)管理已經(jīng)被注冊(cè)過(guò)的模塊怠褐。
BeeHive提供了三種不同的調(diào)用形式,靜態(tài)plist
您宪,動(dòng)態(tài)注冊(cè)
奈懒,annotation
。Module宪巨、Service之間沒(méi)有關(guān)聯(lián)磷杏,每個(gè)業(yè)務(wù)模塊可以單獨(dú)實(shí)現(xiàn)Module或者Service的功能。
1捏卓、 Annotation方式注冊(cè)
這種方式主要是通過(guò)BeeHiveMod
宏進(jìn)行Annotation
標(biāo)記
//***** 使用
BeeHiveMod(ShopModule)
//***** BeeHiveMod的宏定義
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
//***** BeeHiveDATA的宏定義
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
//***** 全部轉(zhuǎn)換出來(lái)后為下面的格式
char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";
這里針對(duì)__attribute
需要說(shuō)明以下幾點(diǎn)
第一個(gè)參數(shù)
used
:用來(lái)修飾函數(shù)极祸,被used修飾以后,意味著即使函數(shù)沒(méi)有被引用怠晴,在Release下也不會(huì)被優(yōu)化遥金。如果不加這個(gè)修飾,那么Release環(huán)境鏈接器下會(huì)去掉沒(méi)有被引用的段蒜田。通過(guò)使用
__attribute__((section("name")))
來(lái)指明哪個(gè)段稿械。數(shù)據(jù)則用__attribute__((used))
來(lái)標(biāo)記,防止鏈接器會(huì)優(yōu)化刪除未被使用的段冲粤,然后將模塊注入到__DATA
中
此時(shí)Module已經(jīng)被存儲(chǔ)到Mach-O文件的特殊段中美莫,那么如何取呢页眯?
- 進(jìn)入
BHReadConfiguration
方法,主要是通過(guò)Mach-O
找到存儲(chǔ)的數(shù)據(jù)段厢呵,取出放入數(shù)組中
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
// 找到之前存儲(chǔ)的數(shù)據(jù)段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片內(nèi)存
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
unsigned long counter = size/sizeof(void*);
// 把特殊段里面的數(shù)據(jù)都轉(zhuǎn)換成字符串存入數(shù)組中
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;
BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
return configs;
}
2餐茵、讀取本地Pilst文件
- 首先,需要設(shè)置好路徑
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可選述吸,默認(rèn)為BeeHive.bundle/BeeHive.plist
-
創(chuàng)建plist文件忿族,
Plist
文件的格式也是數(shù)組中包含多個(gè)字典。字典里面有兩個(gè)Key蝌矛,一個(gè)是@"moduleLevel"
道批,另一個(gè)是@"moduleClass"
。注意根
的數(shù)組的名字叫@“moduleClasses”
入撒。 進(jìn)入
loadLocalModules
方法隆豹,主要是從Plist
里面取出數(shù)組,然后把數(shù)組加入到BHModuleInfos
數(shù)組里面茅逮。
//初始化context時(shí)璃赡,加載Modules和Services
-(void)setContext:(BHContext *)context
{
_context = context;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self loadStaticServices];
[self loadStaticModules];
});
}
??
//加載modules
- (void)loadStaticModules
{
// 讀取本地plist文件里面的Module,并注冊(cè)到BHModuleManager的BHModuleInfos數(shù)組中
[[BHModuleManager sharedManager] loadLocalModules];
//注冊(cè)所有modules献雅,在內(nèi)部根據(jù)優(yōu)先級(jí)進(jìn)行排序
[[BHModuleManager sharedManager] registedAllModules];
}
??
- (void)loadLocalModules
{
//plist文件路徑
NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
//判斷文件是否存在
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
return;
}
//讀取整個(gè)文件[@"moduleClasses" : 數(shù)組]
NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
//通過(guò)moduleClasses key讀取 數(shù)組 [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]]
NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy;
//遍歷數(shù)組
[self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]];
}];
[modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (!moduleInfoByClass[[obj objectForKey:kModuleInfoNameKey]]) {
//存儲(chǔ)到 BHModuleInfos 中
[self.BHModuleInfos addObject:obj];
}
}];
}
3碉考、load方法注冊(cè)
該方法注冊(cè)Module
就是在Load
方法里面注冊(cè)Module的類(lèi)
+ (void)load
{
[BeeHive registerDynamicModule:[self class]];
}
- 進(jìn)入
registerDynamicModule
實(shí)現(xiàn)
+ (void)registerDynamicModule:(Class)moduleClass
{
[[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
??
- (void)registerDynamicModule:(Class)moduleClass
{
[self registerDynamicModule:moduleClass shouldTriggerInitEvent:NO];
}
??
- (void)registerDynamicModule:(Class)moduleClass
shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
{
[self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent];
}
其底層還是同第一種方式一樣,最終會(huì)走到addModuleFromObject:shouldTriggerInitEvent:
方法中
- load方法挺身,還可以使用
BH_EXPORT_MODULE
宏代替
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
BH_EXPORT_MODULE
宏里面可以傳入一個(gè)參數(shù)侯谁,代表是否異步加載Module模塊
,如果是YES
就是異步加載
章钾,如果是NO
就是同步加載
墙贱。
2、BeeHive 模塊事件
BeeHive會(huì)給每個(gè)模塊提供生命周期事件贱傀,用于與BeeHive宿主環(huán)境進(jìn)行必要信息交互惨撇,感知模塊生命周期的變化。
BeeHive各個(gè)模塊會(huì)收到一些事件府寒。在BHModuleManager
中魁衙,所有的事件被定義成了BHModuleEventType
枚舉。如下所示椰棘,其中有2個(gè)事件很特殊纺棺,一個(gè)是BHMInitEvent
,一個(gè)是BHMTearDownEvent
typedef NS_ENUM(NSInteger, BHModuleEventType)
{
//設(shè)置Module模塊
BHMSetupEvent = 0,
//用于初始化Module模塊邪狞,例如環(huán)境判斷祷蝌,根據(jù)不同環(huán)境進(jìn)行不同初始化
BHMInitEvent,
//用于拆除Module模塊
BHMTearDownEvent,
BHMSplashEvent,
BHMQuickActionEvent,
BHMWillResignActiveEvent,
BHMDidEnterBackgroundEvent,
BHMWillEnterForegroundEvent,
BHMDidBecomeActiveEvent,
BHMWillTerminateEvent,
BHMUnmountEvent,
BHMOpenURLEvent,
BHMDidReceiveMemoryWarningEvent,
BHMDidFailToRegisterForRemoteNotificationsEvent,
BHMDidRegisterForRemoteNotificationsEvent,
BHMDidReceiveRemoteNotificationEvent,
BHMDidReceiveLocalNotificationEvent,
BHMWillPresentNotificationEvent,
BHMDidReceiveNotificationResponseEvent,
BHMWillContinueUserActivityEvent,
BHMContinueUserActivityEvent,
BHMDidFailToContinueUserActivityEvent,
BHMDidUpdateUserActivityEvent,
BHMHandleWatchKitExtensionRequestEvent,
BHMDidCustomEvent = 1000
};
主要分為三種
-
1、
系統(tǒng)事件
:主要是指Application生命周期事件
!
一般的做法是AppDelegate
改為繼承自BHAppDelegate
@interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>
-
2帆卓、
應(yīng)用事件
:官方給出的流程圖巨朦,其中modSetup
米丘、modInit
等,可以用于編碼實(shí)現(xiàn)各插件模塊的設(shè)置與初始化糊啡。 3拄查、
自定義事件
以上所有的事件都可以通過(guò)調(diào)用BHModuleManager
的triggerEvent:
來(lái)處理。
- (void)triggerEvent:(NSInteger)eventType
{
[self triggerEvent:eventType withCustomParam:nil];
}
??
- (void)triggerEvent:(NSInteger)eventType
withCustomParam:(NSDictionary *)customParam {
[self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam];
}
??
#pragma mark - module protocol
- (void)handleModuleEvent:(NSInteger)eventType
forTarget:(id<BHModuleProtocol>)target
withCustomParam:(NSDictionary *)customParam
{
switch (eventType) {
//初始化事件
case BHMInitEvent:
//special
[self handleModulesInitEventForTarget:nil withCustomParam :customParam];
break;
//析構(gòu)事件
case BHMTearDownEvent:
//special
[self handleModulesTearDownEventForTarget:nil withCustomParam:customParam];
break;
//其他3類(lèi)事件
default: {
NSString *selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
[self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam];
}
break;
}
}
從上面的代碼中可以發(fā)現(xiàn)棚蓄,除去BHMInitEvent
初始化事件和BHMTearDownEvent
拆除Module事件這兩個(gè)特殊事件以外堕扶,所有的事件都是調(diào)用的handleModuleEvent:forTarget:withSeletorStr:andCustomParam:
方法,其內(nèi)部實(shí)現(xiàn)主要是遍歷 moduleInstances
實(shí)例數(shù)組梭依,調(diào)用performSelector:withObject:
方法實(shí)現(xiàn)對(duì)應(yīng)方法調(diào)用
- (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 實(shí)例數(shù)組稍算,調(diào)用performSelector:withObject:方法實(shí)現(xiàn)對(duì)應(yīng)方法調(diào)用
[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"
//進(jìn)行方法調(diào)用
[moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
}
}];
}
注意:這里所有的
Module
必須是遵循BHModuleProtocol
的,否則無(wú)法接收到這些事件的消息役拴。
3糊探、BeeHive模塊調(diào)用
在BeeHive中是通過(guò)BHServiceManager
來(lái)管理各個(gè)Protocol
的。BHServiceManager
中只會(huì)管理已經(jīng)被注冊(cè)過(guò)的Protocol
河闰。
注冊(cè)Protocol
的方式總共有三種科平,和注冊(cè)Module
是一樣一一對(duì)應(yīng)的
1、Annotation方式注冊(cè)
//****** 1姜性、通過(guò)BeeHiveService宏進(jìn)行Annotation標(biāo)記
BeeHiveService(HomeServiceProtocol,BHViewController)
//****** 2瞪慧、宏定義
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
//****** 3、轉(zhuǎn)換后的格式污抬,也是將其存儲(chǔ)到特殊的段
char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";
2汞贸、讀取本地plist文件
- 首先同Module一樣,需要先設(shè)置好路徑
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
-
設(shè)置plist文件
同樣也是在
setContext
時(shí)注冊(cè)services
//加載services
-(void)loadStaticServices
{
[BHServiceManager sharedManager].enableException = self.enableException;
[[BHServiceManager sharedManager] registerLocalServices];
}
??
- (void)registerLocalServices
{
NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
//獲取plist文件路徑
NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
if (!plistPath) {
return;
}
NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
[self.lock lock];
//遍歷并存儲(chǔ)到allServicesDict中
for (NSDictionary *dict in serviceList) {
NSString *protocolKey = [dict objectForKey:@"service"];
NSString *protocolImplClass = [dict objectForKey:@"impl"];
if (protocolKey.length > 0 && protocolImplClass.length > 0) {
[self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
}
[self.lock unlock];
}
3印机、load方法注冊(cè)
在Load方法里面注冊(cè)Protocol
協(xié)議,主要是調(diào)用BeeHive
里面的registerService:service:
完成protocol
的注冊(cè)
+ (void)load
{
[[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
}
??
- (void)registerService:(Protocol *)proto service:(Class) serviceClass
{
[[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
}
到此门驾,三種方式就創(chuàng)建完成了
Protocol的獲取
Protocol
與Module
的區(qū)別在于射赛,Protocol
比Module
多了一個(gè)方法,可以返回Protocol實(shí)例對(duì)象
- (id)createService:(Protocol *)proto;
{
return [[BHServiceManager sharedManager] createService:proto];
}
??
- (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 {
if (!serviceName.length) {
serviceName = NSStringFromProtocol(service);
}
id implInstance = nil;
//判斷protocol是否已經(jīng)注冊(cè)過(guò)
if (![self checkValidService:service]) {
if (self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
}
}
NSString *serviceStr = serviceName;
//如果有緩存奶是,則直接從緩存中獲取
if (shouldCache) {
id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
if (protocolImpl) {
return protocolImpl;
}
}
//獲取類(lèi)后楣责,然后響應(yīng)下層的方法
Class implClass = [self serviceImplClass:service];
if ([[implClass class] respondsToSelector:@selector(singleton)]) {
if ([[implClass class] singleton]) {
if ([[implClass class] respondsToSelector:@selector(shareInstance)])
//創(chuàng)建單例對(duì)象
implInstance = [[implClass class] shareInstance];
else
//創(chuàng)建實(shí)例對(duì)象
implInstance = [[implClass alloc] init];
if (shouldCache) {
//緩存
[[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
return implInstance;
} else {
return implInstance;
}
}
}
return [[implClass alloc] init];
}
createService
會(huì)先檢查Protocol協(xié)議是否是注冊(cè)過(guò)的。然后接著取出字典里面對(duì)應(yīng)的Class聂沙,如果實(shí)現(xiàn)了shareInstance
方法秆麸,那么就創(chuàng)建一個(gè)單例對(duì)象
,如果沒(méi)有及汉,那么就創(chuàng)建一個(gè)實(shí)例對(duì)象
沮趣。如果還實(shí)現(xiàn)了singleton,就能進(jìn)一步的把implInstance
和serviceStr
對(duì)應(yīng)的加到BHContext
的servicesByName
字典里面緩存
起來(lái)坷随。這樣就可以隨著上下文傳遞了
- 進(jìn)入
serviceImplClass
實(shí)現(xiàn)弧腥,從這里可以看出 protocol和類(lèi)是通過(guò)字典
綁定的屡久,protocol
作為key
徽千,serviceImp
(類(lèi)的名字)作為value
- (Class)serviceImplClass:(Protocol *)service
{
//通過(guò)字典將 協(xié)議 和 類(lèi) 綁定,其中協(xié)議作為key翁狐,serviceImp(類(lèi)的名字)作為value
NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
if (serviceImpl.length > 0) {
return NSClassFromString(serviceImpl);
}
return nil;
}
Module & Protocol
這里簡(jiǎn)單總結(jié)下:
對(duì)于
Module
:數(shù)組存儲(chǔ)對(duì)于
Protocol
:通過(guò)字典將protocol
與類(lèi)進(jìn)行綁定,key
為protocol
凌蔬,value
為serviceImp
即類(lèi)名
輔助類(lèi)
BHConfig
類(lèi):是一個(gè)單例露懒,其內(nèi)部有一個(gè)NSMutableDictionary
類(lèi)型的config
屬性,該屬性維護(hù)了一些動(dòng)態(tài)的環(huán)境變量砂心,作為BHContext
的補(bǔ)充存在BHContext
類(lèi):是一個(gè)單例懈词,其內(nèi)部有兩個(gè)NSMutableDictionary
的屬性,分別是modulesByName
和servicesByName
计贰。這個(gè)類(lèi)主要用來(lái)保存上下文信息的钦睡。例如在application:didFinishLaunchingWithOptions:
的時(shí)候,就可以初始化大量的上下文信息
//保存信息
[BHContext shareInstance].application = application;
[BHContext shareInstance].launchOptions = launchOptions;
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可選躁倒,默認(rèn)為BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
BHTimeProfiler
類(lèi):用來(lái)進(jìn)行計(jì)算時(shí)間性能方面的ProfilerBHWatchDog
類(lèi):用來(lái)開(kāi)一個(gè)線(xiàn)程荞怒,監(jiān)聽(tīng)主線(xiàn)程是否堵塞