組件化概述
在一個(gè)App長時(shí)間的發(fā)展的過程中烹植,必然存在著一下的問題:
- 項(xiàng)目臃腫不堪:除了必要的蓖墅,如AFN,SD等三方庫外叉橱,其代碼都存在于主工程中,每一次都要編譯整個(gè)工程的代碼谬莹,且組員之間的修改及其容易出現(xiàn)沖突,效率極低
- 團(tuán)隊(duì)規(guī)模變化:由于公司的人員之間的變動,負(fù)責(zé)的代碼相互交接到處職責(zé)不清附帽,代碼沖突混亂
- 業(yè)務(wù)增長迭代:先如今公司業(yè)務(wù)都增長迅速埠戳,敏捷開發(fā)盛行,所以就需要一個(gè)靈活多變的結(jié)構(gòu)來應(yīng)對不同的需求
- 代碼混編:當(dāng)出現(xiàn)新技術(shù)時(shí)蕉扮,一般都是需要來嘗試的整胃,比武Swift, RN,Flutter等,當(dāng)這些代碼和現(xiàn)有代碼結(jié)合喳钟,就會產(chǎn)生相當(dāng)多意想不到的問題屁使,所以必須要做到良好的代碼隔離
當(dāng)出現(xiàn)以上情況時(shí),說明你的代碼就急需要使用組件化來規(guī)避這些問題了奔则。使用組件化蛮寂,主要有一下好處
- 加快編譯速度,不用再編譯組件 / 模塊外沒有被依賴到的代碼易茬;
- 便于將每個(gè)模塊指定給不同負(fù)責(zé)人進(jìn)行管理共郭;
- 降低合并難度,減小沖突和出錯(cuò)概率疾呻,提高業(yè)務(wù)開發(fā)效率除嘹;
- 將 其他代碼 和 OC 代碼進(jìn)行分離,不同語言開發(fā)順暢岸蜗,可替換性強(qiáng)尉咕;
- 可為模塊編寫單元測試,提高工作效率璃岳,同時(shí)方便測試人員進(jìn)行有針對性的測試年缎。
組件化的模塊拆分原則
當(dāng)我們梳理目前項(xiàng)目的代碼時(shí),需要按照一下3個(gè)原則來進(jìn)行铃慷,這樣能夠?qū)I(yè)務(wù)和架構(gòu)進(jìn)行更好的拆分:
高層依賴底層单芜,下層不能對上層有依賴的關(guān)系
這點(diǎn)是基本的設(shè)計(jì)原則,可以通過依賴倒置來設(shè)計(jì)犁柜。同層級的模塊不依賴或者盡量少依賴
這點(diǎn)同時(shí)也是基本的設(shè)計(jì)原則洲鸠,可以通過控制反轉(zhuǎn)來設(shè)計(jì),典型的就是使用觀察者模式來實(shí)現(xiàn)同一個(gè)層級模塊的解耦馋缅。最小知識原則和自完備性
一個(gè)獨(dú)立的模塊盡量減少對其他低層模塊的依賴扒腕,比如一個(gè)模塊只是依賴低層模塊的某個(gè)類的方法,不妨把這個(gè)方法拷貝到此模塊中萤悴,如此一來這個(gè)模塊就具有了更好的自完備性瘾腰。
組件化的模塊分層結(jié)構(gòu)
根據(jù)以上組件化拆分的原則,拆分后項(xiàng)目的主要結(jié)構(gòu)如下:
拆分后的主要實(shí)現(xiàn)的目標(biāo)如下:
- 基礎(chǔ)組件獨(dú)立:保證所有的底層功能組件從主工程抽出覆履,獨(dú)立與主工程之外蹋盆,便于復(fù)用费薄、業(yè)務(wù)模塊的調(diào)用
- 業(yè)務(wù)模塊劃分與拆解:將業(yè)務(wù)按對應(yīng)用途進(jìn)行劃分和拆解,想辦法切斷各業(yè)務(wù)之間的強(qiáng)依賴栖雾;
- 所有組件 / 模塊獨(dú)立編譯:所有功能組件和業(yè)務(wù)模塊能夠獨(dú)立于主工程進(jìn)行編譯义锥,有各自的 Demo 工程;
- CocoaPods 發(fā)布:在內(nèi)網(wǎng) GitLab 進(jìn)行發(fā)布岩灭,并且之后對每個(gè)模塊用 GitFlow 工作流進(jìn)行管理和后續(xù)發(fā)布工作拌倍。【關(guān)于使用CocoaPods的拆分噪径,可以參考此博客:iOS 組件化-使用cocoapods集成實(shí)戰(zhàn)演練】
組件化解耦的幾種方式
目前市面上主要存在著3種組件化解耦方案柱恤,分別是URL解耦
,Target-Action中間層
找爱,register-protocol注冊法
梗顺。
通過URL的統(tǒng)跳解耦
URL解耦的概述
統(tǒng)跳路由是頁面解耦的最常見方式,大量應(yīng)用于前端頁面车摄。通過把一個(gè) URL 與一個(gè)頁面綁定寺谤,需要時(shí)通過 URL 可以方便的打開相應(yīng)頁面。
它通過URL來請求資源吮播。不管是H5变屁,RN,Weex意狠,iOS界面或者組件請求資源的方式就都統(tǒng)一了粟关。URL里面也會帶上參數(shù),這樣調(diào)用什么界面或者組件都可以环戈。所以這種方式是最容易闷板,也是最先可以想到的。
優(yōu)點(diǎn):
服務(wù)器可以動態(tài)的控制頁面跳轉(zhuǎn)院塞,可以統(tǒng)一處理頁面出問題之后的錯(cuò)誤處理遮晚,可以統(tǒng)一三端,iOS拦止,Android县遣,H5 / RN / Flutter 的請求方式。
缺點(diǎn):
- URL的map規(guī)則是需要注冊的
- URL鏈接里面關(guān)于組件和頁面的名字都是硬編碼创泄,參數(shù)也都是硬編碼艺玲。而且每個(gè)URL參數(shù)字段都必須要一個(gè)文檔進(jìn)行維護(hù),這個(gè)對于業(yè)務(wù)開發(fā)人員也是一個(gè)負(fù)擔(dān)
- URL短連接散落在整個(gè)App四處鞠抑,維護(hù)起來有點(diǎn)麻煩
- 對于傳遞NSObject的參數(shù),URL是不夠友好的忌警,它最多是傳遞一個(gè)字典
URL的統(tǒng)跳解耦的簡單實(shí)現(xiàn)
- 注冊統(tǒng)跳路由搁拙,建立頁面與統(tǒng)跳的一一對應(yīng)的管理秒梳,維護(hù)Map。
注冊路由的地方箕速,根據(jù)項(xiàng)目實(shí)際情況來定奪酪碘,可以放在load
方法里,或者在啟動時(shí)盐茎,防止在調(diào)用時(shí)兴垦,無注冊的情況
[[Router defaultRouter] registerWithPName:@"hallfollow" handler:[HallRouter class]];
- 建立對應(yīng)
Router
的處理類,用來處理相對應(yīng)的通跳
對于其參數(shù)的處理字柠,可以直接拼接在url后面探越,或者增加一個(gè)extraData
的字典用來進(jìn)行傳遞。
+ (BOOL)openRequest:(IKRouteRequest *)request application:(UIApplication *)application annotation:(id)annotation target:(UIViewController *)target {
if ([request.pName isEqualToString:@"hallfollow"]) {
NSDictionary *options = request.options;
NSString *tab = options[@"tab"];
NSDictionary *dict =[[NSDictionary alloc] initWithObjectsAndKeys:tab,@"tab", nil];
id<NavigationCenterProtocol> navigationCenter = [[ServiceManager sharedInstance] clsServiceForProtocol:@protocol(NavigationCenterProtocol)];
[navigationCenter popToRootFromTarget:target completion:^(UIViewController *currentVC) {
}];
return YES;
}
}
- 在其
Router
的內(nèi)部窑业,通過對URL的解析和封裝钦幔,然后進(jìn)行對應(yīng)的分發(fā)
通過Target-Action方案
眾所周知,如果要解決兩個(gè)模塊之間的耦合關(guān)系常柄,那么在其中間提取出一個(gè)中間層用來處理其事務(wù)是比較合理的手段鲤氢。其中中間層的核心邏輯就是如下面代碼所示,通過字符串獲取到類西潘,并通過performSelector
來調(diào)用相對應(yīng)的函數(shù)卷玉。
Class manager = NSClassFromString(@"GoodsManager");
NSArray *list = [manager performSelector:@selector(getGoodsList)];
//code to handle the list
但是只通過上面是無法實(shí)現(xiàn)解耦的,這種方式存在大量的 hardcode 字符串喷市。無法觸發(fā)代碼自動補(bǔ)全揍庄,容易出現(xiàn)拼寫錯(cuò)誤,而且這類錯(cuò)誤只能在運(yùn)行時(shí)觸發(fā)相關(guān)方法后才能發(fā)現(xiàn)东抹。無論是開發(fā)效率還是開發(fā)質(zhì)量都有較大的影響蚂子。
所以通過上面的分析,實(shí)現(xiàn)一個(gè)方法最主要就是
-
Target
:調(diào)用方 -
Action
:調(diào)用方法 -
Param
:調(diào)用參數(shù)
那么我們在中間層就是要實(shí)現(xiàn)這3方面缭黔,具體的方案可以參考CTMediator的實(shí)現(xiàn)食茎。其主要思想是利用了Target-Action
簡單粗暴的思想,利用Runtime
解決解耦的問題馏谨。
如果單純增加中間層别渔,那么就會如上圖一樣,中間層會導(dǎo)入各模塊惧互,導(dǎo)致所以的耦合都在中間層哎媚,那么中間層就會變得無比龐大。
由于直接使用performSelector
喊儡,對于參數(shù)傳遞并不友好拨与,并且有太多的硬編碼和崩潰的隱患,所以需要對其進(jìn)行改造艾猜。主要邏輯如下:
- 獲取到對應(yīng)targetName买喧,并拼接前綴
Target_
獲取到對應(yīng)的實(shí)現(xiàn)類 - 獲取到對應(yīng)的actionName捻悯,并拼接前綴
Action_
獲取到對應(yīng)的方法 - 通過
NSInvocation
對消息進(jìn)行轉(zhuǎn)發(fā)并做基礎(chǔ)類型的容錯(cuò)處理 - 最終調(diào)用
performSelector:
方法
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這里是處理無響應(yīng)請求的地方之一,這個(gè)demo做得比較簡單淤毛,如果沒有可以響應(yīng)的target今缚,就直接return了。實(shí)際開發(fā)過程中是可以事先給一個(gè)固定的target專門用于在這個(gè)時(shí)候頂上低淡,然后處理這種請求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里是處理無響應(yīng)請求的地方乏悄,如果無響應(yīng)斧吐,則嘗試調(diào)用對應(yīng)target的notFound方法統(tǒng)一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里也是處理無響應(yīng)請求的地方地梨,在notFound都沒有的時(shí)候荆虱,這個(gè)demo是直接return了。實(shí)際開發(fā)過程中纸颜,可以用前面提到的固定的target頂上的兽泣。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
const char* retType = [methodSig methodReturnType];
if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
中間層實(shí)現(xiàn)了消息轉(zhuǎn)發(fā)后,還是不能做到有效的解耦胁孙。我們需要一個(gè)Target_類名
的類來暴露外部調(diào)用的接口唠倦,和一個(gè)對應(yīng)模塊的分類,用于上層封裝CTMediator
的方法涮较,形成隔離稠鼻,減少對庫的依賴。
其分類內(nèi)部分實(shí)現(xiàn)也就是對接口的封裝狂票,也是調(diào)用的CTMediator
的方法候齿,這些模塊的分類可以放在一個(gè)Pod庫中,這樣不同模塊依賴該庫闺属,就可以直接調(diào)用方法了慌盯。
- (void)CTMediator_showAlertWithMessage:(NSString *)message cancelAction:(void(^)(NSDictionary *info))cancelAction confirmAction:(void(^)(NSDictionary *info))confirmAction
{
NSMutableDictionary *paramsToSend = [[NSMutableDictionary alloc] init];
if (message) {
paramsToSend[@"message"] = message;
}
if (cancelAction) {
paramsToSend[@"cancelAction"] = cancelAction;
}
if (confirmAction) {
paramsToSend[@"confirmAction"] = confirmAction;
}
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionShowAlert
params:paramsToSend
shouldCacheTarget:NO];
}
有個(gè)暴露的分類Pod庫,我們還要在自己的模塊中實(shí)現(xiàn)其對應(yīng)暴露接口的實(shí)現(xiàn)掂器,因?yàn)榇藭r(shí)和模塊高度綁定亚皂,所以可以和對應(yīng)模塊放在一起,不必暴露国瓮。形成最終的實(shí)現(xiàn)灭必,這樣一套消息流程就結(jié)束了。
- (id)Action_showAlert:(NSDictionary *)params
{
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
CTUrlRouterCallbackBlock callback = params[@"cancelAction"];
if (callback) {
callback(@{@"alertAction":action});
}
}];
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"confirm" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
CTUrlRouterCallbackBlock callback = params[@"confirmAction"];
if (callback) {
callback(@{@"alertAction":action});
}
}];
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"alert from Module A" message:params[@"message"] preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:cancelAction];
[alertController addAction:confirmAction];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
return nil;
}
最終解耦實(shí)現(xiàn)了下圖所表示的結(jié)構(gòu)
優(yōu)缺點(diǎn)
Target-Action方案的優(yōu)點(diǎn):
- 充分的利用Runtime的特性乃摹,無需注冊這一步禁漓。
- Target-Action方案只有存在組件依賴Mediator這一層依賴關(guān)系。
- 在Mediator中維護(hù)針對Mediator的Category孵睬,每個(gè)category對應(yīng)一個(gè)Target播歼,Categroy中的方法對應(yīng)Action場景。
- Target-Action方案也統(tǒng)一了所有組件間調(diào)用入口肪康。
- Target-Action方案也能有一定的安全保證荚恶,它對url中進(jìn)行Native前綴進(jìn)行驗(yàn)證撩穿。
Target-Action方案的缺點(diǎn):
- Target_Action在Category中將常規(guī)參數(shù)打包成字典磷支,在Target處再把字典拆包成常規(guī)參數(shù)谒撼,這就造成了一部分的硬編碼。
通過注冊協(xié)議方案
如果僅僅通過Target-Action
的方案來解決模塊間的解耦問題雾狈,還是有部分的硬編碼廓潜,而且對于一對多的事件分發(fā)處理還是不到位,所以可以采用注冊協(xié)議方案來解決善榛,比較典型有BeeHive框架辩蛋。
根據(jù)框架圖我們可以知道,其解耦的主要方式是將各模塊暴露的接口和App全局的時(shí)間移盆,沉淀到底層去悼院,并通過Protocol
的方式進(jìn)行分發(fā)。其核心即注冊-分發(fā)
的模式
模塊的注冊
由于我們要使用Protocol
的方式咒循,來進(jìn)行消息的分發(fā)据途,那么必須要有Protocol
的調(diào)用方和實(shí)現(xiàn)方。且Protocol
要與實(shí)現(xiàn)方一一對應(yīng)叙甸,所以必須要有注冊的步驟颖医,不然無法實(shí)現(xiàn)分發(fā)。
模塊的注冊主要分為靜態(tài)注冊和動態(tài)注冊兩種方式裆蒸。都需要維護(hù)一個(gè)全局的Map
來進(jìn)行查找
靜態(tài)注冊
對于靜態(tài)注冊的方式比較簡單熔萧,總結(jié)來說基本就2種:
-
plist
的形式維護(hù)模塊與協(xié)議的對應(yīng)
- (void)loadLocalModules
{
NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
return;
}
NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy;
[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]]) {
[self.BHModuleInfos addObject:obj];
}
}];
}
- 在合適的時(shí)機(jī)讓模塊調(diào)用相對應(yīng)的注冊方法
- (void)registerService:(Protocol *)service implClass:(Class)implClass
{
NSParameterAssert(service != nil);
NSParameterAssert(implClass != nil);
if (![implClass conformsToProtocol:service]) {
if (self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ module does not comply with %@ protocol", NSStringFromClass(implClass), NSStringFromProtocol(service)] userInfo:nil];
}
return;
}
if ([self checkValidService:service]) {
if (self.enableException) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol has been registed", NSStringFromProtocol(service)] userInfo:nil];
}
return;
}
NSString *key = NSStringFromProtocol(service);
NSString *value = NSStringFromClass(implClass);
if (key.length > 0 && value.length > 0) {
[self.lock lock];
[self.allServicesDict addEntriesFromDictionary:@{key:value}];
[self.lock unlock];
}
}
動態(tài)注冊
對于動態(tài)注冊,即沒有對應(yīng)的注冊方法和配置文件僚祷,自動注冊佛致。那么是怎么實(shí)現(xiàn)的呢?
動態(tài)注冊的實(shí)現(xiàn)主要是利用注解和宏定義。因?yàn)楹甓x可以在編譯時(shí)就寫入了Mach-O
文件中的__DATA
段中了辙谜,只需要在dyld
鏈接鏡像文件時(shí)俺榆,把數(shù)據(jù)取出來,然后存入對應(yīng)的字典筷弦,數(shù)組中即完成了注冊流程
如下面代碼肋演,當(dāng)模塊注冊時(shí),宏定義中會在load
方法中增加一個(gè)注冊方法烂琴,將相關(guān)模塊注冊金管理類爹殊,方便時(shí)間分發(fā)
#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
當(dāng)模塊之間調(diào)用時(shí),必須指定協(xié)議和實(shí)現(xiàn)類的綁定奸绷,從而可以在別的模塊調(diào)用梗夸,其綁定的動態(tài)注冊也是一個(gè)宏定義注解。我們可以看到宏定義就是將協(xié)議名和實(shí)現(xiàn)類名存入了__DATA
段
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
---------------------------------------------------------------------------------------------------
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
那么他是怎么從Mach-O
文件中讀取到內(nèi)存的呢号醉,我們知道在App的啟動過程中反症,是通過dyld
來加載相關(guān)的鏡像文件的辛块,那么只需要在啟動鏈接的過程中,把相關(guān)數(shù)據(jù)加載到內(nèi)存中就可以了
我們可以再實(shí)現(xiàn)中發(fā)現(xiàn)一個(gè)全局的靜態(tài)函數(shù)initProphet()
铅碍,其調(diào)用實(shí)在dyld
鏈接之后調(diào)用润绵,會收到一個(gè)全局的dyld_callback
,其中就有Mach-O中存取的數(shù)據(jù)胞谈。
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback);
}
通過調(diào)用BHReadConfiguration
方法尘盼,我們可以在__DATA
中取出我們需要的數(shù)據(jù),并返回一個(gè)數(shù)組
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
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*);
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;
}
通過對數(shù)組的遍歷烦绳,可以獲得相關(guān)的模塊和協(xié)議卿捎,從而調(diào)用注冊方法,完成注冊
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp);
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
for (NSString *modName in mods) {
Class cls;
if (modName) {
cls = NSClassFromString(modName);
if (cls) {
[[BHModuleManager sharedManager] registerDynamicModule:cls];
}
}
}
//register services
NSArray<NSString *> *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
for (NSString *map in services) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];
if (protocol && clsName) {
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
}
全局事件的分發(fā)
了解了注冊的流程径密,對于事件的分發(fā)就比較簡單了午阵。對于全局事件,即App的啟動享扔,閃屏底桂,登錄成功,前后臺等事件伪很。當(dāng)一個(gè)模塊完成了注冊戚啥,想要獲取到這些事件時(shí),只需要遵循相關(guān)協(xié)議
锉试,就可以再其回調(diào)中得到相關(guān)方法的調(diào)用猫十。
對于全局事件的收集,可以看下圖呆盖。在didLanch
等方法中拖云,我們需要初始化我們的管理模塊,并保存相關(guān)上下文,用于模塊的使用应又,并且在靜態(tài)注冊時(shí)宙项,指定相關(guān)的資源文件。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[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";
[BeeHive shareInstance].enableException = YES;
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:@"BeeHive::super start launch"];
[super application:application didFinishLaunchingWithOptions:launchOptions];
id<HomeServiceProtocol> homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
if ([homeVc isKindOfClass:[UIViewController class]]) {
UINavigationController *navCtrl = [[UINavigationController alloc] initWithRootViewController:(UIViewController*)homeVc];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.rootViewController = navCtrl;
[self.window makeKeyAndVisible];
}
return YES;
}
對于一些需要打點(diǎn)的事件尤筐,我們也可以自定義AppDelegate
文件,在BHAppDelegate
中實(shí)現(xiàn)我們的一些基本操作洞就,比如埋點(diǎn)等操作
@interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[BHModuleManager sharedManager] triggerEvent:BHMSetupEvent];
[[BHModuleManager sharedManager] triggerEvent:BHMInitEvent];
dispatch_async(dispatch_get_main_queue(), ^{
[[BHModuleManager sharedManager] triggerEvent:BHMSplashEvent];
});
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
if ([UIDevice currentDevice].systemVersion.floatValue >= 10.0f) {
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
}
#endif
#ifdef DEBUG
[[BHTimeProfiler sharedTimeProfiler] saveTimeProfileDataIntoFile:@"BeeHiveTimeProfiler"];
#endif
return YES;
}
對于事件的分發(fā)盆繁,我們在對應(yīng)的注冊的事件中,通過字符串的轉(zhuǎn)換旬蟋,通過performSelector:
調(diào)用相關(guān)協(xié)議方法即可油昂。
- (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 enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
[[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
}
}];
}
對于事件的接受,遵循協(xié)議,然后實(shí)現(xiàn)需要的方法即可冕碟。
模塊事件的調(diào)用
對于同等級模塊之間事件的調(diào)用如果來解耦拦惋,也是類似的方法,但是他不像系統(tǒng)事件一樣安寺,有系統(tǒng)調(diào)用的代理回調(diào)厕妖。
這時(shí)候應(yīng)該通過注冊管理的方式,將協(xié)議和實(shí)現(xiàn)方通過一一對應(yīng)的方式進(jìn)行注冊管理我衬。調(diào)用方通過調(diào)用協(xié)議中的相關(guān)方法來轉(zhuǎn)發(fā)到實(shí)現(xiàn)方叹放,從而完成解耦饰恕。
首先要提供該模塊暴露的Protocol
挠羔,用來方便外部進(jìn)行調(diào)用,這也是該模塊的"接口"
@protocol HomeServiceProtocol <NSObject, BHServiceProtocol>
-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName;
@end
然后編寫該協(xié)議具體的實(shí)現(xiàn)方并注冊進(jìn)管理模塊
@BeeHiveService(HomeServiceProtocol,BHViewController)
@interface BHViewController ()<HomeServiceProtocol>
@property(nonatomic,strong) NSMutableArray *registerViewControllers;
@end
-(void)registerViewController:(UIViewController *)vc title:(NSString *)title iconName:(NSString *)iconName
{
vc.tabBarItem.image = [UIImage imageNamed:[NSString stringWithFormat:@"Home.bundle/%@", iconName]];
vc.tabBarItem.title = title;
[self.registerViewControllers addObject:vc];
self.viewControllers = self.registerViewControllers;
}
最后再需要調(diào)用的地方埋嵌,通過管理類取出協(xié)議破加,調(diào)用方法即可
id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
[homeVc registerViewController:self title:@"" iconName:@""];
優(yōu)缺點(diǎn)
這種方案ModuleEntry是同時(shí)需要依賴ModuleManager和組件里面的頁面或者組件兩者的。當(dāng)然ModuleEntry也是會依賴ModuleEntryProtocol的雹嗦,但是這個(gè)依賴是可以去掉的范舀,比如用Runtime的方法NSProtocolFromString,加上硬編碼是可以去掉對Protocol的依賴的了罪。但是考慮到硬編碼的方式對出現(xiàn)bug锭环,后期維護(hù)都是不友好的,所以對Protocol的依賴還是不要去除泊藕。
最后一個(gè)缺點(diǎn)是組件方法的調(diào)用是分散在各處的辅辩,沒有統(tǒng)一的入口,也就沒法做組件不存在時(shí)或者出現(xiàn)錯(cuò)誤時(shí)的統(tǒng)一處理娃圆。
參考
CTMediator
蜂鳥商家版 iOS 組件化
iOS 組件化 —— 路由設(shè)計(jì)思路分析
淺談 iOS 組件化開發(fā)