1. Target-Action
這種方案是基于 OC
的runtime
碍遍、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)圖菠净,如下圖所示:
我們以 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_A
對 Module_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)用target
的 action
方法,調(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:¶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);
}
...
}
就是獲取 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)的
target
和mediator
分類姐扮,模塊化代碼時(shí)較為繁瑣 - 在
category
中仍然使用了字符串硬編碼絮供,內(nèi)部使用字典傳參 - 無法保證所調(diào)用的目標(biāo)模塊一定存在,運(yùn)行時(shí)才能發(fā)現(xiàn)錯(cuò)誤