組件化-CTMediator

組件化的意義

當業(yè)務變得越來越復雜的時候,整個工程代碼量隨時間推移會越來越多。最大的影響是降低開發(fā)人員的開發(fā)效率焚刺,比如編譯時間匪蝙,包括合并代碼的時間主籍。組件化的過程中會把不同的業(yè)務拆分出來,每個獨立的業(yè)務層的模塊能夠只依賴它自己直接相關的業(yè)務的情況下獨立運行逛球。如何理解直接依賴呢千元,我覺得可以從編譯的角度去判斷,經(jīng)常會遇到的一種情況是A依賴B颤绕,B依賴C和D幸海,C和D又依賴一堆其他的模塊,為了讓A能夠運行起來奥务,最后可能是依賴了大量的其他模塊∥锒溃現(xiàn)在組件化的目的并不只是把不同的業(yè)務模塊化,而是希望A不直接依賴B氯葬,通過抽象或者其他方式來讓A可以調(diào)用到B的方法挡篓,A只依賴一個中心化的組件提供的服務,來實現(xiàn)A可以不依賴B的情況下獨立運行起來帚称。至于中心化的組件要怎樣通過A的請求上下文來找到對應的對象以及調(diào)用到對象的方法并返回給A正確的結果官研,這是組件化需要做的事情。

當我們完成了組件化之后好處除了可以提高開發(fā)效率闯睹,還可以減少開發(fā)工作∠酚穑現(xiàn)在每個模塊都更獨立了更容易維護了,也不需要很關注其他模塊B的細節(jié)楼吃,比如B引用了其他的模塊C和D始花。每個人都只需要把注意力放到自己的業(yè)務上就可以了,工程也可以進行優(yōu)化孩锡,比如把代碼打成二進制包酷宵,減少編譯時間或者直接只配置必要的組件通過殼工程進行開發(fā),開發(fā)完成再通過pod發(fā)布新的版本浮创。

方案

調(diào)研了一下組件化相關的實現(xiàn)方案忧吟,整體上有兩種基本的思路砌函。一種是基于NSInvocation實現(xiàn)的斩披,因為NSInvocation可以動態(tài)調(diào)用某個對象的方法,相當于一個萬能的方法讹俊,由上層對象傳入的上下文對象來初始化NSInvocation垦沉,然后觸發(fā)對象方法的調(diào)用,代表框架是CTMediator仍劈。另外一種思路是基于服務注冊的思路厕倍,通過中心化的服務管理對象來注冊和分發(fā)服務,通過接口來抽象方法本身贩疙,從而業(yè)務層不需要依賴具體的方法實現(xiàn)對象轉(zhuǎn)而依賴接口讹弯,而調(diào)用方通過之前注冊的映射動態(tài)創(chuàng)建實際的組件對象進行調(diào)用况既,代表框架是BeeHive。這篇文章组民,主要是分析CTMediator相關的對象的封裝以及接口設計棒仍,之后對BeeHive會單獨進行分析 算是對這個主題的總結和學習。

本地調(diào)用

eg:點擊ViewController中的table某一行臭胜,跳轉(zhuǎn)到AViewController莫其。通過這個例子來分析一下CTMediator是怎么來實現(xiàn)Client代碼對組件方法的調(diào)用:
ViewController.m:

#import <CTMediator/CTMediator.h>
#import <A_Category/CTMediator+A.h>

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.row == 0) {
        UIViewController *viewController = [[CTMediator sharedInstance] A_Category_Objc_ViewControllerWithCallback:^(NSString *result) {
            NSLog(@"%@", result);
        }];
        [self.navigationController pushViewController:viewController animated:YES];
    }
}

對于ViewController對象來說,目的是為了能跳轉(zhuǎn)到B頁面耸三,但是沒有直接引用B的頭文件乱陡,而是依賴了兩個跟CTMediator相關的頭文件 <CTMediator/CTMediator.h> 和 <A_Category/CTMediator+A.h>。依賴前者是必須的仪壮,因為在ViewController需要訪問CTMediator對象憨颠,而后者是因為調(diào)用的A_Category_Objc_ViewControllerWithCallback方法,而這個方法是定義在后者的。

CTMediator+A.h

#import <CTMediator/CTMediator.h>
#import <UIKit/UIKit.h>

@interface CTMediator (A)
  
- (UIViewController *)A_Category_Objc_ViewControllerWithCallback:(void(^)(NSString *result))callback;

@end

CTMediator+A.m

#import "CTMediator+A.h"

@implementation CTMediator (A)
  
- (UIViewController *)A_Category_Objc_ViewControllerWithCallback:(void (^)(NSString *))callback
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"callback"] = callback;
    return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO];
}

在CTMediator+A分類中俱病,擴展CTMediator方法我抠,這些方法是只與A組件相關的。在A_Category_Objc_ViewControllerWithCallback方法的具體實現(xiàn)中淫茵,對傳進來的callback參數(shù)做了一層封裝放到了params字典中,然后調(diào)用了CTMediator的方法performTarget:action:params:shouldCacheTarget蹬跃〕妆瘢可以看到,前面兩個參數(shù)是hardcode方式傳入的蝶缀,為什么這里可以hardcode丹喻,因為現(xiàn)在是在與A直接有關聯(lián)的這個category中的,所以它能夠把A相關的信息寫死到這里翁都。相對而言碍论,問題不是很大。

CTMediator.h

@interface CTMediator : NSObject

+ (instancetype _Nonnull)sharedInstance;

// 遠程App調(diào)用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;
// 本地組件調(diào)用入口
- (id _Nullable )performTarget:(NSString * _Nullable)targetName action:(NSString * _Nullable)actionName params:(NSDictionary * _Nullable)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithFullTargetName:(NSString * _Nullable)fullTargetName;

@end

CTMediator.m

- (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 safeFetchCachedTarget: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) {
        // 這里是處理無響應請求的地方之一柄慰,這個demo做得比較簡單鳍悠,如果沒有可以響應的target,就直接return了坐搔。實際開發(fā)過程中是可以事先給一個固定的target專門用于在這個時候頂上藏研,然后處理這種請求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 這里是處理無響應請求的地方,如果無響應概行,則嘗試調(diào)用對應target的notFound方法統(tǒng)一處理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這里也是處理無響應請求的地方蠢挡,在notFound都沒有的時候,這個demo是直接return了。實際開發(fā)過程中业踏,可以用前面提到的固定的target頂上的禽炬。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            @synchronized (self) {
                [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
}

CTMediator中的performTarget:action:params:shouldCacheTarget 這個萬能方法核心步驟其實就只有3步:

  1. 找到target,生成target對象
  2. 找到target對象的方法
  3. 調(diào)用safePerformAction:target:params:通過NSInvocation動態(tài)調(diào)用方法
    除了這3步勤家,剩下的主要是對于一些對象的緩存處理以及1)和2)失敗的情況下的異常處理瞎抛。通過CTMediator處理之后,最后會調(diào)用到Target_A的Action_Category_ViewController方法却紧。

TargetA.h:

@interface Target_A : NSObject

@end

TargetA.m:

#import "Target_A.h"
#import "AViewController.h"

- (UIViewController *)Action_Category_ViewController:(NSDictionary *)params
{
    typedef void (^CallbackType)(NSString *);
    CallbackType callback = params[@"callback"];
    if (callback) {
        callback(@"success");
    }
    AViewController *viewController = [[AViewController alloc] init];
    return viewController;
}

Target_A算是最終落地的對象桐臊,在Action_Category_ViewController方法中,主要是解析了params參數(shù)從里面獲取對應的參數(shù)信息晓殊。這部分參數(shù)也是寫死的断凶,也是同樣的道理,Target_A已經(jīng)完全屬于AViewController的域了巫俺,與A相關的信息寫死問題不大认烁。除了參數(shù)解析剩下的主要是回調(diào)處理以及AViewController的生成和返回。關于Target_A這個方法里的邏輯介汹,對于不同的組件可能實現(xiàn)完全不同却嗡,但是可以抽象的理解為對參數(shù)的解析以及對目標組件初始化邏輯的封裝。

URL調(diào)用

在上面這個例子中沒有涉及url跳轉(zhuǎn)這種例子的處理嘹承,需要單獨說明一下窗价,這里的URL調(diào)用既可能是遠程調(diào)用,也有可能是本地調(diào)用叹卷。 先看一下接口及其實現(xiàn):

CTMediator.h

// 遠程App調(diào)用入口
- (id _Nullable)performActionWithUrl:(NSURL * _Nullable)url completion:(void(^_Nullable)(NSDictionary * _Nullable info))completion;

CTMediator.m

/*
 scheme://[target]/[action]?[params]
 
 url sample:
 aaa://targetA/actionB?id=1234
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    if (url == nil) {
        return nil;
    }
    
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 這里這么寫主要是出于安全考慮撼港,防止黑客通過遠程方式調(diào)用本地模塊。這里的做法足以應對絕大多數(shù)場景骤竹,如果要求更加嚴苛帝牡,也可以做更加復雜的安全邏輯。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }
    
    // 這個demo針對URL的路由處理非常簡單蒙揣,就只是取對應的target名字和method名字靶溜,但這已經(jīng)足以應對絕大部份需求。如果需要拓展懒震,可以在這個方法調(diào)用之前加入完整的路由邏輯
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}

在對url跳轉(zhuǎn)的處理上罩息,主要有兩步:

  1. 按照 scheme://[target]/[action]?[params] 的方式解析url,獲得target, action, params
  2. 轉(zhuǎn)換成本地調(diào)用流程挎狸,也就是上面例子的那種方式

相當于CTMediator對于url這部分的處理扣汪,最終還是本地調(diào)用的方式断楷,只不過封裝了一層對于url解析的過程锨匆。

設計思想

根據(jù)上面這兩種情況,我們可以畫出整個CTMediator的架構圖:


CTMediator.001.png

CTMediator組件自身主要還是對于本地調(diào)用的處理,因為URL調(diào)用最后也會被轉(zhuǎn)化為對本地接口的調(diào)用恐锣。對于CTMediator自身茅主,它所做的工作主要是對于某個對象方法調(diào)用的請求,按照固定的規(guī)則找到對應的target對象以及對應的selector土榴,target對象以及selector的創(chuàng)建都是通過動態(tài)的方式生成的诀姚,最終也是通過NSInvocator來觸發(fā)方法的動態(tài)調(diào)用。所以CTMediator相當于一個萬能的方法調(diào)用組件玷禽,通過OC Runtime來實現(xiàn)動態(tài)化赫段。

CTMediator組件的重要性自然是不必說的,現(xiàn)在主要說一下CTMediator+X擴展方法以及Target_X對象的意義:

1.CTMediator+X本身是真正對業(yè)務層的接口的封裝矢赁,因為它是通過category來實現(xiàn)的糯笙,所以對業(yè)務層來說調(diào)用X組件的時候跟平時調(diào)用一個普通對象方法沒什么區(qū)別,因為在category里有完整的方法聲明撩银,參數(shù)和返回值都是比較清楚的给涕。如果沒有這一層的封裝,業(yè)務層直接調(diào)用CTMediator本身的方法是非常頭疼的额获,因為涉及參數(shù)拼裝够庙,并且這樣也導致必須要了解參數(shù)將被如何處理這些邏輯,但是這些本應該封裝到X組件內(nèi)部的抄邀。而對于CTMediator本身而言耘眨,它的意義在于創(chuàng)造了一個查找Target對象和Selector的上下文環(huán)境。

2.Target_X的意義在于方法調(diào)用之前準備工作境肾,比如參數(shù)的解析毅桃,AViewController對象的創(chuàng)建等等,是方法調(diào)用之前的相關邏輯的封裝和過濾准夷。假設沒有Target_X這一層存在钥飞,那么X組件內(nèi)部那些原生的業(yè)務代碼必定會耦合一些組件調(diào)用的參數(shù)解析的邏輯,這對于X組件內(nèi)部業(yè)務變化是不利的衫嵌,感覺解耦不夠充分读宙。

總體來看,正因為CTMediator+X的存在使得調(diào)用方可以很方便的調(diào)用X組件的方法楔绞,而因為有Target_X的存在结闸,對于被調(diào)用方X組件來說,自身可以完全不了解任何組件調(diào)用的邏輯酒朵,只需要處理好本身的業(yè)務需求即可桦锄。架構設計本質(zhì)上就是封裝和代碼分層。

工程結構

上面的圖已經(jīng)通過顏色區(qū)分了它們在項目中的結構蔫耽,XViewController和Target_X是在一個pod之下的结耀,因為這兩必須在同一個域之下,Target_X的意義已經(jīng)在上面說過了,它是需要知道XViewController初始化相關邏輯的图甜。而CTMediator+A是需要單獨列一個pod碍粥,它是依賴CTMediator,但是它并不需要放在XViewController的域之中黑毅,它只是CTMediator在XViewController對外接口的分類嚼摩,專門給其他調(diào)用方使用。

CTMediator是完全獨立的矿瘦,列為單獨一個pod枕面。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者缚去。
  • 序言:七十年代末膊畴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子病游,更是在濱河造成了極大的恐慌唇跨,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衬衬,死亡現(xiàn)場離奇詭異买猖,居然都是意外死亡,警方通過查閱死者的電腦和手機滋尉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門玉控,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狮惜,你說我怎么就攤上這事高诺。” “怎么了碾篡?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵虱而,是天一觀的道長。 經(jīng)常有香客問我开泽,道長牡拇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任穆律,我火速辦了婚禮惠呼,結果婚禮上,老公的妹妹穿的比我還像新娘峦耘。我一直安慰自己剔蹋,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布辅髓。 她就那樣靜靜地躺著泣崩,像睡著了一般少梁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上律想,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天猎莲,我揣著相機與錄音绍弟,去河邊找鬼技即。 笑死,一個胖子當著我的面吹牛樟遣,可吹牛的內(nèi)容都是我干的而叼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼豹悬,長吁一口氣:“原來是場噩夢啊……” “哼葵陵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瞻佛,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤脱篙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伤柄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绊困,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年适刀,在試婚紗的時候發(fā)現(xiàn)自己被綠了秤朗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡笔喉,死狀恐怖取视,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情常挚,我是刑警寧澤作谭,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站奄毡,受9級特大地震影響丢早,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秧倾,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一怨酝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧那先,春花似錦农猬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慷垮。三九已至,卻和暖如春揍堕,著一層夾襖步出監(jiān)牢的瞬間料身,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工衩茸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芹血,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓楞慈,卻偏偏與公主長得像幔烛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子囊蓝,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 久違的晴天饿悬,家長會。 家長大會開好到教室時聚霜,離放學已經(jīng)沒多少時間了狡恬。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學鈴聲...
    飄雪兒5閱讀 7,523評論 16 22
  • 今天感恩節(jié)哎蝎宇,感謝一直在我身邊的親朋好友弟劲。感恩相遇!感恩不離不棄夫啊。 中午開了第一次的黨會函卒,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,567評論 0 11
  • 可愛進取,孤獨成精撇眯。努力飛翔报嵌,天堂翱翔。戰(zhàn)爭美好熊榛,孤獨進取锚国。膽大飛翔,成就輝煌玄坦。努力進取血筑,遙望,和諧家園煎楣〔蜃埽可愛游走...
    趙原野閱讀 2,727評論 1 1
  • 在妖界我有個名頭叫胡百曉,無論是何事择懂,只要找到胡百曉即可有解決的辦法喻喳。因為是只狐貍大家以訛傳訛叫我“傾城百曉”,...
    貓九0110閱讀 3,265評論 7 3