最近这几天一直在调研市场上,关于组件通信这一块的实施方案和技术选型,关于路由
方式和target-action
的方式,因为硬编码
问题,担心后续维护硬编码可能会耗费大量精力,还有就是基于runtime
的通信方式编译期难以检查是否有错,这可能会产生运行时问题,所以 Pass 掉了。我们项目目前 VC 之间通过路由方式进行跳转,实际内部就是通过字符串反射出 Class 进行实例化跳转,提供给 JS 的接口也是基于 runtime 进行了插件化,原理是差不多的,都是通过硬编码在运行时拿到真实类型,再去调用。虽然这两种方案都能进行模块间的解耦,但是在实践过程中,我们发现,在进行回归测试的时候,因为同事解决代码冲突,确实发生过路由丢失、插件丢失的情况,所以这次我直接调研了基于 Protocol
构建 Service
的方式(下面 Service 指 Protocol ),以及总结了一下自己的看法。
下面主要分析下两个框架,这两个框架是典型的基于 Service
构建的组件通信,内部有着很多实用技巧,透过这两个框架,我们去探究下 Service
构建组件通信的原理,目前我知道的,高德、天猫还有有赞
的一系列App都是基于此方式(可能还有其他我还未接触到)。
阿里:《BeeHive》
有赞:《Bifrost》。
基本原理
关于组件化的介绍网上文章非常多,讲的也很详细,并非本篇重点。本篇主要分析上述两个框架在组件通信的优劣,以及一些个人的思考,供技术选型使用。
《Bifrost》有赞将它比喻为彩虹桥,对组件间进行连接通信。代码相当清晰、简单。有赞的思路大概是这样:
- 1:每一个业务组件,都定义一个Module和一个Service,Module用来实现对外提供的一些功能,Service用来定义组件对外暴漏的接口,旨在对外提供服务。
- 2:再通过一个管理类,在
+load
方法内将他们之间的映射关系注册到字典里。 - 3:app启动的时候,将所有Module进行实例化,实际所有Module皆为
单例
,支持同步、异步初始化,支持加载优先级。
这样其他模块想要获取Module实例,只需要通过它的Service,将Service作为key,去管理类中注册的字典,即可拿到,从而实现了组件间依赖解除,大致调用流程如下:
id<xxxService> module = [[Bifrost moduleByService:@protocol(xxxService)] doSomething:xxx];
+ (id<BifrostModuleProtocol> _Nullable)moduleByService:(Protocol*_Nonnull)serviceProtocol {
// 映射String
NSString *protocolStr = NSStringFromProtocol(serviceProtocol);
...
// moduleDict 之前注册的字典取Class
Class class = BFInstance.moduleDict[protocolStr];
// 单例,此时已经是在启动的时候初始化好的了
id instance = [class sharedInstance];
return instance;
}
《BeeHive》和它的思路实际上大体一致,代码相对多些,功能也相对细些,大概思路如下:
- 1:从源码上来看,BeeHive认为每个需要对其他组件提供接口的类,都可以注册一个Service,旨在哪里需要对外提供服务,哪里进行注册,相对灵活。例如组件A的某个类需要提供一个接口给组件B,那么组件A的这个类需要对组件B提供一个Service(定义接口),再将这个Service和这个类注册到BeeHive中。这样B组件或者其他组件只需要引用Service即可。BeeHive将所有Service抽离处理放到一起让其他组件引用。
- 2:BeeHive通过多种方式用来注册Module和Service的映射关系,不管是哪种方式最后都会通过管理类单例注册到字典中。
- 3:组件间接口调用的时候,会通过管理类找到注册的字典,再将注册的Service为key,获取到对应的Module实例,Module实例支持单例和多例的初始化形式,在获取的过程中,还支持将其缓存到字典,这样拿到实例就可以直接调用了,从源码来看有通过递归锁保证在多线程访问的情况下,按序访问数据安全。代码大致流程如下:
id<xxxServiceProtocol> module = [[BeeHive shareInstance] createService:@protocol(xxxServiceProtocol)];
- (id)createService:(Protocol *)service
{
return [self createService:service withServiceName:nil];
}
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {
return [self createService:service withServiceName:serviceName shouldCache:YES];
}
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {
...
NSString *serviceStr = serviceName;
// 支持缓存,先去缓存中查找,存在返回,不存在继续往下走
if (shouldCache) {
id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
if (protocolImpl) {
return protocolImpl;
}
}
// 去管理类的字典中找module类名字符串并转为Class
NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
if (serviceImpl.length > 0) {
Class implClass = NSClassFromString(serviceImpl);
}
// 如果实现了singleton
if ([[implClass class] respondsToSelector:@selector(singleton)]) {
if ([[implClass class] singleton]) {
if ([[implClass class] respondsToSelector:@selector(shareInstance)])
// 实现了shareInstance就设置为单例
implInstance = [[implClass class] shareInstance];
else
implInstance = [[implClass alloc] init];
// 设置了缓存那就存储一下
if (shouldCache) {
[[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
return implInstance;
} else {
return implInstance;
}
}
}
// 未实现singleton直接返回为多例
return [[implClass alloc] init];
}
- 4:BeeHive还有一些解耦AppDelegate的逻辑,这里暂不展开。
对比选型
总结一下,大体上看这两个框架思路差不多,但是有些小细节需要再梳理下(以下Module全部表示为Service的具体实现类
):
Module划分
《Bifrost》基于外观模式
,组件间的调用关系全部都有外观类来实现,一个外观类对应一个Service,也就是说一个组件一个Service,有赞认为这样一来,组件间的复杂关系由外观角色来实现,降低了系统的耦合度。它将所有的外观类,也就是Module类都设置为了单例。
《BeeHive》就我从源码分析来看偏向于主张哪个类有接口需要被其他组件使用,哪个类注册一个Service,这个类可以是单例,也可以是多例,但是我觉得灵活一点为每个组件定义一个外观类也可以实现,不然可能Service文件会过多,维护困难。
这一块个人认为两者思路基本一致,相比之下《BeeHive》更灵活。
Module注册
《Bifrost》注册全部在+load
方法中,每一个Module均要实现其+load方法并对Service进行注册,以达到这种映射关系。
+ (void)load {
[Bifrost registerService:@protocol(xxxServiceProtocol) withModule:self.class];
}
相比之下《BeeHive》注册有多种方式,最新颖的是通过__attribute()
函数在编译期将这种映射关系添加到 Mach-O 的数据段,在 App 启动的时候将其取出注册到字典中,具体实现都在 BHAnnotation 中。
Module管理
《Bifrost》在 App 启动的时候,在 AppDelegate 的 willFinishLaunchingWithOptions
中,将所有 Module,按照顺序进行初始化,且全部为单例。有赞在实践的过程中组件最多在20几个,所以这些单例不会带来内存问题。初始化支持异步。《Bifrost》在组件间调用的时候实际上拿到的实例已经是被初始化好的单例了。
+ (void)setupAllModules {
NSArray *modules = [self allRegisteredModules];
for (Class<BifrostModuleProtocol> moduleClass in modules) {
...省略一些代码
if (setupSync) {
[[moduleClass sharedInstance] setup];
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[moduleClass sharedInstance] setup];
});
}
}
}
《BeeHive》在 App 启动的时候,并未将所有 Module 实例化,而是将其类名及对应的 Service 名添加到管理类的字典中,在组件间真正要实施通讯的时候,根据 Service 名称,去字典中取 Module 类名,才去进行实例化,实例化的过程中支持将其设置为单例或者多例。
- (void)registerService:(Protocol *)service implClass:(Class)implClass {
...
NSString *key = NSStringFromProtocol(service);
NSString *value = NSStringFromClass(implClass);
if (key.length > 0 && value.length > 0) {
[self.lock lock];
// 实际上只是将string存储了起来并未将其实例化
[self.allServicesDict addEntriesFromDictionary:@{key:value}];
[self.lock unlock];
}
}
在管理这一块个人感觉他们之间有着本质区别,《Bifrost》将所有 Module 设置为单例,在实践中我发现这样配置使用起来确实非常的方便,通过 Service 直接获取单例即可,尤其在需要 Module 存储某些状态时。但是这样做也发现了一个问题,因为 Module 的定位是整个组件所有对外暴漏接口的包装层,但我往往因为一些业务场景,需要 Module 持有那个具体的实现类,这时会发现被单例持有的这个类内存释放会比较麻烦,绕点弯也可以解决,但总觉得不那么美观。所以这里我相对来说偏向于《BeeHive》对组件的外观类添加多例的实现,需要的时候进行初始化,用完即释放。
传参处理
《BeeHive》在传参处理上,未看到对 model 传递的处理,如果我们需要将一个 model 从组件 A 传递到组件 B,至少在 BeeHive 的 Demo 里,如果想要传递整个 model,需要将 model 所有字段都以参数的形式传递给组件 B 使用,这样会让接口显得非常的长,也不够直观。如果组件 B 可以直接拿到 model,那么组件 B 将会很轻松的知道这个接口传递的参数来源于哪,具体是做什么的,也会侧面加强业务关联性,另外还可以通过点语法来获取参数值,这其实将非常利于读写。《Bifrost》就提供了一个很好的思路,它为 model 也构建了 Service,代码编写在 Module 所在的那个 Service 中,如下所示:
@interface GoodsModel : NSObject<GoodsProtocol>
@property(nonatomic, strong) NSString *goodsId;
@property(nonatomic, strong) NSString *name;
@property(nonatomic, assign) CGFloat price;
@property(nonatomic, assign) NSInteger inventory;
@end
#pragma mark - Model Protocols
@protocol GoodsProtocol <NSObject>
- (NSString*)goodsId;
- (NSString*)name;
- (CGFloat)price;
- (NSInteger)inventory;
@end
使用起来也很方便:
id<GoodsProtocol> goods = [BFModule(GoodsModuleService) goodsById:item.goodsId];
BFModule宏定义展开:
#define BFModule(service_protocol) ((id<service_protocol>)[Bifrost moduleByService:@protocol(service_protocol)])
总的来说,在《Bifrost》的基础上,Module管理这块,融汇一下《BeeHive》的注册方式,支持多例,在使用时创建用完释放等思想会不会更好些。
总结
额外的再说下基于 Protocol
的方式最主要的优势,就是出问题编译期就能报错,编译器帮我们检查了是否有文件缺失,是否有引用缺失,我想这也是很多公司采用这种方式的最主要原因。两个模块,通过 id <xxxServiceProtocol> xxx = ...
即可拿到其中一个模块的实例,而不需要对模块的头文件引用,从而达到模块间编译隔离和模块间通信。
接触少的同学可能会觉得这有点绕,这实际上和我们常用的代理原理一致,当我们编写一个工具类对外提供一个代理的时候,你会关心调用你的这个工具类具体是哪一个类吗?答案当然是不会的,我们只需要关心调用方是否遵循了 xxxServiceProtocol
协议并且实现了其中的方法,如果是的话我们自然就可以调用这些方法了。