[iOS] 組件化方案學(xué)習(xí) - CTMediator

1. Target-Action

這種方案是基于 OCruntime、category 特性動態(tài)獲取模塊,例如通過 NSClassFromString 獲取類并創(chuàng)建實(shí)例,通過 performSelector + NSInvocation動態(tài)調(diào)用方法。

這種方案的代表框架:CTMediator,實(shí)現(xiàn)原理可以看這篇文章,具體實(shí)踐可以看這篇文章

本篇文章主要就是通過上面提到的兩篇文章學(xué)習(xí)使用并記錄一下CTMediator這個(gè)框架,然后分析一下其在組件化實(shí)施中的優(yōu)點(diǎn)和缺點(diǎn)。

2. CTMediator的實(shí)現(xiàn)

我們先來看一下該方案實(shí)現(xiàn)的架構(gòu)圖,如下圖所示:

截屏2021-04-11 下午6.38.48.png

我們以 Module_A為例,打算將 Module_A抽離為單獨(dú)的組件,在使用CTMediator 此種方案時(shí),需要先給 CTMediator添加一個(gè)相應(yīng)的 CTMediator+A 的分類,這個(gè)分類里面包含了Module_A對外提供的接口,以及負(fù)責(zé)具體實(shí)現(xiàn)的 Target,并且使用performTarget:action方法來調(diào)用Target 的方法,示例如下:

NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionAViewController = @"getAViewController";

@implementation CTMediator (A)

- (UIViewController *)CTMediator_AViewController
{  
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action: kCTMediatorActionAViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之后,可以由外界選擇是push還是present
        return viewController;
    } else {
        // 這里處理異常場景,具體如何處理取決于產(chǎn)品
        return [[UIViewController alloc] init];
    }
}

我們可以看到上面的performTarget:action方法是 CTMediator 類中的,這個(gè)并不影響我們繼續(xù)學(xué)習(xí),它其實(shí)是將 Target 字符串,用 NSClassFromString 反射方法,構(gòu)建了 Target 對應(yīng)類的實(shí)例對象,然后調(diào)用了 action 對應(yīng)的方法。

我們繼續(xù)往下看Module_A對應(yīng)的 Target_A,這個(gè) Target_AModule_A有單向引用,負(fù)責(zé)對于Module_A暴露接口方法的具體實(shí)現(xiàn),比如上面的那個(gè)方法,將會調(diào)用到Target_A中的getAViewController方法,如下:

#import "DemoModuleAViewController.h"

@implementation Target_A

- (UIViewController *)Action_getAViewController:(NSDictionary *)params
{
    // 因?yàn)閍ction是從屬于ModuleA的,所以action直接可以使用ModuleA里的所有聲明
    DemoModuleAViewController *viewController = [[DemoModuleAViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
}
@end

從而實(shí)現(xiàn)了對Module_A 中方法的調(diào)用。

CTMediator 主要是利用了Category 分類提供的特性,去調(diào)用組件對應(yīng)的Catetory中的的方法,進(jìn)而再去調(diào)用Category分類中指定的 Target 的 Action方法,這一步是利用反射 + NSInvocation調(diào)用方法,下面源碼分析時(shí)會有介紹。

3. CTMediator 源碼分析

CTMediator代碼很簡短,也很容易理解,但這并不是我們要學(xué)習(xí)的重點(diǎn),我們需要了解的是CTMediator這種實(shí)現(xiàn)方案的思想,不同項(xiàng)目中對于組件之間通信的方案是不一樣的,這個(gè)就需要在實(shí)踐的時(shí)候具體去衡量了。

我們以上面提到的performTarget:action:params:方法為切入點(diǎn):

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    // 支持 Swift
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // 創(chuàng)建 Target 對應(yīng)的類名
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    // 創(chuàng)建類的實(shí)例對象,這里為了效率,使用了緩存
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    if (target == nil) {
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    // 獲取 action 對應(yīng)的方法
    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;
    }
    
    // 如果需要緩存的話,就將剛才創(chuàng)建的實(shí)例對象加入緩存
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }
    
    // 去調(diào)用 target 實(shí)例對象的 action 方法
    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];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}

上面有對于方法或者類找不到的容錯(cuò)處理,這里就不做介紹了,該方法主要是通過 NSClassFromString 將傳入的target 轉(zhuǎn)成類,并創(chuàng)建一個(gè)該類的對象,然后通過 NSSelectorFromString 將傳入的 action 轉(zhuǎn)成 方法,然后調(diào)用targetaction 方法,調(diào)用的方法實(shí)現(xiàn)如下:

- (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);
    }
...
}

就是獲取 action 對應(yīng)的方法簽名,然后使用NSInvocation調(diào)用, 或者使用系統(tǒng)的performSelector:withObject:調(diào)用方法。

4. 總結(jié)

優(yōu)點(diǎn):

  • 利用 category 可以明確聲明的接口,進(jìn)行編譯檢查
  • 實(shí)現(xiàn)方式屬于輕量級

缺點(diǎn):

  • 需要給每一個(gè)模塊創(chuàng)建對應(yīng)的 targetmediator 分類,模塊化代碼時(shí)較為繁瑣
  • category 中仍然使用了字符串硬編碼,內(nèi)部使用字典傳參
  • 無法保證所調(diào)用的目標(biāo)模塊一定存在,運(yùn)行時(shí)才能發(fā)現(xiàn)錯(cuò)誤
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容