组件化的目的
“组件”指的是较大粒度的业务功能模块;一个APP通常由多个业务模块组成,或者是多个不同子业务线的APP通过模块的形式在主应用中进行集成;模块之间通常会有通信、互相调用;实现组件化的目的,就是为了让组件之前的互相调用可以更松散,消除组件之间由于互相依赖导致的耦合;
组件化的益处是可以使每个组件的开发过程都更加独立,更好的提升单一组件的迭代速度;开发人员可以只关注自己负责的部分,不需要全量编译整个工程,同时也提升了开发效率;
在不使用组件化的应用中,我们看到的不同模块间的相互调用通常如下所示:
#import "ModuleAViewController.h"
#import "ModuleBViewController.h"
#import "ModuleCViewController.h"
@implementation ModuleBViewController
- (void)gotoModuleAVC {
ModuleAViewController *moduleAVC = [[ModuleAViewController alloc] init];
//....参数传递
[self.navigationController pushViewController: moduleAVC animated:YES];
}
- (void) gotoModuleCVC {
ModuleCViewController *cVC = [[ModuleCViewController alloc] init];
//...参数传递
[self.navigationController pushViewController: cVC animated:YES];
}
@end
通过直接引用不同模块的Controller完成调用,这种方式在较小的项目里并没有什么问题;但是随着应用的扩展与版本迭代,APP会变得越来越大,这种方式很容易导致模块之间互相依赖越来越多, 模块之间无法清晰划分、形成强耦合,不利于后期的持续扩展与维护,从而降低了APP整体的迭代速度;且这种方式还不利于有单独业务线的开发模式,单个业务线上的开发需要编译整个工程,影响开发效率与编译速度;
iOS业内当前也形成了多种不同的组件化方案;通过对比不同的组件化设计方案、以及优缺点,可以加深对组件化实现的理解;增强我们自己的设计能力;
不同组件化方案的对比
前面提到过,组件化方案的主要目的就是解耦模块之间的直接调用;计算机领域里有一句话没有什么问题,是不能通过增加一个中间层来解决的
;组件化方案的思路也类似,通过增加一个中间层,让不同的模块之间的互相依赖下沉
到这个中间层;通过这个中间层进行通信,来解决模块之间的直接耦合问题;
因此通常的做法就是增加一个“ModuleManager”的组件管理中间层,并通过这个中间层来管理所有的组件间通信;类似于以下的结构:
通过中间层的实施组件化的实践做法有很多种,对应于不同的组件化方案;
方案一:通过提前在“ ModuleManager”中注册服务的方式,实现组件化;注册的方式至少包括以下几种:
- 通过“URL-Instance”或“URL-Class” 注册实现组件化
- 通过“URL-Block”注册实现组件化
- 通过“Protocol-Class”注册实现组件化
这种做法会在每一个组件模块内部 增加一个与其他组件的通信类,通信类包含该组件需要与外部通信的所有回调接口(即组件自己提供的与外部通信的服务接口);然后通过“ ModuleManager”的中间层来管理所有的组件通信类
,在应用启动时提前把这些通信类注册到“ ModuleManager”内部;
这里“ ModuleManager”要实现组件间的相互调用,需要解决以下两个问题:
- 1、在“ ModuleManager”中如何发现这些组件通信类,并调用到通信类内部的通信接口;
- 2、“ ModuleManager”提供什么形式的外部统一接口供跨组件通信调用;
对于第一个问题,具体的实现也存在多种不同的做法,一种比较简单和直观的做法是,在APP内维护一个组件模块的配置文件“ModulesConfig.json”,配置文件内部记录每个组件的通信类名称信息;在应用启动时读取配置文件,获取出所有的组件通信类信息;
第二个问题关联到在获取到所有的组件通信类后,通过什么样的方式把这些类注册到“ ModuleManager”模块中;注册的方式,就决定了可以提供什么样的与之对应的回调接口;这里就与以上提到过的三种注册方式相关;
通过“URL-Instance” 注册实现的组件化
这种方式通过注册“URL与通信对象”之间的Map关联,并在“ ModuleManager”中存储这个Map信息;在回调时通过URL在Map中获取出通信对象,并在通信对象内部根据指定的URL调用到对应的接口,跳转到对应的组件内页面;注册过程与调用过程的简化实现 类似如下:
// 注册的过程
// modulesConfigClass是 之前从“ModulesConfig.json”配置文件中读取出来的组件通信类信息
// 遍历每一个组件类,并在实例化通信对象后,通过URL方式的完成注册
for (Class cls in [ModuleManager sharedInstance]. modulesConfigClass) {
//实例化通信对象
id impl = [[cls alloc] init];
// route的注册;
// 通信类内部需要实现registModuleRoutes方法,方法内返回一个数组,数组内记录了所有需要注册的URL的硬编码
if ([impl respondsToSelector:@selector(registModuleRoutes)]) {
//遍历所有的URL
[[impl registModuleRoutes] enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//遍历所有的URL,并把url与通信实例impl进行绑定,存放在一个NSDictionary中
if ([obj isKindOfClass:[NSString class]]) {
[ModuleManager.routeService registRoute:obj forModule:impl];
}
}];
}
}
//调用的过程,通过传入URL完成跨组件调用
- (BOOL)openRoute:(NSString *)urlStr withParams:(nullable NSDictionary *)userInfo {
...
//解析urlStr
...
//根据urlStr信息,在“URL与通信对象”的Map字典里获取出实例对象
...
//通过获取到的实例对象,调用对象的路由跳转方法,并在通信类内部根据URL判断 完成对应页面的跳转;
//
...
}
这种方式需要提前注册组件的通信实例对象,会有较大的内存常驻,且大量对象的初始化对应用的启动速度也会存在一定的影响;并会存在较多的URL硬编码,这部分编码的声明可以写在组件通信类的内部;可以在“URL与通信对象”Map字典里只做“URL与通信类”的关联,这可以减少内存开销,只要调用时在懒加载通信类的实例,这种方式就是“URL-Class”的注册方式了;
通过“URL-Block” 注册实现的组件化
“URL-Block”的方式与“URL-Instance”对比并没有本质上的差别;在应用启动时会注册所有的“URL与Block的Map关联”,回调时通过URL获取出对应的Block并执行,完成跨组件通信;与URL-Instance相比,在注册的内存占用上相对来说更节俭(可以不用实例化组件通信类);
在 蘑菇街的组件化实践 里有这种方式的具体实践;简化的实现方式类似如下:
// 注册的过程,某一个组件内部注册的实现
+ (void)registerComponent {
[[ModuleManager sharedInstance] registerURLPattern:@"url://PageA" withCallBack:^(NSDictionary *param) {
PageAViewController *detailVC = [[ModuleAPageViewController alloc] init];
//完成传参与跳转
参数param解析与传递
页面跳转
}
//在ModuleManager内部,注册与调用的实现
- (void)registerURLPattern:(NSString *)urlPattern withCallBack:(componentBlock)blk {
[self.cache setObject:blk forKey:urlPattern];
}
//通过传入URL完成回调
- (BOOL)openRoute:(NSString *)urlStr withParams:(nullable NSDictionary *)userInfo {
...
Block block = self.cache[urlStr]; //获取出回调
block(userInfo); //执行回调
...
}
以上两种通过URL的方式注册的回调,还需要解决应该怎么与调用方约定调用的参数传递的,回调的block里可以获取出正确的参数并做解析使用;蘑菇街内部为了解决这个问题,在内部做了一个网页统一管理所有的URL与参数传递的约定;
通过“Protocol-Class” 注册实现的组件化
这种方式通过给每一个组件提供一个满足通信协议(Protocol)的通信对象;并在应用启动时注册“Protocol与通信对象”之间的Map关联;回调时在通过Protocol获取出Map内部的通信对象,并调用通信对象内部对应的协议方法完成跨组件间的跳转;注册与调用的过程类似以下示例:
//注册的过程,buildModules是之前读取配置记录下来的组件通信类
//遍历每一个组件类,并在实例化通信对象后,通过Protocal方式的完成注册
for (Class cls in [ModuleManager sharedInstance].buildModules) {
//实例化通信类
id impl = [[cls alloc] init];
// Protocol-Service的注册
if ([impl respondsToSelector:@selector(registModuleServices)]) {
//通信类内部需要实现registModuleServices方法,方法内登记所有需要注册的Protocol-Class的关系
// service的注册,serviceArr接收组件内所有的Protocol-Class注册对象
NSArray<TYModuleServiceInfo *> *serviceArr;
if ([impl respondsToSelector:@selector(registModuleServices)]) {
serviceArr = [impl registModuleServices];
}
[serviceArr enumerateObjectsUsingBlock:^(ModuleServiceInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop) {
//遍历所有的Protocol对象,并把Protocol和info对象的关联 存放在一个NSDictionary中
NSString *protocolStr = NSStringFromProtocol(serviceInfo.protocol);
[self.servicesMapping setObject: info forKey:protocolStr];
}];
}
}
//调用的过程,以某一个组件间调用为例
//ModuleManager根据Protocol或取出通信对象,并调用通信对象内部实现的通信协议方法,完成跨组件调用
id< ModuleADetailProtocol > impl = [ModuleManager serviceOfProtocol:@protocol(ModuleADetailProtocol)];
[impl gotoMoudleASubPageViewController:nil];
//serviceOfProtocol方法内部的实现
- (nullable id)serviceOfProtocol:(Protocol *)protocol {
ModuleServiceInfo *info = self.servicesMapping[NSStringFromProtocol(protocol)];
Class cls = info.implClass;
id implInstance = nil;
implInstance = [self singleInstanceOfClass:cls];
return implInstance;
}
这种方式组件间的通信通过约定好的通信协议,并在组件的通信类中实现好协议方法;在跨组件调用时只需要根据对应的协议名称获取出组件的通信对象,调用对应的协议方法即可完成;
以上就是通过提前注册的方式实现的组件化方案,基本原理都是在“ ModuleManager”中通过 提前注册的方式让服务可以被ModuleManager层发现,之后根据不同的注册方式调用对应的组件通信方法,完成跨组件调用;
方案二:通过集成阿里的 BeeLive 实现组件化
BeeHive
是一个比较重量级的开源框架,关注点是方便APP实现模块化编程;能达到组件化的效果,比较适合大型APP的接入与使用;每一个独立的模块还能接受到所有的AppDelegate代理事件;
BeeHive主要也是通过模块注册的方式实现模块化,其中就包括“Protocol-Class”的形式;在应用启动时会完成所有模块服务的注册;
方案三:Casa提出的组件化方案
Casa的组件化方案相对而言是最简单也是耦合性最低的,组件化的目的同时能很好的达到;
前面提到过的几种方式都需要提前注册组件的服务类,提前注册的目的是为了可以根据注册的Key(不管是URL还是Protocol)去发现对应的服务;然后在调起对应的服务完成组件调用;Casa认为:在iOS领域,服务的发现不需要通过注册的方式,通过运行时就能做到;因此Casa实现的组件化方案是基于runtime的方式调用到对应的服务的;
这种方式的原理是通过封装“Target-Action”层实现组件通信模块,并通过中间层“CTMediator”完成跨组件间的调用;“Target”是对象,Action是对象内的方法;通过在每个组件内部封装一个Target层,来提供组件的对外通信;
在“CTMediator”中通过调用“performTarget: action: pramas:”方法调用到组件的通信方法,完成跨组件调用;方法的内部实现大概如下:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
。。。。
NSString *targetClassString = 由targetName转化而来;
//初始化出对应Target对象
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
// 初始化出Action方法
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (shouldCacheTarget) {
//是否执行缓存处理
。。。
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params]; //调用到对应方法
#pragma clang diagnostic pop
}
这里不同组件的调用方法如果都封装在“CTMediator”类内部的话,会导致这个类过于庞大和复杂,难以维护;解决方式是给每一个Target层封装一个CTMediator的分类;达到不同组件模块的接口分离;具体的实践可以看这个demo
在这个组件化框架中还做了远程调用与本地调用的路径区分,方便处理一些远程与本地调用发生异常是的可能存在的不同处理方式等;因为本质上组件化是需要为远程调用服务的;