关于 iOS 组件通讯的思考

最近这几天一直在调研市场上,关于组件通讯这一块的实施方案和技术选型,关于路由方式和target-action的方式,由于硬编码问题,担忧后续维护硬编码可能会耗费大量精力,还有就是基于runtime的通讯方式编译期难以检查是否有错,这可能会产生运行时问题,因此 Pass 掉了。咱们项目目前 VC 之间经过路由方式进行跳转,实际内部就是经过字符串反射出 Class 进行实例化跳转,提供给 JS 的接口也是基于 runtime 进行了插件化,原理是差很少的,都是经过硬编码在运行时拿到真实类型,再去调用。虽然这两种方案都能进行模块间的解耦,可是在实践过程当中,咱们发现,在进行回归测试的时候,由于同事解决代码冲突,确实发生过路由丢失、插件丢失的状况,因此此次我直接调研了基于 Protocol 构建 Service 的方式(下面 Service 指 Protocol ),以及总结了一下本身的见解。ios

下面主要分析下两个框架,这两个框架是典型的基于 Service 构建的组件通讯,内部有着不少实用技巧,透过这两个框架,咱们去探究下 Service 构建组件通讯的原理,目前我知道的,高德、天猫还有有赞的一系列App都是基于此方式(可能还有其余我还未接触到)。git

阿里:《BeeHive》github

有赞:《Bifrost》缓存

基本原理

关于组件化的介绍网上文章很是多,讲的也很详细,并不是本篇重点。本篇主要分析上述两个框架在组件通讯的优劣,以及一些我的的思考,供技术选型使用。安全

《Bifrost》有赞将它比喻为彩虹桥,对组件间进行链接通讯。代码至关清晰、简单。有赞的思路大概是这样:bash

  • 1:每个业务组件,都定义一个Module和一个Service,Module用来实现对外提供的一些功能,Service用来定义组件对外暴漏的接口,旨在对外提供服务。
  • 2:再经过一个管理类,在+load方法内将他们之间的映射关系注册到字典里。
  • 3:app启动的时候,将全部Module进行实例化,实际全部Module皆为单例,支持同步、异步初始化,支持加载优先级。

这样其余模块想要获取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》和它的思路实际上大致一致,代码相对多些,功能也相对细些,大概思路以下:多线程

  • 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类都设置为了单例。app

《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 协议而且实现了其中的方法,若是是的话咱们天然就能够调用这些方法了。

ref:

《BeeHive,一次 iOS 模块化解耦实践》

《有赞移动 iOS 组件化(模块化)架构设计实践》

相关文章
相关标签/搜索