目录
- 先说说模块化
- 如何将中间层与业务层剥离
- 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
最后
本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果愿意补充以及不吝赐教小弟会更加感激。