目錄
- 先說說模塊化
- 如何將中間層與業務層剝離
- performSelector與協議的異同
- 調用方式
- 中間件的路由策略
- 模塊入口
- 低版本兼容
- 重定向路由
- 項目的結構
- 模塊化的程度
- 哪些模塊適合下沉
- 關於協作開發
- 效果演示
先說說模塊化
網上有很多談模塊化的文章、這裏有一篇《IOS-組件化架構漫談》有興趣可以讀讀。
總之有三個階段
MVC模式下、我們的總工程長這樣:
加一箇中間層、負責調用指定文件
將中間層與模塊進行解耦
如何將中間層與業務層剝離
-
剛纔第二張圖裏的基本原理:
將原本在業務文件(KTHomeViewController
)代碼裏的耦合代碼
KTAModuleUserViewController * vc = [[KTAModuleUserViewController alloc]initWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];
轉移到中間層(KTComponentManager
)中
//KTHomeViewController.h
UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];
//KTComponentManager.h
return [[KTAModuleUserViewController alloc]initWithUserName:userName age:age];
看似業務之間相互解耦、但是中間層將要引用所有的業務模塊。
直接把耦合的對象轉移了而已。
-
解耦的方式
想要解耦、前提就是不引用頭文件。
那麼、通過字符串代替頭文件的引用就是了。
簡單來講有兩種方式:
1. - (id)performSelector:(SEL)aSelector withObject:(id)object;
具體使用上
Class targetClass = NSClassFromString(@"targetName");
SEL action = NSSelectorFromString(@"ActionName");
return [target performSelector:action withObject:params];
但這樣有一個問題、就是返回值如果不爲id類型、有機率造成崩潰。
不過這可以通過NSInvocation
進行彌補。
這段代碼摘自《iOS從零到一搭建組件化項目架構》
- (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
}
-
利用協議的方式調用未知對象方法(這也是我使用的方式)
首先你需要一個協議:
@protocol KTComponentManagerProtocol <NSObject>
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;
@end
然後調用:
if ([targetClass respondsToSelector:@selector(handleAction:params:)]) {
//向已經註冊的對象發送Action信息
returnObj = [targetClass handleAction:actionName params:params];
}else {
//未註冊的、進行進一步處理。比如上報啊、返回一個佔位對象啊等等
NSLog(@"未註冊的方法");
}
如果有返回基本類型可以在具體入口文件裏處理:
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params {
id returnValue = nil;
if ([action isEqualToString:@"isLogin"]) {
returnValue = @([[KTLoginManager sharedInstance] isLogin]);
}
if ([action isEqualToString:@"loginIfNeed"]) {
returnValue = @([[KTLoginManager sharedInstance] loginIfNeed]);
}
if ([action isEqualToString:@"loginOut"]) {
[[KTLoginManager sharedInstance] loginOut];
}
return returnValue;
}
performSelector與協議的異同
以上兩種方式的中心思想基本相同、也有許多共同點:
- 需要用字典方式傳遞參數
- 需要處理返回值爲非id的情況
只不過一個交給路由、一個交給具體模塊。
協議相比performSelector
當然也有不同:
- 突破了
performSelector
最多隻能傳遞一個參數的限制、並且你可以定製自己想要的格式
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;
- 具體方法的調用、協議要多一層調用
由handleAction
方法根據具體的action
代替performSelector
進行動作的分發。
不過我還是覺得第二種方便、因爲你的performSelector
與實際調用的方法、也解耦了。
比如有一天你換了方法:
performSelector
的方式還需要修改整個url、以保證調用到正確的Selector
。
而協議則不然、你可以在handleAction
方法的內部進行二次路由。
調用方式
-
中間件調用模塊
這裏我做了兩種方案、一種純Url一種帶參
UIViewController *vc = [self openUrl:[NSString stringWithFormat:@"https://www.bilibili.com/KTModuleHandlerForA/getUserViewController?userName=%@&age=%d",userName,age]];
NSNumber *value = [self openUrl:@"ModuleHandlerForLogin/loginIfNeed" params:@{@"delegate":delegate}];
這兩種方式都會用到、區別隨後再說。
-
模塊間調用
用上面的方式直接調用也可以、但是容易寫錯。
通過爲中間件加入Category的方式、對接口進行約束。
並且將url以及參數的拼裝工作交給對應模塊的開發人員。
@interface KTComponentManager (ModuleA)
- (UIViewController *)ModuleA_getUserViewControllerWithUserName:(NSString *)userName age:(int)age;
@end
然後直接代用中間件的Category接口
UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];
中間件的路由策略
-
遠程路由 && 降級路由
- (id)openUrl:(NSString *)url{
id returnObj;
NSURL * openUrl = [NSURL URLWithString:url];
NSString * path = [openUrl.path substringWithRange:NSMakeRange(1, openUrl.path.length - 1)];
NSRange range = [path rangeOfString:@"/"];
NSString *targetName = [path substringWithRange:NSMakeRange(0, range.location)];
NSString *actionName = [path substringWithRange:NSMakeRange(range.location + 1, path.length - range.location - 1)];
//可以對url進行路由。比如從服務器下發json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
if (self.redirectionjson[path]) {
path = self.redirectionjson[path];
}
//如果該target的action已經註冊
if ([self.registeredDic[targetName] containsObject:actionName]) {
returnObj = [self openUrl:path params:[self getURLParameters:openUrl.absoluteString]];
}else if ([self.webUrlSet containsObject:[NSString stringWithFormat:@"%@%@",openUrl.host,openUrl.path]]){
//低版本兼容
//如果有某些H5頁面、打開H5頁面
//webUrlSet可以由服務器下發
NSLog(@"跳轉網頁:%@",url);
}
return returnObj;
}
遠程路由需要考慮由於本地版本過低導致需要跳轉H5的情況。
如果本地支持、則直接使用本地路由。
-
本地路由
- (id)openUrl:(NSString *)url params:(NSDictionary *)params {
id returnObj;
if (url.length == 0) {
return nil;
}
//可以對url進行路由。比如從服務器下發json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
if (self.redirectionjson[url]) {
url = self.redirectionjson[url];
}
NSRange range = [url rangeOfString:@"/"];
NSString *targetName = [url substringWithRange:NSMakeRange(0, range.location)];
NSString *actionName = [url substringWithRange:NSMakeRange(range.location + 1, url.length - range.location - 1)];
Class targetClass = NSClassFromString(targetName);
if ([targetClass respondsToSelector:@selector(handleAction:params:)]) {
//向已經實現了協議的對象發送Target&&Action信息
returnObj = [targetClass handleAction:actionName params:params];
}else {
//未註冊的、進行進一步處理。比如上報啊、返回一個佔位對象啊等等
NSLog(@"未註冊的方法");
}
return returnObj;
}
通過調用模塊入口模塊targetClass
遵循的中間件協議方法handleAction:params:
將動作action
以及參數params
傳遞。
模塊入口
模塊入口實現了中間件的協議方法
handleAction:params:
根據不同的Action
、內部自己負責邏輯處理。
#import "ModuleHandlerForLogin.h"
#import "KTLoginManager.h"
#import "KTComponentManager+LoginModule.h"
@implementation ModuleHandlerForLogin
/**
相當於每個模塊維護自己的註冊表
*/
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params {
id returnValue = nil;
if ([action isEqualToString:@"getUserViewController"]) {
returnValue = [[KTAModuleUserViewController alloc]initWithUserName:params[@"userName"] age:[params[@"age"] intValue]];
}
return returnValue;
}
低版本兼容
有時低版本的App也可能被遠程進行路由、但卻並沒有原生頁面。
這時、如果有H5頁面、則需要跳轉H5
//如果該target的action已經註冊
if ([self.registeredDic[targetName] containsObject:actionName]) {
returnObj = [self openUrl:path params:[self getURLParameters:openUrl.absoluteString]];
}else if ([self.webUrlSet containsObject:[NSString stringWithFormat:@"%@%@",openUrl.host,openUrl.path]]){
//低版本兼容
//如果有某些H5頁面、打開H5頁面
//webUrlSet可以由服務器下發
NSLog(@"跳轉網頁:%@",url);
}
registeredDic
負責維護註冊表、記錄了本地模塊實現了那些Target && Action。
這個註冊動作、交給每個模塊的入口進行:
/**
在load中向模塊管理器註冊
這裏其實如果引入KTComponentManager會方便很多
但是會依賴管理中心、所以算了
*/
+ (void)load {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
Class KTComponentManagerClass = NSClassFromString(@"KTComponentManager");
SEL sharedInstance = NSSelectorFromString(@"sharedInstance");
id KTComponentManager = [KTComponentManagerClass performSelector:sharedInstance];
SEL addHandleTargetWithInfo = NSSelectorFromString(@"addHandleTargetWithInfo:");
NSMutableSet * actionSet = [[NSMutableSet alloc]initWithArray:@[@"getUserViewController"]];
NSDictionary * targetInfo = @{
@"targetName":@"KTModuleHandlerForA",
@"actionSet":actionSet
};
[KTComponentManager performSelector:addHandleTargetWithInfo withObject:targetInfo];
#pragma clang diagnostic pop
}
重定向路由
由於某些原因、有時我們需要修改某些Url路由的指向(比如順風車?)
//可以對url進行路由。比如從服務器下發json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
if (self.redirectionjson[path]) {
path = self.redirectionjson[path];
}
這個redirectionjson
由服務器下發、本地路由時如果發現有需要被重定向的Path則進行重定向動作、修改路由的目的地。
項目的結構
模塊全部以私有Pods的形式引入、單個模塊內部遵循MVC(隨便你用什麼MVP啊、MVVM啊。只要別引入其他模塊的東西)。
我只是寫一個demo、所以嫌麻煩沒有搞Pods。意會吧。
模塊化的程度
每個模塊、引入了公共模塊之後。
可以在自己的Target工程獨立運行。
哪些模塊適合下沉
可以跨產品使用的模塊
日誌、網絡層、三方SDK、持久化、分享、工具擴展等等。
關於協作開發
pods一定要保證版本的清晰、比如Category哪怕只更新了一個入口、也要當做一個新的版本。
於是開發的階段由於要經常更新代碼、最好還是不要用pods。
大家可以寫好Category在自己模塊的Target先工作。
最後調試上線的時候再統一上傳pods並且打包。
效果演示
寫了三個按鈕
- (IBAction)pushToModuleAUserVC:(UIButton *)sender {
if (![[KTComponentManager sharedInstance] loginIfNeedWithDelegate:self]) {
return;
}
UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];
}
- (IBAction)LoginBtnClick:(UIButton *)sender {
if ([[KTComponentManager sharedInstance] loginIfNeedWithDelegate:self]) {
[[KTComponentManager sharedInstance] loginOutWithDelegate:self];
}
}
- (IBAction)openWebUrl:(id)sender {
[[KTComponentManager sharedInstance] openUrl:[NSString stringWithFormat:@"https://www.bilibili.com/video/av25305807"]];
}
//這裏應該用通知獲取的
- (void)didLoginIn {
[self.loginBtn setTitle:@"退出登錄" forState:UIControlStateNormal];
}
- (void)didLoginOut {
[self.loginBtn setTitle:@"登錄" forState:UIControlStateNormal];
}
Demo
最後
本文主要是自己的學習與總結。如果文內存在紕漏、萬望留言斧正。如果願意補充以及不吝賜教小弟會更加感激。