逆水行舟,不進則退
這段時間處于項目空檔期,別提有多開心了(如果讓老大看到我這樣估計我會死的很慘),開心并不是因為懶,而是為終于有了可以自由翱翔的時間
最近看了一些組件化方面的文章,感觸良多,今天主要是基于CTMediator
組件化方案進行分享,大家如果覺得我理解的不對可以留言或者直接去看Caca寫的 iOS應用架構談 組件化方案
一:CTMediator源碼
源碼里面代碼不是特別的多,大概就是200多行
先看一下.h文件
#import <UIKit/UIKit.h>
extern NSString * const kCTMediatorParamsKeySwiftTargetModuleName;
@interface CTMediator : NSObject
+ (instancetype)sharedInstance;
// 遠程App調用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地組件調用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;
@end
+ (instancetype)sharedInstance;
:單例,返回CTMediator
對象
performActionWithUrl
:這個方法主要是用于遠程APP調用,比如從A應用傳遞一個URL到B應用,在B應用的openURL
方法中去處理url
performTarget
: 本地組件調用,使用RunTime處理target和action,shouldCacheTarget
是否對傳入的target進行緩存
releaseCachedTargetWithTargetName
:把傳入的target從緩存中刪除
接下來去.m文件中看看具體是怎么實現的
sharedInstance
,這里就不多說了
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
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]];
}
// 這里這么寫主要是出于安全考慮,防止黑客通過遠程方式調用本地模塊。這里的做法足以應對絕大多數場景,如果要求更加嚴苛,也可以做更加復雜的安全邏輯。
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
if ([actionName hasPrefix:@"native"]) {
return @(NO);
}
// 這個demo針對URL的路由處理非常簡單,就只是取對應的target名字和method名字,但這已經足以應對絕大部份需求。如果需要拓展,可以在這個方法調用之前加入完整的路由邏輯
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
if (completion) {
if (result) {
completion(@{@"result":result});
} else {
completion(nil);
}
}
return result;
}
這個方法主要是針對遠程APP的互相調起,通過openURL實現APP之間的跳轉,通過URL進行數據傳遞
一個完整的URL就像上圖一樣,上面的代碼中,優先從URL中獲取到query中的數據,然后進行遍歷然后把對應的參數的key和value添加到字典中,然后從URL中取出actionName,也就是要調用的方法名,最后通過
performTarget
方法去實現方法的調用,根據返回值處理回調
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
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.cachedTarget[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了。實際開發過程中是可以事先給一個固定的target專門用于在這個時候頂上,然后處理這種請求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里是處理無響應請求的地方,如果無響應,則嘗試調用對應target的notFound方法統一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程中,可以用前面提到的固定的target頂上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
根據傳遞的targetName在緩存中查找,沒有找到就通過NSClassFromString獲取這個類,如果tatget==nil進行錯誤處理,如果傳入的shouldCacheTarget
為YES就把target添加到集合中緩存起來,然后判斷target是否可以響應傳進來的方法,不能響應錯誤處理,可以響應就調用safePerformAction
這個方法
- (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
}
這段代碼主要是判斷返回值類型,如果是void
,NSInteger
,BOOL
,CGFloat
,NSUInteger
就進行特殊處理,不是的話就直接返回performSelector
的返回值類型
二:使用CTMediator實戰
CTMediator
筆者用的是cocopods進行的組件化管理,我這邊用的是framework進行的
首頁是一個單獨的模塊,按照原來的開發方式,如果把這個模塊從項目中刪除,肯定就會報錯,因為項目中有幾個地方是對首頁進行引用的,如何才能做到刪除它而對項目不產生影響呢?下面開始介紹我做了哪些操作:
- 抽取出主工程,包括:工具類,三方框架,常用的一些配置等,有了這些作為支撐,才可以開始子模塊的開發和測試
- 創建framework,把首頁功能封裝在framework里面,通過一個中間類
Target_HomeVCAction
來操作首頁功能,包括實例化,和外界參數的傳遞,只有中間類是可以供外部調用的 - 增加一個
CTMediator
的分類,在分類里面去關聯上面提到的中間類,此處的關聯其實也不需要導入文件,而是以字符串的形式傳遞類名和方法名,再通過調用CTMediator
中的performTarget
方法實現函數調用
分類里面的實現
NSString * const kCTMediatorTargetA = @"HomeVCAction";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail:(NSDictionary *)dict
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:dict
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界選擇是push還是present
return viewController;
} else {
// 這里處理異常場景,具體如何處理取決于產品
return [[UIViewController alloc] init];
}
}
在整個過程中只有一處對CTMediator
分類的引用,如果傳遞的參數錯誤或者找不到類,可以在CTMediator
中進行統一處理,如果需要修改代碼可以回到自己的framework中進行修改,修改完成后只需要把framework更新一下就可以了,,項目一天天的變的龐大起來,每次編譯都會耗費很長的時間,對自己也是一種折磨,這樣做可以大大的減少項目的編譯時間了
這種通過Target-Action的組件化方案,我個人覺得挺好的,只是多了一些硬編碼,但是方便各模塊傳值,使用URL路由跳轉的話,傳遞對象就沒那么簡單了
大家有意見歡迎提出,幫助別人成長的同時,也是對自己的一次錘煉