
目錄
- 先說(shuō)說(shuō)模塊化
- 如何將中間層與業(yè)務(wù)層剝離
- performSelector與協(xié)議的異同
- 調(diào)用方式
- 中間件的路由策略
- 模塊入口
- 低版本兼容
- 重定向路由
- 項(xiàng)目的結(jié)構(gòu)
- 模塊化的程度
- 哪些模塊適合下沉
- 關(guān)于協(xié)作開(kāi)發(fā)
- 效果演示
先說(shuō)說(shuō)模塊化
網(wǎng)上有很多談模塊化的文章、這里有一篇《IOS-組件化架構(gòu)漫談》有興趣可以讀讀。
總之有三個(gè)階段
MVC模式下、我們的總工程長(zhǎng)這樣:

加一個(gè)中間層、負(fù)責(zé)調(diào)用指定文件

將中間層與模塊進(jìn)行解耦

如何將中間層與業(yè)務(wù)層剝離
-
剛才第二張圖里的基本原理:
將原本在業(yè)務(wù)文件(KTHomeViewController)代碼里的耦合代碼
KTAModuleUserViewController * vc = [[KTAModuleUserViewController alloc]initWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];
轉(zhuǎn)移到中間層(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];
看似業(yè)務(wù)之間相互解耦、但是中間層將要引用所有的業(yè)務(wù)模塊。
直接把耦合的對(duì)象轉(zhuǎn)移了而已。
-
解耦的方式
想要解耦、前提就是不引用頭文件。
那么、通過(guò)字符串代替頭文件的引用就是了。
簡(jiǎn)單來(lái)講有兩種方式:
1. - (id)performSelector:(SEL)aSelector withObject:(id)object;
具體使用上
Class targetClass = NSClassFromString(@"targetName");
SEL action = NSSelectorFromString(@"ActionName");
return [target performSelector:action withObject:params];
但這樣有一個(gè)問(wèn)題、就是返回值如果不為id類型、有幾率造成崩潰。
不過(guò)這可以通過(guò)NSInvocation進(jìn)行彌補(bǔ)。
這段代碼摘自《iOS從零到一搭建組件化項(xiàng)目架構(gòu)》
- (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
}
-
利用協(xié)議的方式調(diào)用未知對(duì)象方法(這也是我使用的方式)
首先你需要一個(gè)協(xié)議:
@protocol KTComponentManagerProtocol <NSObject>
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;
@end
然后調(diào)用:
if ([targetClass respondsToSelector:@selector(handleAction:params:)]) {
//向已經(jīng)注冊(cè)的對(duì)象發(fā)送Action信息
returnObj = [targetClass handleAction:actionName params:params];
}else {
//未注冊(cè)的、進(jìn)行進(jìn)一步處理。比如上報(bào)啊、返回一個(gè)占位對(duì)象啊等等
NSLog(@"未注冊(cè)的方法");
}
如果有返回基本類型可以在具體入口文件里處理:
+ (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與協(xié)議的異同
以上兩種方式的中心思想基本相同、也有許多共同點(diǎn):
- 需要用字典方式傳遞參數(shù)
- 需要處理返回值為非id的情況
只不過(guò)一個(gè)交給路由、一個(gè)交給具體模塊。
協(xié)議相比performSelector當(dāng)然也有不同:
- 突破了
performSelector最多只能傳遞一個(gè)參數(shù)的限制、并且你可以定制自己想要的格式
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;
- 具體方法的調(diào)用、協(xié)議要多一層調(diào)用
由handleAction方法根據(jù)具體的action代替performSelector進(jìn)行動(dòng)作的分發(fā)。
不過(guò)我還是覺(jué)得第二種方便、因?yàn)槟愕?code>performSelector與實(shí)際調(diào)用的方法、也解耦了。
比如有一天你換了方法:
performSelector的方式還需要修改整個(gè)url、以保證調(diào)用到正確的Selector。
而協(xié)議則不然、你可以在handleAction方法的內(nèi)部進(jìn)行二次路由。
調(diào)用方式
-
中間件調(diào)用模塊
這里我做了兩種方案、一種純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}];
這兩種方式都會(huì)用到、區(qū)別隨后再說(shuō)。
-
模塊間調(diào)用
用上面的方式直接調(diào)用也可以、但是容易寫(xiě)錯(cuò)。
通過(guò)為中間件加入Category的方式、對(duì)接口進(jìn)行約束。
并且將url以及參數(shù)的拼裝工作交給對(duì)應(yīng)模塊的開(kāi)發(fā)人員。
@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];
中間件的路由策略
-
遠(yuǎn)程路由 && 降級(jí)路由
- (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)];
//可以對(duì)url進(jìn)行路由。比如從服務(wù)器下發(fā)json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
if (self.redirectionjson[path]) {
path = self.redirectionjson[path];
}
//如果該target的action已經(jīng)注冊(cè)
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頁(yè)面、打開(kāi)H5頁(yè)面
//webUrlSet可以由服務(wù)器下發(fā)
NSLog(@"跳轉(zhuǎn)網(wǎng)頁(yè):%@",url);
}
return returnObj;
}
遠(yuǎn)程路由需要考慮由于本地版本過(guò)低導(dǎo)致需要跳轉(zhuǎn)H5的情況。
如果本地支持、則直接使用本地路由。
-
本地路由
- (id)openUrl:(NSString *)url params:(NSDictionary *)params {
id returnObj;
if (url.length == 0) {
return nil;
}
//可以對(duì)url進(jìn)行路由。比如從服務(wù)器下發(fā)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:)]) {
//向已經(jīng)實(shí)現(xiàn)了協(xié)議的對(duì)象發(fā)送Target&&Action信息
returnObj = [targetClass handleAction:actionName params:params];
}else {
//未注冊(cè)的、進(jìn)行進(jìn)一步處理。比如上報(bào)啊、返回一個(gè)占位對(duì)象啊等等
NSLog(@"未注冊(cè)的方法");
}
return returnObj;
}
通過(guò)調(diào)用模塊入口模塊targetClass遵循的中間件協(xié)議方法handleAction:params:將動(dòng)作action以及參數(shù)params傳遞。
模塊入口
模塊入口實(shí)現(xiàn)了中間件的協(xié)議方法
handleAction:params:
根據(jù)不同的Action、內(nèi)部自己負(fù)責(zé)邏輯處理。
#import "ModuleHandlerForLogin.h"
#import "KTLoginManager.h"
#import "KTComponentManager+LoginModule.h"
@implementation ModuleHandlerForLogin
/**
相當(dāng)于每個(gè)模塊維護(hù)自己的注冊(cè)表
*/
+ (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;
}
低版本兼容
有時(shí)低版本的App也可能被遠(yuǎn)程進(jìn)行路由、但卻并沒(méi)有原生頁(yè)面。
這時(shí)、如果有H5頁(yè)面、則需要跳轉(zhuǎn)H5
//如果該target的action已經(jīng)注冊(cè)
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頁(yè)面、打開(kāi)H5頁(yè)面
//webUrlSet可以由服務(wù)器下發(fā)
NSLog(@"跳轉(zhuǎn)網(wǎng)頁(yè):%@",url);
}
registeredDic負(fù)責(zé)維護(hù)注冊(cè)表、記錄了本地模塊實(shí)現(xiàn)了那些Target && Action。
這個(gè)注冊(cè)動(dòng)作、交給每個(gè)模塊的入口進(jìn)行:
/**
在load中向模塊管理器注冊(cè)
這里其實(shí)如果引入KTComponentManager會(huì)方便很多
但是會(huì)依賴管理中心、所以算了
*/
+ (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
}
重定向路由
由于某些原因、有時(shí)我們需要修改某些Url路由的指向(比如順風(fēng)車?)
//可以對(duì)url進(jìn)行路由。比如從服務(wù)器下發(fā)json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
if (self.redirectionjson[path]) {
path = self.redirectionjson[path];
}
這個(gè)redirectionjson由服務(wù)器下發(fā)、本地路由時(shí)如果發(fā)現(xiàn)有需要被重定向的Path則進(jìn)行重定向動(dòng)作、修改路由的目的地。
項(xiàng)目的結(jié)構(gòu)
模塊全部以私有Pods的形式引入、單個(gè)模塊內(nèi)部遵循MVC(隨便你用什么MVP啊、MVVM啊。只要?jiǎng)e引入其他模塊的東西)。

我只是寫(xiě)一個(gè)demo、所以嫌麻煩沒(méi)有搞Pods。意會(huì)吧。
模塊化的程度
每個(gè)模塊、引入了公共模塊之后。
可以在自己的Target工程獨(dú)立運(yùn)行。
哪些模塊適合下沉
可以跨產(chǎn)品使用的模塊
日志、網(wǎng)絡(luò)層、三方SDK、持久化、分享、工具擴(kuò)展等等。
關(guān)于協(xié)作開(kāi)發(fā)
pods一定要保證版本的清晰、比如Category哪怕只更新了一個(gè)入口、也要當(dāng)做一個(gè)新的版本。
于是開(kāi)發(fā)的階段由于要經(jīng)常更新代碼、最好還是不要用pods。
大家可以寫(xiě)好Category在自己模塊的Target先工作。
最后調(diào)試上線的時(shí)候再統(tǒng)一上傳pods并且打包。
效果演示

寫(xiě)了三個(gè)按鈕
- (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"]];
}
//這里應(yīng)該用通知獲取的
- (void)didLoginIn {
[self.loginBtn setTitle:@"退出登錄" forState:UIControlStateNormal];
}
- (void)didLoginOut {
[self.loginBtn setTitle:@"登錄" forState:UIControlStateNormal];
}
Demo
最后
本文主要是自己的學(xué)習(xí)與總結(jié)。如果文內(nèi)存在紕漏、萬(wàn)望留言斧正。如果愿意補(bǔ)充以及不吝賜教小弟會(huì)更加感激。