組件化的意義
當業(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:¶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
}
CTMediator中的performTarget:action:params:shouldCacheTarget 這個萬能方法核心步驟其實就只有3步:
- 找到target,生成target對象
- 找到target對象的方法
- 調(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)的處理上罩息,主要有兩步:
- 按照 scheme://[target]/[action]?[params] 的方式解析url,獲得target, action, params
- 轉(zhuǎn)換成本地調(diào)用流程挎狸,也就是上面例子的那種方式
相當于CTMediator對于url這部分的處理扣汪,最終還是本地調(diào)用的方式断楷,只不過封裝了一層對于url解析的過程锨匆。
設計思想
根據(jù)上面這兩種情況,我們可以畫出整個CTMediator的架構圖:
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枕面。