美文网首页
iOS 基于Service构建组件通信的思考

iOS 基于Service构建组件通信的思考

作者: LotLewis | 来源:发表于2020-03-18 22:54 被阅读0次

    最近这几天一直在调研市场上,关于组件通信这一块的实施方案和技术选型,关于路由方式和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 协议并且实现了其中的方法,如果是的话我们自然就可以调用这些方法了。

    ref:

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

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

    相关文章

      网友评论

          本文标题:iOS 基于Service构建组件通信的思考

          本文链接:https://www.haomeiwen.com/subject/wuuhyhtx.html