iOS架構(gòu)優(yōu)化 - 組件化

組件化概述

在一個(gè)App長時(shí)間的發(fā)展的過程中烹植,必然存在著一下的問題:

  1. 項(xiàng)目臃腫不堪:除了必要的蓖墅,如AFN,SD等三方庫外叉橱,其代碼都存在于主工程中,每一次都要編譯整個(gè)工程的代碼谬莹,且組員之間的修改及其容易出現(xiàn)沖突,效率極低
  2. 團(tuán)隊(duì)規(guī)模變化:由于公司的人員之間的變動,負(fù)責(zé)的代碼相互交接到處職責(zé)不清附帽,代碼沖突混亂
  3. 業(yè)務(wù)增長迭代:先如今公司業(yè)務(wù)都增長迅速埠戳,敏捷開發(fā)盛行,所以就需要一個(gè)靈活多變的結(jié)構(gòu)來應(yīng)對不同的需求
  4. 代碼混編:當(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)如下:


結(jié)構(gòu)示意圖

拆分后的主要實(shí)現(xiàn)的目標(biāo)如下:

  1. 基礎(chǔ)組件獨(dú)立:保證所有的底層功能組件從主工程抽出覆履,獨(dú)立與主工程之外蹋盆,便于復(fù)用费薄、業(yè)務(wù)模塊的調(diào)用
  2. 業(yè)務(wù)模塊劃分與拆解:將業(yè)務(wù)按對應(yīng)用途進(jìn)行劃分和拆解,想辦法切斷各業(yè)務(wù)之間的強(qiáng)依賴栖雾;
  3. 所有組件 / 模塊獨(dú)立編譯:所有功能組件和業(yè)務(wù)模塊能夠獨(dú)立于主工程進(jìn)行編譯义锥,有各自的 Demo 工程;
  4. 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)

  1. URL的map規(guī)則是需要注冊的
  2. URL鏈接里面關(guān)于組件和頁面的名字都是硬編碼创泄,參數(shù)也都是硬編碼艺玲。而且每個(gè)URL參數(shù)字段都必須要一個(gè)文檔進(jìn)行維護(hù),這個(gè)對于業(yè)務(wù)開發(fā)人員也是一個(gè)負(fù)擔(dān)
  3. URL短連接散落在整個(gè)App四處鞠抑,維護(hù)起來有點(diǎn)麻煩
  4. 對于傳遞NSObject的參數(shù),URL是不夠友好的忌警,它最多是傳遞一個(gè)字典

URL的統(tǒng)跳解耦的簡單實(shí)現(xiàn)

  1. 注冊統(tǒng)跳路由搁拙,建立頁面與統(tǒng)跳的一一對應(yīng)的管理秒梳,維護(hù)Map。

注冊路由的地方箕速,根據(jù)項(xiàng)目實(shí)際情況來定奪酪碘,可以放在load方法里,或者在啟動時(shí)盐茎,防止在調(diào)用時(shí)兴垦,無注冊的情況

[[Router defaultRouter] registerWithPName:@"hallfollow" handler:[HallRouter class]];
  1. 建立對應(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;
    }
}
  1. 在其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解決解耦的問題馏谨。

中間層簡單實(shí)現(xiàn)

如果單純增加中間層别渔,那么就會如上圖一樣,中間層會導(dǎo)入各模塊惧互,導(dǎo)致所以的耦合都在中間層哎媚,那么中間層就會變得無比龐大。

由于直接使用performSelector喊儡,對于參數(shù)傳遞并不友好拨与,并且有太多的硬編碼和崩潰的隱患,所以需要對其進(jìn)行改造艾猜。主要邏輯如下:

  1. 獲取到對應(yīng)targetName买喧,并拼接前綴Target_獲取到對應(yīng)的實(shí)現(xiàn)類
  2. 獲取到對應(yīng)的actionName捻悯,并拼接前綴Action_獲取到對應(yīng)的方法
  3. 通過NSInvocation對消息進(jìn)行轉(zhuǎn)發(fā)并做基礎(chǔ)類型的容錯(cuò)處理
  4. 最終調(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:&params 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:&params 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:&params 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:&params 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:&params 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框架辩蛋。

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種:

  1. 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];
        }
    }];
}

  1. 在合適的時(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)的資源文件。

全局事件分發(fā)

- (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ā)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玫锋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子讼呢,更是在濱河造成了極大的恐慌撩鹿,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悦屏,死亡現(xiàn)場離奇詭異节沦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)础爬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門甫贯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人幕帆,你說我怎么就攤上這事获搏。” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵常熙,是天一觀的道長纬乍。 經(jīng)常有香客問我,道長裸卫,這世上最難降的妖魔是什么仿贬? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮墓贿,結(jié)果婚禮上茧泪,老公的妹妹穿的比我還像新娘。我一直安慰自己聋袋,他們只是感情好队伟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著幽勒,像睡著了一般嗜侮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啥容,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天锈颗,我揣著相機(jī)與錄音,去河邊找鬼咪惠。 笑死击吱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遥昧。 我是一名探鬼主播覆醇,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼渠鸽!你這毒婦竟也來了叫乌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤徽缚,失蹤者是張志新(化名)和其女友劉穎憨奸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凿试,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡排宰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了那婉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片板甘。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖详炬,靈堂內(nèi)的尸體忽然破棺而出盐类,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布在跳,位于F島的核電站枪萄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏猫妙。R本人自食惡果不足惜瓷翻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望割坠。 院中可真熱鬧齐帚,春花似錦、人聲如沸彼哼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沪羔。三九已至饥伊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蔫饰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工愉豺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留篓吁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓蚪拦,卻偏偏與公主長得像杖剪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子驰贷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344