轉(zhuǎn)載自: 知乎-henry磊 iOS組件化的那些事 - CTMediator
前言
在組件化之前井濒,app都是在一個(gè)工程里開(kāi)發(fā)的导狡,開(kāi)發(fā)的人員也是比較少的,業(yè)務(wù)發(fā)展也不是非吵ㄎ耍快,項(xiàng)目中不引用組件化開(kāi)發(fā)也是合適的。但是當(dāng)開(kāi)發(fā)人員越來(lái)越多潘悼,代碼量也就越來(lái)越多,業(yè)務(wù)也就越來(lái)越復(fù)雜爬橡,這時(shí)候單一的開(kāi)發(fā)模式會(huì)顯露出一些弊端:
- 耦合代碼嚴(yán)重【代碼沒(méi)有明確的約束挥等,越來(lái)越臃腫】
- 開(kāi)發(fā)效率不高【難以維護(hù),維護(hù)成本過(guò)高】
為了解決這些問(wèn)題堤尾,于是出現(xiàn)了組件化開(kāi)發(fā)的策略肝劲,擁有以下好處:
- 方便針對(duì)性測(cè)試
- 研發(fā)人員維護(hù)不同Module【提高開(kāi)發(fā)效率,降低維護(hù)成本】
目前組件化開(kāi)發(fā)的方式大約有三種:url - block郭宝、protocol - class 以及CTMediator target - action方案辞槐,本篇主要是本人項(xiàng)目中使用到的CTMediator target - action方案,將從源碼分析粘室,以及其優(yōu)劣性榄檬。
一、準(zhǔn)備
了解下面的概念衔统,對(duì)理解swift的靜態(tài)語(yǔ)言特性是有幫助的鹿榜。
問(wèn): 動(dòng)態(tài)語(yǔ)言海雪、靜態(tài)語(yǔ)言、動(dòng)態(tài)類型語(yǔ)言舱殿、靜態(tài)類型語(yǔ)言奥裸、編譯性語(yǔ)言、解釋性語(yǔ)言區(qū)別沪袭?
1.1 編譯型語(yǔ)言
需通過(guò)編譯器【compliler】將源代碼編譯成機(jī)器碼湾宙,之后才能執(zhí)行的語(yǔ)言。一般需經(jīng)過(guò)編譯【compile】冈绊、鏈接【linker】這兩個(gè)步驟侠鳄,編譯是把源代碼編譯成機(jī)器碼,鏈接是把各個(gè)模塊的機(jī)器碼和依賴庫(kù)串聯(lián)起來(lái)生成可執(zhí)行文件死宣。
優(yōu)點(diǎn):
編譯器一般會(huì)有預(yù)編譯的過(guò)程對(duì)代碼進(jìn)行優(yōu)化伟恶。因?yàn)榫幾g只做一次,運(yùn)行時(shí)不需要編譯毅该,所以編譯型語(yǔ)言的程序執(zhí)行效率高知押。
可以脫離語(yǔ)言環(huán)境獨(dú)立運(yùn)行。
缺點(diǎn):
- 編譯之后如果需要修改某個(gè)模塊就需要重新編譯鹃骂。編譯的時(shí)候根據(jù)對(duì)應(yīng)的運(yùn)行環(huán)境生成機(jī)器碼台盯,不同的操作系統(tǒng)之間移植就會(huì)有問(wèn)題。需要根據(jù)運(yùn)行的操作系統(tǒng)環(huán)境編譯不同的可執(zhí)行文件畏线。
語(yǔ)言: C静盅、C++、Object-C以及Swift
1.2 解釋型語(yǔ)言
解釋型語(yǔ)言的程序不需要編譯寝殴,相比編譯型語(yǔ)言省了道工序蒿叠,解釋性語(yǔ)言在運(yùn)行程序的時(shí)候才逐行翻譯。
優(yōu)點(diǎn):
- 良好平臺(tái)兼容性蚣常,在任何環(huán)境都可以運(yùn)行
- 靈活市咽,修改代碼的時(shí)候修改就可以,可以快速部署
缺點(diǎn):
- 每次運(yùn)行都要解釋一遍抵蚊,性能不如編譯型語(yǔ)言施绎。
語(yǔ)言:JavaScript、Python贞绳、PHP谷醉、Ruby
1.3 動(dòng)態(tài)語(yǔ)言
是一類運(yùn)行時(shí)可以改變其結(jié)構(gòu)的語(yǔ)言:已有的函數(shù),對(duì)象可以被刪除或者其他結(jié)構(gòu)上的變化冈闭。通俗點(diǎn)說(shuō)是運(yùn)行時(shí)可以根據(jù)某些條件改變自身結(jié)構(gòu)俱尼。
語(yǔ)言: Object-C、C#萎攒、JavaScript遇八、PHP矛绘、Python
【Object-C是編譯型語(yǔ)言,但也是動(dòng)態(tài)語(yǔ)言刃永,得益于特有的run time機(jī)制货矮,OC代碼是可以在運(yùn)行時(shí)插入、替換方法的】
1.4 靜態(tài)語(yǔ)言
運(yùn)行時(shí)結(jié)構(gòu)不可變的語(yǔ)言就是靜態(tài)語(yǔ)言揽碘。
語(yǔ)言:Java次屠、C园匹、Swift
1.5 動(dòng)態(tài)類型語(yǔ)言
動(dòng)態(tài)類型語(yǔ)言和動(dòng)態(tài)語(yǔ)言是完全不同的兩個(gè)概念雳刺。
動(dòng)態(tài)類型語(yǔ)言是指在運(yùn)行期間才去做數(shù)據(jù)類型檢查的語(yǔ)言,說(shuō)的是數(shù)據(jù)類型裸违,動(dòng)態(tài)語(yǔ)言說(shuō)的是運(yùn)行是改變結(jié)構(gòu)掖桦,說(shuō)的是代碼結(jié)構(gòu)。
動(dòng)態(tài)類型語(yǔ)言的數(shù)據(jù)類型不是在編譯階段決定的供汛,而是把類型綁定延后到了運(yùn)行階段枪汪。
語(yǔ)言:Python、Ruby怔昨、JavaScript雀久、swift、PHP趁舀。
1.6 靜態(tài)類型語(yǔ)言
靜態(tài)語(yǔ)言的數(shù)據(jù)類型是在編譯其間確定的赖捌,寫編寫代碼的時(shí)候要明確確定變量的數(shù)據(jù)類型。
語(yǔ)言:C矮烹、C++越庇、C#、Java奉狈、Object-C卤唉。
拓展:
@objc 是用來(lái)將Swift 的API導(dǎo)出給Objective-C和Objective-C runtime使用的,如果你的類繼承自O(shè)bjective-C【如NSObject】將會(huì)自動(dòng)被編譯器插入@objc標(biāo)識(shí)仁期。加了@objc標(biāo)識(shí)的方法桑驱、屬性無(wú)法保證都會(huì)被運(yùn)行時(shí)調(diào)用,因?yàn)镾wift會(huì)做靜態(tài)優(yōu)化跛蛋。要想完全被動(dòng)態(tài)調(diào)用碰纬,必須使用dynamic修飾。使用dynamic修飾將會(huì)隱式的加上@objc標(biāo)識(shí)问芬。
二悦析、CTMediator源碼
swift由于靜態(tài)的本質(zhì),至今沒(méi)有什么好的解耦方式此衅,因?yàn)殪o態(tài)語(yǔ)言的類型是在編譯時(shí)就確定的【上面準(zhǔn)備的重要性】强戴,注定了它無(wú)法像上面一樣使用字符串就能獲取到亭螟,所以現(xiàn)在iOS常用的解耦框架都是OC編寫的,如果想要使用解耦框架骑歹,那就只能混編啦预烙!
CTMediator代碼結(jié)構(gòu)圖
下面我們將講解CTMediator源碼具體內(nèi)容。
2.1 target-action工作圖
target-action 中間采取了Runtime來(lái)完成調(diào)用道媚。
2.2 本地調(diào)用入口
下面是本地調(diào)用的CTMediator的源代碼:
本地組件調(diào)用:本地組件A在一處調(diào)用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator發(fā)起了跨組件調(diào)用,CTMediator根據(jù)發(fā)送過(guò)來(lái)的target和action,然后經(jīng)過(guò)OC的runtime機(jī)制轉(zhuǎn)為target實(shí)例以及action,最后調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯,完成要求.
首先利用runtime進(jìn)行反射扁掸,將類字符串和方法字符串轉(zhuǎn)換成類和SEL方法。
/**
* 本地組件調(diào)用入口
*
* @param targetName 類對(duì)象 OC中類對(duì)象是要Target_為前綴的
* @param actionName 方法名稱 最后實(shí)際調(diào)用的是以Action_為前綴的
* @param params 參數(shù)
* @param shouldCacheTarget 是否緩存拼接后的類對(duì)象
*
* @return return value JSon格式的字符串
*/
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
//供swift項(xiàng)目使用最域,swift必須要加入模塊名谴分,swift工程內(nèi)獲取Target需要帶上module的名稱
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];
}
//先從緩存中取對(duì)象
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
//不存在直接根據(jù)字符串創(chuàng)建類,并且初始化對(duì)象
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// 拼裝方法字符串
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
// 生成SEL
SEL action = NSSelectorFromString(actionString);
//先從緩存取镀脂,取不到去創(chuàng)建牺蹄,但是也有可能創(chuàng)建失敗的情況(targetName值不正確)
if (target == nil) {
// 這里是處理無(wú)響應(yīng)請(qǐng)求的地方之一,這個(gè)demo做得比較簡(jiǎn)單薄翅,如果沒(méi)有可以響應(yīng)的target沙兰,就直接return了。實(shí)際開(kāi)發(fā)過(guò)程中是可以事先給一個(gè)固定的target專門用于在這個(gè)時(shí)候頂上翘魄,然后處理這種請(qǐng)求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
// 是否緩存該對(duì)象
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
// 該對(duì)象是否能響應(yīng)調(diào)起該方法
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里是處理無(wú)響應(yīng)請(qǐng)求的地方鼎天,如果無(wú)響應(yīng),則嘗試調(diào)用對(duì)應(yīng)target的notFound方法統(tǒng)一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里也是處理無(wú)響應(yīng)請(qǐng)求的地方暑竟,在notFound都沒(méi)有的時(shí)候斋射,這個(gè)demo是直接return了。實(shí)際開(kāi)發(fā)過(guò)程中光羞,可以用前面提到的固定的target頂上的绩鸣。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
如果target 和 action 都有值,會(huì)調(diào)用[self safePerformAction:action target:target params:params]方法【【生成的類前面的會(huì)加上Target_,生成的方法前面會(huì)加上Action_】】
- (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
}
將消息和消息接受者封裝成一個(gè)對(duì)象纱兑,然后執(zhí)行呀闻。利用target-action生成方法簽名,然后創(chuàng)建NSInvocation對(duì)象潜慎,進(jìn)行執(zhí)行invoke捡多。
這個(gè)方法比較長(zhǎng),但其實(shí)前面就是重復(fù)的代碼铐炫,來(lái)實(shí)現(xiàn)返回類型是基本數(shù)據(jù)類型的調(diào)用垒手,使用的是NSInvocation
來(lái)完成調(diào)用;最后使用performSelector
實(shí)現(xiàn)返回值是對(duì)象的調(diào)用倒信。 最后performSelector方法前后的#pragma clang diagnostic
是消除內(nèi)存警告的預(yù)處理命令
2.2 遠(yuǎn)程調(diào)用入口
遠(yuǎn)程應(yīng)用的調(diào)瓤票帷:遠(yuǎn)程應(yīng)用是通過(guò)openURL的方式,由iOS 系統(tǒng)根據(jù)info.plist里的scheme配置用來(lái)可以找到響應(yīng)的URL的應(yīng)用,應(yīng)用直接通過(guò)AppDelegate接收到URL之后,調(diào)用了CTMediator的OpenURL方法將接收到的信息傳入進(jìn)去.當(dāng)然,CTMediator也可以用CTMediator的openURL:options:方式順便將option也接收,這取決于是否包含了option數(shù)據(jù),傳入U(xiǎn)RL之后,CTMediator進(jìn)行解析URL,將請(qǐng)求的路由到相對(duì)應(yīng)的target-action中,隨后的過(guò)程就變成了上面的本地應(yīng)用調(diào)用過(guò)程了,最終完成了響應(yīng).
/*
scheme://[target]/[action]?[params]
url sample:
aaa://targetA/actionB?id=1234&title=title
[url query]: id=1234&title=title
[url path]: /actionB
[url host]: targetA
*/
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
//url參數(shù)的處理
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]];
}
// 這里這么寫主要是出于安全考慮,防止黑客通過(guò)遠(yuǎn)程方式調(diào)用本地模塊。這里的做法足以應(yīng)對(duì)絕大多數(shù)場(chǎng)景榜掌,如果要求更加嚴(yán)苛优妙,也可以做更加復(fù)雜的安全邏輯。
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
if ([actionName hasPrefix:@"native"]) {
return @(NO);
}
// 這個(gè)demo針對(duì)URL的路由處理非常簡(jiǎn)單憎账,就只是取對(duì)應(yīng)的target名字和method名字套硼,但這已經(jīng)足以應(yīng)對(duì)絕大部份需求。如果需要拓展胞皱,可以在這個(gè)方法調(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;
}
拓展:
NSClassFromString 通過(guò)字符串的名稱來(lái)獲取一個(gè)類邪意,可以根據(jù)Target來(lái)進(jìn)行獲取
NSSelectorFromString 通過(guò)字符串(已存在的方法名稱)獲取一個(gè)SEL
三、項(xiàng)目
從搜索結(jié)果頁(yè)-另個(gè)模塊詳情頁(yè)界面如上,跨越了兩個(gè)模塊,項(xiàng)目采用了CTMediator的Target-Action模式.下面按照?qǐng)?zhí)行的順序截圖如下:
3.1 點(diǎn)擊tableViewCell->func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
3.2 進(jìn)入openBrokerDetailScene,開(kāi)始調(diào)CTMediator【公司采用Viper的變形版架構(gòu)】
3.3 開(kāi)始進(jìn)入EngineToBroker_viewController,調(diào)用self.performTarget("Broker", action: "brokerDetailVC", params: params, shouldCacheTarget: false)【返回vc】
通過(guò)調(diào)用self.performTarget("Broker", action: *****) ,會(huì)執(zhí)行下面方法
3.4 最終得到了ViewController,回到即將跳轉(zhuǎn)到
這樣就完成了CTMediator在Swift中的使用反砌。
作者:國(guó)孩
鏈接:https://juejin.im/post/68799807