美文网首页iOS-开发iOS组件化EMoney 学习
iOS 组件化(一)常见方案解析

iOS 组件化(一)常见方案解析

作者: 辉辉岁月 | 来源:发表于2021-03-09 15:58 被阅读0次

    关于组件化的探讨已经有不少了,综合比较了各种方案后,我倾向于使用面向接口的方式进行组件化。

    这是一篇从代码层面讲解模块解耦的文章,会全方位地展示如何实践面向接口的思想,尽量全面地探讨在模块管理和解耦的过程中,需要考虑到的各种问题,并且给出实际的解决方案,以及对应的模块管理开源工具:ZIKRouter。你也可以根据本文的内容改造自己现有的方案,即使你的项目不进行组件化,也可以参考本文进行代码解耦。

    文章主要内容:

    • 如何衡量模块解耦的程度
    • 对比不同方案的优劣
    • 在编译时进行静态路由检查,避免使用不存在的模块
    • 如何进行模块解耦,包括模块重用、模块适配、模块间通信、子模块交互
    • 模块的接口和依赖管理
    • 管理界面跳转逻辑

    什么是组件化

    将模块单独抽离、分层,并制定模块间通信的方式,从而实现解耦,以及适应团队开发。

    为什么需要组件化

    主要有4个原因:

    • 模块间解耦
    • 模块重用
    • 提高团队协作开发效率
    • 单元测试

    当项目越来越大的时候,各个模块之间如果是直接互相引用,就会产生许多耦合,导致接口滥用,当某天需要进行修改时,就会牵一发而动全身,难以维护。

    问题主要体现在:

    • 修改某个模块的功能时,需要修改许多其他模块的代码,因为这个模块被其他模块引用
    • 模块对外的接口不明确,外部甚至会调用不应暴露的私有接口,修改时会耗费大量时间
    • 修改的模块涉及范围较广,很容易影响其他团队成员的开发,产生代码冲突
    • 当需要抽离模块到其他地方重用时,会发现耦合导致根本无法单独抽离
    • 模块间的耦合导致接口和依赖混乱,难以编写单元测试

    所以需要减少模块之间的耦合,用更规范的方式进行模块间交互。这就是组件化,也可以叫做模块化。

    你的项目是否需要组件化

    组件化也不是必须的,有些情况下并不需要组件化:

    • 项目较小,模块间交互简单,耦合少
    • 模块没有被多个外部模块引用,只是一个单独的小模块
    • 模块不需要重用,代码也很少被修改
    • 团队规模很小
    • 不需要编写单元测试

    组件化也是有一定成本的,你需要花时间设计接口,分离代码,所以并不是所有的模块都需要组件化。

    不过,当你发现这几个迹象时,就需要考虑组件化了:

    • 模块逻辑复杂,多个模块间频繁互相引用
    • 项目规模逐渐变大,修改代码变得越来越困难
    • 团队人数变多,提交的代码经常和其他成员冲突
    • 项目编译耗时较大
    • 模块的单元测试经常由于其他模块的修改而失败

    组件化方案的8条指标

    决定了要开始组件化之路后,就需要思考我们的目标了。一个组件化方案需要达到怎样的效果呢?我在这里给出8个理想情况下的指标:

    1. 模块间没有直接耦合,一个模块内部的修改不会影响到另一个模块
    2. 模块可以被单独编译
    3. 模块间能够清晰地进行数据传递
    4. 模块可以随时被另一个提供了相同功能的模块替换
    5. 模块的对外接口容易查找和维护
    6. 当模块的接口改变时,使用此模块的外部代码能够被高效地重构
    7. 尽量用最少的修改和代码,让现有的项目实现模块化
    8. 支持 Objective-C 和 Swift,以及混编

    前4条用于衡量一个模块是否真正解耦,后4条用于衡量在项目实践中的易用程度。最后一条必须支持 Swift,是因为 Swift 是一个必然的趋势,如果你的方案不支持 Swift,说明这个方案在将来的某个时刻必定要改进改变,而到时候所有基于这个方案实现的模块都会受到影响。

    基于这8个指标,我们就能在一定程度上对我们的方案做出衡量了。

    方案对比

    现在主要有3种组件化方案:URL 路由、target-action、protocol 匹配。

    接下来我们就比较一下这几种组件化方案,看看它们各有什么优缺点。必须要先说明的是,没有一个完美的方案能满足所有场景下的需求,需要根据每个项目的需求选择最适合的方案。

    URL 路由

    目前 iOS 上绝大部分的路由工具都是基于 URL 匹配的,或者是根据命名约定,用 runtime 方法进行动态调用。

    这些动态化的方案的优点是实现简单,缺点是需要维护字符串表,或者依赖于命名约定,无法在编译时暴露出所有问题,需要在运行时才能发现错误。

    代码示例:

    // 注册某个URL
    
    [URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {
    
        UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];
    
        return editorViewController;
    
    }];
    
    // 调用路由
    
    [URLRouter openURL:@"app://editor/?debug=true" completion:^(NSDictionary *info) {
    
    }];
    

    URL router 的优点:

    • 极高的动态性,适合经常开展运营活动的 app,例如电商
    • 方便地统一管理多平台的路由规则
    • 易于适配 URL Scheme

    URL router 的缺点:

    • 传参方式有限,并且无法利用编译器进行参数类型检查,因此所有的参数都只能从字符串中转换而来
    • 只适用于界面模块,不适用于通用模块
    • 不能使用 designated initializer 声明必需参数
    • 要让 view controller 支持 url,需要为其新增初始化方法,因此需要对模块做出修改
    • 不支持 storyboard
    • 无法明确声明模块提供的接口,只能依赖于接口文档,重构时无法确保修改正确
    • 依赖于字符串硬编码,难以管理
    • 无法保证所使用的模块一定存在
    • 解耦能力有限,url 的”注册”、”实现”、”使用”必须用相同的字符规则,一旦任何一方做出修改都会导致其他方的代码失效,并且重构难度大

    字符串解耦的问题

    如果用上面的8个指标来衡量,URL 路由只能满足”支持模块单独编译”、”支持 OC 和 Swift”两条。它的解耦程度非常一般。

    所有基于字符串的解耦方案其实都可以说是伪解耦,它们只是放弃了编译依赖,但是当代码变化之后,即便能够编译运行,逻辑仍然是错误的。

    例如修改了模块定义时的 URL:

    // 注册某个URL
    
    [URLRouter registerURL:@"app://editorView" handler:^(NSDictionary *userInfo) {
    
        ...
    
    }];
    

    那么调用者的 URL 也必须修改,代码仍然是有耦合的,只不过此时编译器无法检查而已。这会导致维护更加困难,一旦 URL 中的参数有了增减,或者决定替换为另一个模块,参数命名有了变化,几乎没有高效的方式来重构代码。可以使用宏定义来管理字符串,不过这要求所有模块都使用同一个头文件,并且也无法解决参数类型和数量变化的问题。

    URL 路由适合用来做远程模块的网络协议交互,而在管理本地模块时,最大的甚至是唯一的优势,就是适合经常跨多端运营活动的 app,因为可以由运营人员统一管理多平台的路由规则。

    代表框架

    改进:避免字符串管理

    改进 URL 路由的方式,就是避免使用字符串,通过接口管理模块。

    参数可以通过 protocol 直接传递,能够利用编译器检查参数类型,并且在 ZIKRouter 中,能通过路由声明和编译检查,保证所使用的模块一定存在。在为模块创建路由时,也无需修改模块的代码。

    但是必须要承认的是,尽管 URL 路由缺点多多,但它在跨平台路由管理上的确是最适合的方案。因此 ZIKRouter 也对 URL 路由做出了支持,在用 protocol 管理的同时,可以通过字符串匹配 router,也能和其他 URL router 框架对接。

    Target-Action 方案

    有一些模块管理工具基于 Objective-C 的 runtime、category 特性动态获取模块。例如通过NSClassFromString获取类并创建实例,通过performSelector: NSInvocation动态调用方法。

    例如基于 target-action 模式的设计,大致是利用 category 为路由工具添加新接口,在接口中通过字符串获取对应的类,再用 runtime 创建实例,动态调用实例的方法。

    示例代码:

    // 模块管理者,提供了动态调用 target-action 的基本功能
    
    @interface Mediator : NSObject
    
    + (instancetype)sharedInstance;
    
    - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
    
    @end
    
    // 在 category 中定义新接口
    
    @interface Mediator (ModuleActions)
    
    - (UIViewController *)Mediator_editorViewController;
    
    @end
    
    @implementation Mediator (ModuleActions)
    
    - (UIViewController *)Mediator_editorViewController {
    
        // 使用字符串硬编码,通过 runtime 动态创建 Target_Editor,并调用 Action_viewController:
    
        UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];
    
        return viewController;
    
    }
    
    @end
    
    // 调用者通过 Mediator 的接口调用模块
    
    UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
    
    // 模块提供者提供 target-action 的调用方式
    
    @interface Target_Editor : NSObject
    
    - (UIViewController *)Action_viewController:(NSDictionary *)params;
    
    @end
    
    @implementation Target_Editor
    
    - (UIViewController *)Action_viewController:(NSDictionary *)params {
    
        // 参数通过字典传递,无法保证类型安全
    
        EditorViewController *viewController = [[EditorViewController alloc] init];
    
        viewController.valueLabel.text = params[@"key"];
    
        return viewController;
    
    }
    
    @end
    

    优点:

    • 利用 category 可以明确声明接口,进行编译检查
    • 实现方式轻量

    缺点:

    • 需要在 mediator 和 target 中重新添加每一个接口,模块化时代码较为繁琐
    • 在 category 中仍然引入了字符串硬编码,内部使用字典传参,一定程度上也存在和 URL 路由相同的问题
    • 无法保证所使用的模块一定存在,target 模块在修改后,使用者只有在运行时才能发现错误
    • 过于依赖 runtime 特性,无法应用到纯 Swift 上。在 Swift 中扩展 mediator 时,无法使用纯 Swift 类型的参数
    • 可能会创建过多的 target 类
    • 使用 runtime 相关的接口调用任意类的任意方法,需要注意别被苹果的审核误伤。参考:Are performSelector and respondsToSelector banned by App Store?

    字典传参的问题

    字典传参时无法保证参数的数量和类型,只能依赖调用约定,就和字符串传参一样,一旦某一方做出修改,另一方也必须修改。

    相比于 URL 路由,target-action 通过 category 的接口把字符串管理的问题缩小到了 mediator 内部,不过并没有完全消除,而且在其他方面仍然有很多改进空间。上面的8个指标中其实只能满足第2个”支持模块单独编译”,另外在和接口相关的第3、5、6点上,比 URL 路由要有改善。

    代表框架

    CTMediator

    CTMediator源码分析

    通过分类中调用的performTarget来到CTMediator中的具体实现,即performTarget:action:params:shouldCacheTarget:,主要是通过传入的name,找到对应的target 和 action

    - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
    {
        if (targetName == nil || actionName == nil) {
            return nil;
        }
        //在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
        NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
        
        // generate target 生成target
        NSString *targetClassString = nil;
        if (swiftModuleName.length > 0) {
            //swift中target文件名拼接
            targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
        } else {
            //OC中target文件名拼接
            targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
        }
        //缓存中查找target
        NSObject *target = [self safeFetchCachedTarget:targetClassString];
        //缓存中没有target
        if (target == nil) {
            //通过字符串获取对应的类
            Class targetClass = NSClassFromString(targetClassString);
            //创建实例
            target = [[targetClass alloc] init];
        }
    
        // generate action 生成action方法名称
        NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
        //通过方法名字符串获取对应的sel
        SEL action = NSSelectorFromString(actionString);
        
        if (target == nil) {
            // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            return nil;
        }
        //是否需要缓存
        if (shouldCacheTarget) {
            [self safeSetCachedTarget:target key:targetClassString];
        }
        //是否响应sel
        if ([target respondsToSelector:action]) {
            //动态调用方法
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
                [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
                @synchronized (self) {
                    [self.cachedTarget removeObjectForKey:targetClassString];
                }
                return nil;
            }
        }
    }
    
    • 进入safePerformAction:target:params:实现,主要是通过invocation进行参数传递+消息转发
    - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
    {
        //获取方法签名
        NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
        if(methodSig == nil) {
            return nil;
        }
        //获取方法签名中的返回类型,然后根据返回值完成参数传递
        const char* retType = [methodSig methodReturnType];
        //void类型
        if (strcmp(retType, @encode(void)) == 0) {
            ...
        }
        //...省略其他类型的判断
    }
    

    改进:避免字典传参

    Target-Action 方案最大的优点就是整个方案实现轻量,并且也一定程度上明确了模块的接口。只是这些接口都需要通过 Target-Action 封装一次,并且每个模块都要创建一个 target 类,既然如此,直接用 protocol 进行接口管理会更加简单。

    ZIKRouter 避免使用 runtime 获取和调用模块,因此可以适配 OC 和 swift。同时,基于 protocol 匹配的方式,避免引入字符串硬编码,能够更好地管理模块,也避免了字典传参。

    protocol class

    protocol匹配的实现思路是:

    • 1、将protocol和对应的进行字典匹配

    • 2、通过用protocol获取class,在动态创建实例

    protocol比较典型的三方框架就是阿里的BeeHiveBeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能基础功能模块以模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务。

    BeeHive 核心思想

    • 1、各个模块间调用从直接调用对应模块,变成调用Service的形式,避免了直接依赖。

    • 2、App生命周期的分发,将耦合在AppDelegate中逻辑拆分,每个模块以微应用的形式独立存在。

    示例如下:

    //******** 1、注册
    [[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];
    
    //******** 2、使用
    #import "BHService.h"
    
    id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];
    
    

    优点

    • 1、利用接口调用,实现了参数传递时的类型安全

    • 2、直接使用模块的protocol接口,无需再重复封装

    缺点

    • 1、用框架来创建所有对象,创建方式不同,即不支持外部传入参数

    • 2、用OC runtime创建对象,不支持swift

    • 3、只做了protocolclass 的匹配,不支持更复杂的创建方式 和依赖注入

    • 4、无法保证所使用的protocol 一定存在对应的模块,也无法直接判断某个protocol是否能用于获取模块

    除了BeeHive,还有Swinject

    BeeHive 模块注册

    BeeHive主要是通过BHModuleManager来管理各个模块的。BHModuleManager中只会管理已经被注册过的模块。

    BeeHive提供了三种不同的调用形式,静态plist动态注册annotation。Module、Service之间没有关联,每个业务模块可以单独实现Module或者Service的功能。

    1、 Annotation方式注册
    这种方式主要是通过BeeHiveMod宏进行Annotation标记

    //***** 使用
    BeeHiveMod(ShopModule)
    
    //***** BeeHiveMod的宏定义
    #define BeeHiveMod(name) \
    class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
    
    //***** BeeHiveDATA的宏定义 
    #define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
    
    //*****  全部转换出来后为下面的格式
    char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";
    
    

    这里针对__attribute需要说明以下几点

    • 第一个参数used:用来修饰函数,被used修饰以后,意味着即使函数没有被引用,在Release下也不会被优化。如果不加这个修饰,那么Release环境链接器下会去掉没有被引用的段。

    • 通过使用__attribute__((section("name")))来指明哪个段。数据则用__attribute__((used))来标记,防止链接器会优化删除未被使用的段,然后将模块注入到__DATA

    此时Module已经被存储到Mach-O文件的特殊段中,那么如何取呢?

    • 进入BHReadConfiguration方法,主要是通过Mach-O找到存储的数据段,取出放入数组中
    NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
    {
    
        NSMutableArray *configs = [NSMutableArray array];
        unsigned long size = 0;
    #ifndef __LP64__
        // 找到之前存储的数据段(Module找BeehiveMods段 和 Service找BeehiveServices段)的一片内存
        uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
    #else
        const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
        uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
    #endif
    
        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];
        }
    
        return configs; 
    }
    

    2、读取本地Pilst文件

    • 首先,需要设置好路径
    [BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist
    
    
    • 创建plist文件,Plist文件的格式也是数组中包含多个字典。字典里面有两个Key,一个是@"moduleLevel",另一个是@"moduleClass"。注意的数组的名字叫@“moduleClasses”

      image
    • 进入loadLocalModules方法,主要是从Plist里面取出数组,然后把数组加入到BHModuleInfos数组里面。

    //初始化context时,加载Modules和Services
    -(void)setContext:(BHContext *)context
    {
        _context = context;
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self loadStaticServices];
            [self loadStaticModules];
        });
    }
    👇
    //加载modules
    - (void)loadStaticModules
    {
        // 读取本地plist文件里面的Module,并注册到BHModuleManager的BHModuleInfos数组中
        [[BHModuleManager sharedManager] loadLocalModules];
        //注册所有modules,在内部根据优先级进行排序
        [[BHModuleManager sharedManager] registedAllModules];
    
    }
    👇
    - (void)loadLocalModules
    {
        //plist文件路径
        NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"];
        //判断文件是否存在
        if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath]) {
            return;
        }
        //读取整个文件[@"moduleClasses" : 数组]
        NSDictionary *moduleList = [[NSDictionary alloc] initWithContentsOfFile:plistPath];
        //通过moduleClasses key读取 数组 [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]]
        NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey];
        NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy;
        //遍历数组
        [self.BHModuleInfos enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            [moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]];
        }];
        [modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if (!moduleInfoByClass[[obj objectForKey:kModuleInfoNameKey]]) {
                //存储到 BHModuleInfos 中
                [self.BHModuleInfos addObject:obj];
            }
        }];
    }
    
    

    3、load方法注册

    该方法注册Module就是在Load方法里面注册Module的类

    + (void)load
    {
        [BeeHive registerDynamicModule:[self class]];
    }
    
    
    • 进入registerDynamicModule实现
    + (void)registerDynamicModule:(Class)moduleClass
    {
        [[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
    }
    👇
    - (void)registerDynamicModule:(Class)moduleClass
    {
        [self registerDynamicModule:moduleClass shouldTriggerInitEvent:NO];
    }
    👇
    - (void)registerDynamicModule:(Class)moduleClass
           shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent
    {
        [self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent];
    }
    
    

    其底层还是同第一种方式一样,最终会走到addModuleFromObject:shouldTriggerInitEvent:方法中

    • load方法,还可以使用BH_EXPORT_MODULE宏代替
    #define BH_EXPORT_MODULE(isAsync) \
    + (void)load { [BeeHive registerDynamicModule:[self class]]; } \
    -(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}
    
    

    BH_EXPORT_MODULE宏里面可以传入一个参数,代表是否异步加载Module模块,如果是YES就是异步加载,如果是NO就是同步加载

    2、BeeHive 模块事件

    BeeHive会给每个模块提供生命周期事件,用于与BeeHive宿主环境进行必要信息交互,感知模块生命周期的变化。

    BeeHive各个模块会收到一些事件。在BHModuleManager中,所有的事件被定义成了BHModuleEventType枚举。如下所示,其中有2个事件很特殊,一个是BHMInitEvent,一个是BHMTearDownEvent

    typedef NS_ENUM(NSInteger, BHModuleEventType)
    {
        //设置Module模块
        BHMSetupEvent = 0,
        //用于初始化Module模块,例如环境判断,根据不同环境进行不同初始化
        BHMInitEvent,
        //用于拆除Module模块
        BHMTearDownEvent,
        BHMSplashEvent,
        BHMQuickActionEvent,
        BHMWillResignActiveEvent,
        BHMDidEnterBackgroundEvent,
        BHMWillEnterForegroundEvent,
        BHMDidBecomeActiveEvent,
        BHMWillTerminateEvent,
        BHMUnmountEvent,
        BHMOpenURLEvent,
        BHMDidReceiveMemoryWarningEvent,
        BHMDidFailToRegisterForRemoteNotificationsEvent,
        BHMDidRegisterForRemoteNotificationsEvent,
        BHMDidReceiveRemoteNotificationEvent,
        BHMDidReceiveLocalNotificationEvent,
        BHMWillPresentNotificationEvent,
        BHMDidReceiveNotificationResponseEvent,
        BHMWillContinueUserActivityEvent,
        BHMContinueUserActivityEvent,
        BHMDidFailToContinueUserActivityEvent,
        BHMDidUpdateUserActivityEvent,
        BHMHandleWatchKitExtensionRequestEvent,
        BHMDidCustomEvent = 1000
    
    };
    
    

    主要分为三种

    • 1、系统事件:主要是指Application生命周期事件!

    一般的做法是AppDelegate改为继承自BHAppDelegate

    @interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>
    
    
    • 2、应用事件:官方给出的流程图,其中modSetupmodInit等,可以用于编码实现各插件模块的设置与初始化。

    • 3、自定义事件

    以上所有的事件都可以通过调用BHModuleManagertriggerEvent:来处理。

    - (void)triggerEvent:(NSInteger)eventType
    {
        [self triggerEvent:eventType withCustomParam:nil];
    
    }
    👇
    - (void)triggerEvent:(NSInteger)eventType
         withCustomParam:(NSDictionary *)customParam {
        [self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam];
    }
    👇
    #pragma mark - module protocol
    - (void)handleModuleEvent:(NSInteger)eventType
                    forTarget:(id<BHModuleProtocol>)target
              withCustomParam:(NSDictionary *)customParam
    {
        switch (eventType) {
                //初始化事件
            case BHMInitEvent:
                //special
                [self handleModulesInitEventForTarget:nil withCustomParam :customParam];
                break;
                //析构事件
            case BHMTearDownEvent:
                //special
                [self handleModulesTearDownEventForTarget:nil withCustomParam:customParam];
                break;
                //其他3类事件
            default: {
                NSString *selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
                [self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam];
            }
                break;
        }
    
    }
    
    

    从上面的代码中可以发现,除去BHMInitEvent初始化事件和BHMTearDownEvent拆除Module事件这两个特殊事件以外,所有的事件都是调用的handleModuleEvent:forTarget:withSeletorStr:andCustomParam:方法,其内部实现主要是遍历 moduleInstances 实例数组,调用performSelector:withObject:方法实现对应方法调用

    - (void)handleModuleEvent:(NSInteger)eventType
                    forTarget:(id<BHModuleProtocol>)target
               withSeletorStr:(NSString *)selectorStr
               andCustomParam:(NSDictionary *)customParam
    {
        BHContext *context = [BHContext shareInstance].copy;
        context.customParam = customParam;
        context.customEvent = eventType;
        if (!selectorStr.length) {
            selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
        }
        SEL seletor = NSSelectorFromString(selectorStr);
        if (!seletor) {
            selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
            seletor = NSSelectorFromString(selectorStr);
        }
        NSArray<id<BHModuleProtocol>> *moduleInstances;
        if (target) {
            moduleInstances = @[target];
        } else {
            moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
        }
        //遍历 moduleInstances 实例数组,调用performSelector:withObject:方法实现对应方法调用
        [moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([moduleInstance respondsToSelector:seletor]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                //进行方法调用
                [moduleInstance performSelector:seletor withObject:context];
    #pragma clang diagnostic pop
    
                [[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
    
            }
        }];
    }
    
    

    注意:这里所有的Module必须是遵循BHModuleProtocol的,否则无法接收到这些事件的消息。

    3、BeeHive模块调用

    在BeeHive中是通过BHServiceManager来管理各个Protocol的。BHServiceManager中只会管理已经被注册过的Protocol

    注册Protocol的方式总共有三种,和注册Module是一样一一对应的

    1、Annotation方式注册

    //****** 1、通过BeeHiveService宏进行Annotation标记
    BeeHiveService(HomeServiceProtocol,BHViewController)
    
    //****** 2、宏定义
    #define BeeHiveService(servicename,impl) \
    class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
    
    //****** 3、转换后的格式,也是将其存储到特殊的段
    char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";
    
    

    2、读取本地plist文件

    • 首先同Module一样,需要先设置好路径
    [BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
    
    
    • 设置plist文件

    • 同样也是在setContext时注册services

    //加载services
    -(void)loadStaticServices
    {
        [BHServiceManager sharedManager].enableException = self.enableException;
    
        [[BHServiceManager sharedManager] registerLocalServices];
    
    }
    👇
    - (void)registerLocalServices
    {
        NSString *serviceConfigName = [BHContext shareInstance].serviceConfigName;
        //获取plist文件路径
        NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
        if (!plistPath) {
            return;
        }
    
        NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];
    
        [self.lock lock];
        //遍历并存储到allServicesDict中
        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}];
            }
        }
        [self.lock unlock];
    }
    
    

    3、load方法注册

    在Load方法里面注册Protocol协议,主要是调用BeeHive里面的registerService:service:完成protocol的注册

    + (void)load
    {
       [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]];
    }
    👇
    - (void)registerService:(Protocol *)proto service:(Class) serviceClass
    {
        [[BHServiceManager sharedManager] registerService:proto implClass:serviceClass];
    }
    
    

    到此,三种方式就创建完成了

    Protocol的获取

    ProtocolModule的区别在于,ProtocolModule多了一个方法,可以返回Protocol实例对象

    - (id)createService:(Protocol *)proto;
    {
        return [[BHServiceManager sharedManager] createService:proto];
    }
    👇
    - (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 {
        if (!serviceName.length) {
            serviceName = NSStringFromProtocol(service);
        }
        id implInstance = nil;
        //判断protocol是否已经注册过
        if (![self checkValidService:service]) {
            if (self.enableException) {
                @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil];
            }
    
        }
    
        NSString *serviceStr = serviceName;
        //如果有缓存,则直接从缓存中获取
        if (shouldCache) {
            id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
            if (protocolImpl) {
                return protocolImpl;
            }
        }
        //获取类后,然后响应下层的方法
        Class implClass = [self serviceImplClass:service];
        if ([[implClass class] respondsToSelector:@selector(singleton)]) {
            if ([[implClass class] singleton]) {
                if ([[implClass class] respondsToSelector:@selector(shareInstance)])
                    //创建单例对象
                    implInstance = [[implClass class] shareInstance];
                else
                    //创建实例对象
                    implInstance = [[implClass alloc] init];
                if (shouldCache) {
                    //缓存
                    [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
                    return implInstance;
                } else {
                    return implInstance;
                }
            }
        }
        return [[implClass alloc] init];
    }
    
    

    createService会先检查Protocol协议是否是注册过的。然后接着取出字典里面对应的Class,如果实现了shareInstance方法,那么就创建一个单例对象,如果没有,那么就创建一个实例对象。如果还实现了singleton,就能进一步的把implInstanceserviceStr对应的加到BHContextservicesByName字典里面缓存起来。这样就可以随着上下文传递了

    • 进入serviceImplClass实现,从这里可以看出 protocol和类是通过字典绑定的,protocol作为keyserviceImp(类的名字)作为value
    - (Class)serviceImplClass:(Protocol *)service
    {
        //通过字典将 协议 和 类 绑定,其中协议作为key,serviceImp(类的名字)作为value
        NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
        if (serviceImpl.length > 0) {
            return NSClassFromString(serviceImpl);
        }
        return nil;
    }
    
    

    Module & Protocol

    这里简单总结下:

    • 对于Module:数组存储

    • 对于Protocol:通过字典将protocol与类进行绑定,keyprotocolvalueserviceImp即类名

    辅助类

    • BHConfig类:是一个单例,其内部有一个NSMutableDictionary类型的config属性,该属性维护了一些动态的环境变量,作为BHContext的补充存在

    • BHContext类:是一个单例,其内部有两个NSMutableDictionary的属性,分别是modulesByNameservicesByName。这个类主要用来保存上下文信息的。例如在application:didFinishLaunchingWithOptions:的时候,就可以初始化大量的上下文信息

    //保存信息
    [BHContext shareInstance].application = application;
    [BHContext shareInstance].launchOptions = launchOptions;
    [BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";//可选,默认为BeeHive.bundle/BeeHive.plist
    [BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
    
    
    • BHTimeProfiler类:用来进行计算时间性能方面的Profiler

    • BHWatchDog类:用来开一个线程,监听主线程是否堵塞

    参考链接

    相关文章

      网友评论

        本文标题:iOS 组件化(一)常见方案解析

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