前言
组件化(也称模块化),就是将模块进行抽离、分层,并制定模块间的通讯方式,以此实现解耦、模块可重用,多在多人团队开发中使用
组件化标准介绍
项目较小,模块之间交互简单,耦合很少;模块没有被外部模块引用,只是一个单独的小模块;模块不需要重用,代码也很少被修改;团队规模很小,那么,你对项目就没有必要做组件化,否则只会大幅度增加团队任务
因此并不是所有的项目都适合组件化
组件化使用时机
如果你的项目有以下三个特征以上,就要考虑下进行组件化了:
1、模块逻辑复杂,多个模块之间频繁互相引用;
2、项目规模逐渐变大,修改代码变的越来越困难(这里可以理解为:修改一处代码,需要同时修改其他多个地方);
3、团队人数变多,提交的代码经常和其他成员冲突;
4、项目编译耗时较长;
5、模块的单元测试经常由于其他模块的修改而失败。
衡量组件化标准
如何来评判项目组件化后是否彻底,或者是否优秀,可以通过以下8条指标来看:
1、模块之间没有耦合,模块内部的修改不影响其他模块;
2、模块可以单独编译;
3、模块间数据传递明确;
4、模块可以随时被另一个提供了相同功能的模块替换;
5、模块对外接口清晰且易维护;
6、当模块接口改变时,此模块的外部代码能够被高效重构;
7、尽量用最少的修改和代码,让现有的项目实现模块化;
8、支持 OC 和 Swift,以及混编。
前4条主要用于衡量一个模块是否真正解耦,后4条主要用于衡量在项目实践中的易用程度。
组件化原则与分层
一个项目组件化主要分为三层:业务层,通用层,基础层,具体如下图所示:
通过上图也可以看出,模块之间是
分层的,并且业务层当中可能两个模块之间难免会有交流(例如上面的个人和购物模块),如果两个模块直接互相调用,那么组件之间的耦合将会很严重,而解决方案就是后面的组件化通讯方案了
在进行组件化时,有以下几点需要说明:
1、只能上层对下层依赖,不能下层对上层的依赖,因为下层是对上层的抽象
2、项目公共代码资源下沉
3、横向的依赖尽量少有,最好下沉至通用模块或者基础模块
注意:模块向下调用的过程是可以跨级的,而不是说上层只能往下调用一层
组件化方案
组件化方案通常来说有两种:本地组件化、cocoapods组件化
本地组件化
本地组件化,顾名思义,就是在本地项目中布置组件化内容,一般通过framework、library的方式布置组件化
1、如下所示创建 framework,也可以选择后面的 library
2、创建完成之后,添加到项目的 workspace 中去,如下所示
3、如果是 framework,默认可以获取资源文件,如果设置了static library(无资源文件这么设置),即:在 target -> build settings 中,可以搜索 mach,然后将 mach-o类型设置成静态(static library),但其将会出现获取不到framework中的图片和bundle文件资源问题,因此需要额外注意
ps:这里对比 MJRefresh 发现的,之前被别人误导了,才导致资源加载不了的问题
4、然后在 target -> build plases -> link binary with libraries 中 添加 framework或者 library,这样就可以关联起来,在主项目运行的时候,自动编辑 library 相关组件
5、为了外部能够搜索到内部暴露头文件,需要在组件的target -> build phases -> Headers中将需要暴露的头文件拖拽到public中,否则外部会找不到该头文件
6、最后测试一下,成功,如下图所示
注意:实际使用的时候不要这么直接使用,最好直接加入一些中间类,来管理主项目和组件之间的通信,后面也会介绍
另外注意,需要在 bulid settings -> other link 中设置 -all_load参数,否则组件中的类加载会有问题,例如有时获取时获取不到类或者资源,就是这个原因,设置如下所示,我这里目前是没有碰到
简单介绍部分 library 和 framework的区别
1、library的产出物只是一个.a的二进制执行文件,分享的时候,头文件、静态资源文件需要另外提供。
2、framework其中包含代码签名、头文件、二进制执行文件、静态资源文件等(细心点的小伙伴,相信创建的时候就能发现 framework的方式比直接创建 library 多了些文件以及配置信息)
因此,正常开发中,如果选用了本地组件化,推荐采用 framework 的形式,如果是 算法之类,可以考虑采用 library
注意:如果framework给别人用,记得合并模拟器和真机下编译的framework包(分别采用模拟器和真机运行后,在组件的Products目录中,生成对应的framework包,右键 show in finder获取),然后使用下面命令合并:
lipo -create [模拟器framework目录] [iphone的framework目录] -output [导出目录]
6、组件的cocoapods依赖管理
如下所示,workspace为工作项目目录位置(由于组件化多个模块,不设置会有警告,会存在潜在问题)
其中 Component1 就是其中一个组件模块,将其 target 进行如下对应设置,其中 project 则为组件的 项目位置
workspace 'CompanentTestDemo.xcworkspace'
target 'Component1' do
project 'Component1/Component1.xcodeproj'
pod 'YYCache'
end
target 'CompanentTestDemo' do
pod 'AFNetworking'
end
组件的项目位置,根据目录层级位置,寻找即可
具体为什么这么配置,而不是全写到一起,想想为什么组件化(假如这个组件功能废弃了,还要在校对三方中删除么)
cocoapods组件化(推荐)
关于coocapods组件化前面的文件有详细讲过 -- 搭建cocoapods仓库,这里就不多介绍了,可以点击查看,搭建完毕后,可以像cocoapods一样管理,并且里面有加载图片资源解决方案
图片加载逻辑
组件化图片资源加载逻辑,如下所示
//获取组件生成的framework中所在的bundle中(和 mainbundle类似)
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
//直接从该framework组件中,以缓存方式加载图片
UIImage *image = [UIImage imageNamed:@"icon_star" inBundle:bundle withConfiguration:nil];
//直接从该framework组件中,非缓存方式加载图片
UIImage *image2 = [UIImage imageWithContentsOfFile:[bundle pathForResource:@"icon_star1"
ofType:@"png"]];
//调用组件内创建的.bundle类型的内部图片资源,需要进一步获取 .bundle 的bundle
//此时的 bundle 和 前面获取到的 bundle 类似,里面也可以放置必要的资源文件
NSBundle *assetBundle = [NSBundle bundleWithPath:[bundle pathForResource:@"Component1"
ofType:@"bundle"]];
//从组件创建的.bundle中调用图片
UIImage *image3 = [UIImage imageNamed:@"icon_star2"
inBundle:assetBundle withConfiguration:nil];
组件化通讯方案
在同一层模块中,有时难免会出现横向交互的过程,例如下图,聊天和个人,个人和购物之间可能会难免有所交互,但是如果在其业务层直接调用别的模块功能,难免会引入相应的头文件,从而导致两个模块硬耦合
因此以新增路由等方式来解决这硬耦合的问题,从而引出了组件化通讯方案
常见的组件化方案一般分为三种:URL(字符串)、target-action(分类)、protocol(协议)
URL(字符串): 以MGJRouter为代表,但也有着明显的优缺点,其使用简单快捷,采用指定的字符串进行匹配,相对灵活,但由于字符串的多变,依赖于命名,因此需要维护字符串表,且无法在编译期检查,维护难度相对较大
target-action(分类): 以CTMediator为代表,以CTMediator为中间者,以引出分类的方式来协调双方进行交互,有缺点也很明显,需要分出单独的 CTMediator分类库进行维护,以便于协调双方交互,且使用起来分模块维护,功能相对比较集中,审查起来相对方便
protocol(协议):以 BeeHive为代表,还有(Swinject),且 Beehive 一直在完善,在使用过程中,在编译层面通过协议定制规范,从而达到维护组件的目的,但缺点也很明显,使用非常规范,但灵活性不高
MGJRouter
该方案通过 MGJRouter单例作为中间路由,里面以字典的方式保存了模块注册时的协议名、方法、block等信息,通过使用 URL 传递调用函数,使用简单,如下所示
组件内注册对外接口,向外暴露服务名(协议名)为 MGJComponent, 方法为 pushViewController
一般会新起一个类,在load里面注册,避免了硬编码的问题
+ (void)load {
[MGJRouter registerURLPattern:@"MGJComponent://pushViewController"
toHandler:^(NSDictionary *routerParameters) {
NSLog(@"%@", routerParameters);
NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
UIViewController *vc = userInfo[@"viewController"];
void (^block)(id) = userInfo[@"callback"];
if (!vc) return;
LSPushViewController *pv = [LSPushViewController new];
[pv setBackBlock:^{
if (block) block(@"我是传回的数据");
}];
[vc presentViewController:pv animated:YES completion:nil];
}];
[MGJRouter registerURLPattern:@"MGJComponent://testNil" toHandler:nil];
}
调用服务(协议),服务名为 MGJComponent, 方法为 pushViewController
[MGJRouter openURL:@"MGJComponent://pushViewController" withUserInfo:@{
@"title": @"测试用的title",
@"viewController": self,
@"callback": ^(id result) {
NSLog(@"callbackInfo:%@", result);
}
} completion:nil];
调用结果
其注册核心源码实现如下:
- (void)addURLPattern:(NSString *)URLPattern andHandler:(MGJRouterHandler)handler
{
NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
if (handler && subRoutes) {
//返回的第二层 value字典中,保存key为-,value为回调block的字典,以便于 openURL 后回调
subRoutes[@"_"] = [handler copy];
}
}
//创建和取出注册的字典内容
- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern
{
NSArray *pathComponents = [self pathComponentsFromURL:URLPattern];
//获取全局的字典,第一层的key-value表示的是 服务名 和 服务方法
//第二层的key-value 表示的是 服务方法和一个字典
NSMutableDictionary* subRoutes = self.routes;
for (NSString* pathComponent in pathComponents) {
if (![subRoutes objectForKey:pathComponent]) {
subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
}
subRoutes = subRoutes[pathComponent];
}
//返回第二层的value字典
return subRoutes;
}
从url中获取服务和方法,放入到数组中返回
- (NSArray*)pathComponentsFromURL:(NSString*)URL
{
NSMutableArray *pathComponents = [NSMutableArray array];
if ([URL rangeOfString:@"://"].location != NSNotFound) {
NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
// 如果 URL 包含协议,那么把协议作为第一个元素放进去
[pathComponents addObject:pathSegments[0]];
// 如果只有协议,那么放一个占位符~
URL = pathSegments.lastObject;
if (!URL.length) {
[pathComponents addObject:MGJ_ROUTER_WILDCARD_CHARACTER];
}
}
拿出调用的方法
for (NSString *pathComponent in [[NSURL URLWithString:URL] pathComponents]) {
if ([pathComponent isEqualToString:@"/"]) continue;
if ([[pathComponent substringToIndex:1] isEqualToString:@"?"]) break;
[pathComponents addObject:pathComponent];
}
return [pathComponents copy];
}
调用服务的实现如下所示:
//将 url 和 userInfo 信息包装起来,剔除掉用于回调的 block,返回给调用者
+ (void)openURL:(NSString *)URL
withUserInfo:(NSDictionary *)userInfo
completion:(void (^)(id result))completion
{
URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary *parameters = [[self sharedInstance]
extractParametersFromURL:URL matchExactly:NO];
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
if ([obj isKindOfClass:[NSString class]]) {
parameters[key] = [obj
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
}];
if (parameters) {
MGJRouterHandler handler = parameters[@"block"];
if (completion) {
parameters[MGJRouterParameterCompletion] = completion;
}
if (userInfo) {
parameters[MGJRouterParameterUserInfo] = userInfo;
}
if (handler) {
[parameters removeObjectForKey:@"block"];
handler(parameters);
}
}
}
另外可以自行改动一下里面逻辑,可以使其更加快捷简单,逻辑简单,使用方便灵活,也是个人偏爱的一种
CTMediator
CTMediator 是通过 runtime 以 NSClassFromString 方法来获取对应类,通过 NSInvocation直接调用指定函数
使用方法大致分为三步:
1、在独立的组件中生成一个对外暴露的服务类,用来处理对应的功能,且命名方式为 target_ + 类名,而对外开放的函数名则全为 Action_开头,即 Action_ + 函数名
2、在主工程或者另起一个repo中创建用于交互的CTMediator的分类,分类命名方式最好以模块划分多个子类,,用于主工程间接调用独立组件暴露出来的服务(功能)
3、分类中调用的时候,需要传递 类名target 和 函数名action,其中target为第一步取得 target_后面的类名,action 则为 Action_后面的函数名,可以提取出来,以便于后续统一管理
源码分析
其通过 NSClassFromString 等方式获取拼接好的类和sel,然后直接通过 NSInvocation 调用
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
if (targetName == nil || actionName == nil) {
return nil;
}
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
//拼接类
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
//获取缓存
NSObject *target = [self safeFetchCachedTarget:targetClassString];
if (target == nil) {
//通过字符串获取指定类
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
//有了类和函数,开始调用
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
//缓存
if (shouldCacheTarget) {
[self safeSetCachedTarget:target key:targetClassString];
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
@synchronized (self) {
[self.cachedTarget removeObjectForKey:targetClassString];
}
return nil;
}
}
}
//当没有找到相关类的时候的处理
- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
SEL action = NSSelectorFromString(@"Action_response:");
NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"originParams"] = originParams;
params[@"targetString"] = targetString;
params[@"selectorString"] = selectorString;
[self safePerformAction:action target:target params:params];
}
//找到了指定类之后,开始通过 NSInvocation 调用函数
- (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];
//根据类型,调用 invocation
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
}
BeeHive
BeHive这里主要讲解他是怎么使用的,详细可以参考这篇文章 -- BeeHive
1、创建服务协议
如下所示,这里给所有协议新起了一个 repo
如下所示,给指定的组件添加了交互服务文件,暴露的服务类,需要实现相关协议,另外,这里每一个文件夹可以相当于一个 repo
2、注册协议服务
(1)、使用 注解Annotation 的方式注册,只需要在服务类中使用下面宏即可
@BeeHiveService(BHHomeDelegate, BHHomeModule) //分别为 协议名 和 服务类名
(2)、使用 load,在暴露的服务类中,直接调用注册方法即可,如下所示
+ (void)load {
[[BeeHive shareInstance] registerService:@protocol(BHHomeDelegate) service:[self class]];
}
(3)、使用 plist的方式注册,需要设置 bundle的位置,还有 plist的配置
//可选,默认为BeeHive.bundle/BeeHive.plist
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";
//服务plist的配置
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
//开始注册
[[BeeHive shareInstance] setContext:[BHContext shareInstance]];
其中 module 的配置如下所示
服务的配置如下所示,service对应协议, impl 对应类名
3、调用服务
如下所示,直接使用 createService方法注册对应协议,可以获得注册的变量
最后,运行一下,均运行成功了,可以尝试一下哈
注意: 如果需要保存状态,则可以实现 BHServiceProtocol 的 shareInstance 方法,则下次创建则会保留
最后
组件化以及通信方案,已经在demo当中通过了
注意:BeeHive在没注册方法的时候(找不到方法的时候),会调用到CTMediator 的组件demo的一个方法,自己测试时需注意
-- 案例demo
这些组件化的方案,你更喜欢哪一个呢,快选一个尝试一下吧
持续学习可以稳定住我们的状态,大家一起加油吧