iOS--談一談模組化策略

最近讀文件讀的頭疼、寫點舊東西吧。隨便扯扯順便趕個Demo出來。
然後、雖然上個月工資還沒發。明天要開始幹幾天活了~
ofollow,noindex">目錄
- 先說說模組化
- 如何將中間層與業務層剝離
- 呼叫方式
- 中介軟體的路由策略
- 模組入口
- 低版本相容
- 重定向路由
- 專案的結構
- 模組化的程度
- 哪些模組適合下沉
- 效果演示
先說說模組化
網上有很多談模組化的文章、這裡有一篇 《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-Component-Pro" target="_blank" rel="nofollow,noindex">《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; }
以上兩種方式的中心思想基本相同、也有許多共同點:
- 需要用字典方式傳遞引數
- 需要處理返回值為非id的情況
只不過一個交給路由、一個交給具體模組。
呼叫方式
-
中介軟體呼叫模組
這裡我做了兩種方案、一種純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、持久化、分享、工具擴充套件等等。
效果演示

寫了三個按鈕
- (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
最後
本文主要是自己的學習與總結。如果文記憶體在紕漏、萬望留言斧正。如果願意補充以及不吝賜教小弟會更加感激。