iOS 组件化方案
为什么要组件化?
易移植、易维护、易重构、易根据业务做加减法、易开发
理想中的组件化
组件化最终应该达到每个组件可以单独开发,单独维护,不会对其他组件进行强依赖。
理想的架构应该在横向上能够拆分出容器层,开源三方库,基础组件,业务形态SDK组件,普通业务组件;在纵向上能够进行组件解耦,组件之间可以单独开发、维护、复用以及组件之间合理的通信机制。随着业务的复杂度增加,理想中的架构也应该不断的变化,
如何进行组件化
先进行组件的拆分,然后进行组件的之间的服务调度,然后进行事件分发包含系统事件以及组件本身自定义的事件实现比较完善的解耦
组件的拆分
将不同的业务代码按照业务的划分整合成一个独立的组件成为独立repo。
进行组件之间服务的调度实现接口依赖
1、将组件之间的依赖实体转为依赖抽象、依赖接口
2、不进行依赖直接依靠OC自身的动态性进行方法的调用
完善组件之间的通信以及事件分发
将系统事件、应用事件以及业务事件进行分发。基本可以做到每个组件之间,组件之间实现无耦合的通信,以及对系统、应用事件的感知。实现每一个组件都可以成为一个独立的APP运行
通用方案调研
目前业界有三大通用方案:面向接口进行解耦、使用URL路由的方式以及使用runtime进行解耦。各自的代表为:serviceCenter注册的BeeHive、URL注册的Router、使用runtime+Category的CTMediator
BeeHive的架构与解析
Beehive在服务调度上采用了protocol的方式,依赖指定的接口来实现组件之间的耦合。
服务注册
注册一个服务主要有两个问题:如何注册以及注册时间,BeeHive采用了三种服务注册的方式
Annotation方式注册
将所需要注册的服务通过注解的方式保存在可执行文件中,在可执行文件加载是的过程中实现protocol与class的注册。这种方式注册服务简单方便,每个组件可以在自己自行注册,不需要集中注册。整体流程对开发者比较友好
保存服务名
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
//
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
这里是将服务名字存在__Data(这是数据段用于存放初始化好的变量)段中的指定区域,到时候和其他资源一起打入ipa中
__attribute((used))是为了防止release模式下被优化(在单测上尤其必要。有些接口只有单测代码引用了这个时候如果不加used进行修饰就无法进行单测)
为什么#要带两个""
进行注册
通过constructor进行修饰,在可执行文件加载之前设置mach-o和动态共享库的加载后的回调
在回调里面通过传过来的Mach-o的文件头地址以及之前设置好的__data段的section名字读取出存在该section的服务protocol以及class然后利用serviceCenter执行服务注册的逻辑,等待使用的时候进行初始化,避免内存常驻
__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback); //添加image的load回调
}
NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
// 读取存贮的服务的section地址
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, BeehiveServiceSectName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
// 读取存贮的服务的section地址
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, BeehiveServiceSectName, &size);
#endif
// 遍历改section获取服务的protocol以及class
unsigned long counter = size/sizeof(void*);
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;
BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
//register services
for (NSString *map in configs) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];
if (protocol && clsName) {
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}
}
}
}
使用文件的方式进行注册
将所需要注册的服务通过plist文件打包到app中,在APP启动的lauch回调中实现实现protocol与class的注册
保存服务名
在文件中以key-value的形式保存服务名到plist文件中
进行注册
在app的lauch中找到对应保存服务的plist文件进行读取,然后使用serviceCenter将服务保存字典中allServicesDict,等待使用的时候进行初始化,避免内存常驻
NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
if (!plistPath) {
return;
}
NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
[self.lock lock];
for (NSDictionary *dict in serviceList) {
NSString *protocolKey = [dict objectForKey:@"service"];
NSString *protocolImplClass = [dict objectForKey:@"impl"];
if (protocolKey.length > 0 && protocolImplClass.length > 0) {
[self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
}
// 使用的时候主动创建服务的实现方
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
使用+load的方式进行注册
无需保存文件名直接在+load方法中利用serviceCenter进行protocol与class的注册,等待使用的时候进行初始化,避免内存常驻
总结
三种注册方式,使用注解的注册方式整体来说比较优雅,对开发者也是比较友好的,注册的时机也是最靠前的。其次是+load也是执行main函数之前使用起来也比较简单。之后是文件注册,注册时间再main函数之后,APP启动中进行注册,一个是时间需要人为的去保证,其实使用起来需要统一改plist文件不是很好的处理方式。
服务的调度
服务的创建
在使用服务之前进行进行服务的获取,使用懒加载避免了服务在内存中常驻
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
1
服务的实现类可以是普通实例也可以是单例,业务方可以自行实现singleton方法进行选择。内部还有一个shouldCache参数决定是否使用cache(看起来作用不是很大,如果是单例则无论是否使用cache,获取的都会是唯一的实例,如果不是单例则无论是否使用cache获取的都是新的实例)。
shouldCache这里可以修改一下使用,由于业务方是不知道每次获取的服务是单例还是唯一的实例,所以确实可以增加shouldCache给业务方使用,当业务方选择使用缓存的时候,这时候如果没有缓存返回空给业务方一个感知,有缓存的时候正常返回。这样shouldCache才有意义
if (shouldCache) {
id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
if (protocolImpl) {
return protocolImpl;
}
//增加else
else {
return nil
}
}
1
2
3
4
5
6
7
8
9
10
服务获取以及移除
在已经创建的服务列表中移除所创建的服务或者获取服务。
- (id)getServiceInstanceFromServiceName:(NSString *)serviceName
{
return [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceName];
}
- (void)removeServiceWithServiceName:(NSString *)serviceName
{
[[BHContext shareInstance] removeServiceWithServiceName:serviceName];
}
1
2
3
4
5
6
7
8
服务的调用
通过协议定义好的接口进行指定服务的调用
总结
能够进行较好的服务调度基本能满足需求,使用延迟创建的方式可以避免内存常驻。区分了单例获取与普通实例获取。在cache设计上可以稍作完善,使得cache变的更有意义
进行组件之间的通信以及消息分发
BeeHive在实现组件调度的同时,实现了组件的可插拨,同时实现了一套消息分发的机制,利用BHContext容器进行组件与组件,组件之前的通信以及消息分发,完善通信机制,一定程度上做到了每一个组件都是一个五脏俱全的小APP,以及彼此之间的一些相互影响
总结
BeeHive架构下组件实现了可插拔,可以独立存在进行开发。组件之间解耦相对彻底,可以进行通信。每个组件有自己的生命周期,独立管理。组件之间通过protocol进行服务之间的调度,对开发者比较友好。同时protocol对方法调用的参数等也进行了约束,
MGJRouter架构解析
MGJRouter使用URL路由的方式进行组件之间的服务调度
服务的注册
app启动期间注册指定的scheme与block
服务的调度
通过解析scheme,进行对应block的获取以及调用
总结
总体来说比较轻量,使用起来简单,但是可用范围也有限。
1、只能注册block,可以增加protocol的注册或者实现解析指定参数利用NSInvocation进行方法的调用
2、注册过程依赖字符串,容易引发问题
CTMediator Target-Action方案
该方案没有注册的流程,使用的runtime利用字符串来实现服务的调度
服务的调度
远程调度
约定好URL规则之后,将远程调度在内部转为本地调度
本地调度
利用CTMediator使用performTarget:action:param进行方法的调度,在CTMediator中利用invocation的方式行方法的调用
分类的使用
为了避免方法签名的不确定性,CTMediator使用分类的方式明确了对外的接口同时在分类内部将参数转成字典传参,使用runtime主动发现服务方后在利用invocation进行方法的调用
总结
整体上利用runtime主动发现服务比注册服务来说解耦是比较完善的。使用category也一定程度上解决了参数的校验问题
但是在mediator内部使用仍然是perfromSelector来调用,所以整体上始终是无法做到完美的参数校验。此外增加了Category层,在里面进行参数的中转以及一些适配逻辑相比较protocol而言并没有减轻增加服务的复杂度
三种方案对比
方案 S W 适用场景
URLRouter 使用简单;支持远程调用 字符串的方式硬编码较多,维护成本大;内存常驻;组件依赖中间件;参数缺乏强约束(字典) 远程调用
Service 面向接口更符合iOS开发者习惯;编编译阶段进行检查,使用上比rumtime方式更安全;支持复杂参数 组件依赖中间件,新增服务稍微麻烦 内部调用
Runtime+category 新增组件更加快速;依赖最小;支持复杂参数 使用runtime缺少参数的校验;对开发者要求更高,使用不当容易产生问题,;不利于重构 内部调用
公司内部解决方案
抖音与头条使用的方案与Beehive类似主要使用的protocol-class的方案进行解耦
使用 TTRoute进行页面之间的跳转.
利用protocol-class进行方法的调用
————————————————
原文链接:https://blog.csdn.net/songzhuo1991/article/details/115977726