最近这几天一直在调研市场上,关于组件通讯这一块的实施方案和技术选型,关于路由
方式和target-action
的方式,由于硬编码
问题,担忧后续维护硬编码可能会耗费大量精力,还有就是基于runtime
的通讯方式编译期难以检查是否有错,这可能会产生运行时问题,因此 Pass 掉了。咱们项目目前 VC 之间经过路由方式进行跳转,实际内部就是经过字符串反射出 Class 进行实例化跳转,提供给 JS 的接口也是基于 runtime 进行了插件化,原理是差很少的,都是经过硬编码在运行时拿到真实类型,再去调用。虽然这两种方案都能进行模块间的解耦,可是在实践过程当中,咱们发现,在进行回归测试的时候,由于同事解决代码冲突,确实发生过路由丢失、插件丢失的状况,因此此次我直接调研了基于 Protocol
构建 Service
的方式(下面 Service 指 Protocol ),以及总结了一下本身的见解。ios
下面主要分析下两个框架,这两个框架是典型的基于 Service
构建的组件通讯,内部有着不少实用技巧,透过这两个框架,咱们去探究下 Service
构建组件通讯的原理,目前我知道的,高德、天猫还有有赞
的一系列App都是基于此方式(可能还有其余我还未接触到)。git
阿里:《BeeHive》github
有赞:《Bifrost》。缓存
关于组件化的介绍网上文章很是多,讲的也很详细,并不是本篇重点。本篇主要分析上述两个框架在组件通讯的优劣,以及一些我的的思考,供技术选型使用。安全
《Bifrost》有赞将它比喻为彩虹桥,对组件间进行链接通讯。代码至关清晰、简单。有赞的思路大概是这样:bash
+load
方法内将他们之间的映射关系注册到字典里。单例
,支持同步、异步初始化,支持加载优先级。这样其余模块想要获取Module实例,只须要经过它的Service,将Service做为key,去管理类中注册的字典,便可拿到,从而实现了组件间依赖解除,大体调用流程以下:markdown
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》和它的思路实际上大致一致,代码相对多些,功能也相对细些,大概思路以下:多线程
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]; } 复制代码
总结一下,大致上看这两个框架思路差很少,可是有些小细节须要再梳理下(如下Module所有表示为Service的具体实现类
):架构
《Bifrost》基于外观模式
,组件间的调用关系所有都有外观类来实现,一个外观类对应一个Service,也就是说一个组件一个Service,有赞认为这样一来,组件间的复杂关系由外观角色来实现,下降了系统的耦合度。它将全部的外观类,也就是Module类都设置为了单例。app
《BeeHive》就我从源码分析来看偏向于主张哪一个类有接口须要被其余组件使用,哪一个类注册一个Service,这个类能够是单例,也能够是多例,可是我以为灵活一点为每一个组件定义一个外观类也能够实现,否则可能Service文件会过多,维护困难。
这一块我的认为二者思路基本一致,相比之下《BeeHive》更灵活。
《Bifrost》注册所有在+load
方法中,每个Module均要实现其+load方法并对Service进行注册,以达到这种映射关系。
+ (void)load {
[Bifrost registerService:@protocol(xxxServiceProtocol) withModule:self.class];
}
复制代码
相比之下《BeeHive》注册有多种方式,最新颖的是经过__attribute()
函数在编译期将这种映射关系添加到 Mach-O 的数据段,在 App 启动的时候将其取出注册到字典中,具体实现都在 BHAnnotation 中。
《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
协议而且实现了其中的方法,若是是的话咱们天然就能够调用这些方法了。