内容简介:目录先说说模块化网上有很多谈模块化的文章、这里有一篇
目录
-
先说说模块化
-
如何将中间层与业务层剥离
-
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 }
2.利用协议的方式调用未知对象方法(这也是我使用的方式)
首先你需要一个协议:
@protocol KTComponentManagerProtocol + (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;
2.具体方法的调用、协议要多一层调用
由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]; }
最后
本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果愿意补充以及不吝赐教小弟会更加感激。
作者:kirito_song
链接:https://www.jianshu.com/p/d5630a3c8516
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 前端模块化架构设计与实现(二|模块接口设计)
- Android 金融类项目模块化架构
- 前端模块化、分布式架构设计与实现(一)
- iOS架构:Proxy实现局部模块化(附Demo)
- 蚂蚁金服 mPaaS 模块化开发与架构重构深度解析
- 有赞移动 iOS 组件化(模块化)架构设计实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
汇编语言(第2版)
王爽 / 清华大学出版社 / 2008-4 / 33.00元
《汇编语言(第2版)》是各种CPU提供的机器指令的助记符的集合,人们可以用汇编语言直接控制硬件系统进行工作。汇编语言是很多相关课程(如数据结构、操作系统、微机原理等)的重要基础。为了更好地引导、帮助读者学习汇编语言,作者以循序渐进的思想精心创作了《汇编语言(第2版)》。《汇编语言(第2版)》具有如下特点:采用了全新的结构对课程的内容进行组织,对知识进行最小化分割,为读者构造了循序渐进的学习线索;在......一起来看看 《汇编语言(第2版)》 这本书的介绍吧!