今天看啥  ›  专栏  ›  JacobLJ

BeeHive 1.6.0 源码阅读探讨

JacobLJ  · 掘金  ·  · 2020-04-03 08:13
阅读 70

BeeHive 1.6.0 源码阅读探讨

因业务需要,故而有此阅读一举

基本类功能概念

  • 框架内部类总览

  • BHModule

    • 监听系统级别事件并执行事件的类(即用于处理APP生命周期事件及相关系统层的类)
    • BHModule 需要遵守 BHModuleProtocol(系统级别事件接口定义在 BHModuleProtocol)
    • 自行通过alloc init方式自行初始化的模组时不在BHModuleManager下被管理
  • BHContext

    • 管理应用环境、全局配置、事件触发上下文对象(即事件方法的上下文参数
    • 还包括 BHShortcutItemBHOpenURLItemBHNotificationsItemBHUserActivityItemBHWatchItem
  • BHModuleManager:负责【注册】模块 BHModule,并管理

    • BHModules 数组:
      • 存放已注册的 BHModule 实例(每一种 class 的 BHModule 实例只能有一个,注册时会进行过滤操作,isKindOfClass 则不再计入该实例)
      • 排序
        • 先按 basicModuleLeve (从小到大排,basic < normal)
        • 如果前面相等,通过 modulePriority 排(从大到小)
    • BHModuleInfos 字典数组:注册模组时创建的模组信息字典,字典包括模组class、模组level、是否已Instantiated等信息
    • BHSelectorByEvent 事件表字典:管理默认系统事件类型和自定义事件类型
    • BHModulesByEvent 事件-响应实例数组表字典:将能够响应相同类型事件的模组实例按 BHModules 数组的排序方法存放至对应数组中
  • BHServiceManager:管理各个已【注册】Protocol

Module的注册( 按启动时执行的顺序 )

1.Annotation方式注册

  • 功能对应类:BHAnnotation
  1. 通过使用宏 BeeHiveMod(类名) 将类名字符串,在编译阶段写入到指定名称的内存section中

在 ShopModule 中使用该宏,这样就能够在编译阶段将 ShopModule这个类名写入到 __DATA 段中并以 BeehiveMods 作为key标识位置

宏编译后的结果是

  1. 通过__attribute__((constructor))这个宏修饰方法void initProphet() {...},让该方法在main函数【前】被调用用

_dyld_register_func_for_add_image 表示每一次镜像的加载都会触发传入的dyld_callback回调函数

弊端是dyld_callback会回调多次

dyld_callback被触发时,内部会调用BHReadConfiguration方法

  1. 从 Mach-O section 中读取之前写入的类名字符数据(组合成字符串数组)

  1. 遍历类名字符串数组,通过 BHModuleManager 注册该类,[[BHModuleManager sharedManager] registerDynamicModule:cls];

2.Load方式注册

通过使用宏 BH_EXPORT_MODULE(isAsync) 方式,在类被载入内存时调用+load方法注册,实际内部调用的相同的方法[[BHModuleManager sharedManager] registerDynamicModule:cls];

3.读取本地Plist文件

BeeHive 实例首次设置其 BHContext类的属性context时,会触发本地plist的读取,加载对一个的模组字典信息,存放至 BHModuleManagerBHModuleInfos 数组中,然后注册模组

从demo上看,触发时机就是在delegate中了

Services的注册 (方式与Annotation的一样)

1.Annotation方式注册

1、编译宏

2、使用方式

3、编译后结果

4、从__DATA段中的BeehiveServices中读取内容结果,逻辑与Module的annotation方式是一摸一样的,区别是Module在内存段中存放的是一个字符串数组,而service存放的是一个字典数组

5、逐个读取,然后注册

2.Load方式注册

目前的框架库中,service的注册方式是没有了load方式注册的。至于为啥不提供了,我猜应该就是为了突出Module和Service这两层概念的区别吧

【Module】

1、Module主要是用来处理系统及APP声明周期等级别的基本操作单位,更类似于平时开发的三方库的抽象管理类,以及APP声明周期的抽象管理类,还有一些其他的更底层及迭代周期基本不变的那种管理类

2、根据第一点,所以Module出现是必须约定已经遵守了框架提供的一个协议BHModuleProtocol,从该协议定义接口来看(都是APPdelegate的代理转发接口),更加突出第一点的要求,就是Module属于处理系统层级的管理类

3、既然Module是底层型的管理类,那么它具备了load方式的注册方法来看也是合理的,毕竟底层的管理类就会有类加载时就要做一些其他操作的需要(这是从使用方向上的分析,比如Swizzle等行为)

【Service】

1、从注册时提供的内容看,不仅要提供对应的类,还要提供其使用的范围的接口protocol,明显就是一个业务层型的Module版,由于业务的迭代快特点,故而关联的protocol也是需要用户自己来维护才是合理的

2、由于service定位在业务上的操作管理类,则对load形式的需求明显就与Module型的类相提并论了,故而不提供load的注册方式也在情理中

3、还没想到😆、当然硬是要实现也是可以的,模仿Module的load方式即可

3.读取本地Plist文件

同Module的一样,触发时机都是首次对BeeHive这个单例设置context属性时

service与module的注册内容区别是,service(类)与protocol(接口)配对注册的,而module的注册只需要知道类名即可,这也是这两类组件使用上的区别

Module注册过程分析

1.Annotation方式注册

  1. 从内存段中获取到所有module名的字符串数据组后,遍历数组,然后逐个通过[[BHModuleManager sharedManager] registerDynamicModule:cls];注册

  1. [BHModuleManager sharedManager]通过单例形式使用

  1. registerDynamicModule:注册入口逻辑

  1. - (void)addModuleFromObject:(id)object shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent 进入真正的注册流程

参数防空及参数转存层

module必须遵守对应的协议

确定遵守协议,进入module类信息标记及初始化流程

补述:

  • 第1点:

    • 系统事件处理的module(也就是BHModule类)的层级按目前划分normal和basic,未响应方法basicModuleLevel的为normal(值为1)层级,否则为basic层级(值为1),但暂时还无法从给出的demo中区分出这两种层级的划分是作用是什么,但可以明确的是,这两种层级的划分对系统事件的响应顺序是有影响的,后续会进一步书名层级对module的响应排序
  • 第2点:

    • self.BHModuleInfos是一个模组信息字典的数组,存放每一module的类名称字符串、层级的值、是否已初始化等信息。至于是否已初始化这一标识,从annotation方式注册类来看,都会被标记成已初始化状态 * 这个self.BHModuleInfos是没有进一步排序的【是有排序的,只是放到了plist方式注册时触发而已】,与self.BHModules不同。self.BHModuleInfos是信息的存储列表而已,它也不会重复加入相同的module,因为前面已经有处理module的唯一性逻辑了
  • 第3点:

    • 就是将初始化的module实例加入至self.BHModules列表中

对self.BHModules实例列表进行层级排序

1、先按 basicModuleLeve (从小到大排,basic (0) < normal (1))

2、如果前面相等,通过modulePriority排(从大到小)

假设:

moduleA(basic)、
moduleB(normal)、
moduleC(basic)、
moduleD(modulePriority=1)、
moduleE(modulePriority=3)、
moduleF(modulePriority=2)

则,排序结果为:

moduleA、moduleC、moduleB、moduleE、moduleF、moduleD
复制代码

将模组实例注册至对应的事件处理列表集中,也就是说,将刚初始化的module实例按相应的系统事件进行归类,这样也是方便于后续如果触发一个系统事件,能够快速直接将消息转发至所有响应该事件的module实例中

将在下面第5小点详细说明这个逻辑

setup事件、init事件、splash事件的触发。当然annotation的注册方式默认不执行这些方法

  1. 详细说说[self registerEventsByModuleInstance:moduleInstance];处理逻辑

self.BHSelectorByEvent标识一个系统事件的字典表

归类module并进行排序操作

  1. 详细说说前面的三种事件setup事件、init事件、splash事件的触发逻辑
  • InitEvent

  • setup和splash的事件等其他event

  1. TearDownEvent

  1. 主动移除module 方法-unRegisterDynamicModule:

2.Load方式注册

使用对应的宏

宏展开后结果

从上可以知,该宏是重写了类的+ (void)load方法和实现了BHModuleProtocol的一个接口方法- (BOOL)async

load方发中调用的是BeeHive提供的便捷接口+ registerDynamicModule:,实际内部也就是调用BHModuleManager的方法[[BHModuleManager sharedManager] registerDynamicModule:moduleClass];

后续的流程及跟Annotation的注册module一样了。

至于响应的那个异步方法,在Annotation的注册module的最后流程中有说过,就是注册并管理该module后,会触发一次该module的三个事件setup/init/splash,其中init事件时会根据module实例是否响应了-async方法的返回值作为是否异步执行的判断依据

3.读取本地Plist文件

触发时机及位置

bundle的Plist内容

逐步说明一下内部处理的逻辑[[BHModuleManager sharedManager] loadLocalModules];

1、定位plist的路径,防空处理

2、根据路径获取plist内容,也就是一个数组

3、创建一个临时空字典(目的是用来在第5步中进行过滤判断)

4、将已有的module信息字典数组,将对应的类归入至临时字典中

原有self.BHModuleInfos中装的内容如下,这些都是通过annotation或load方式注册的模组信息
@[
    @{
           @"moduleLevel" : 0,
           @"moduleClass" : @"ModuleA",
           @"moduleHasInstantiated" : YES,
        },
    @{
            @"moduleLevel" : 1,
            @"moduleClass" : @"ModuleB",
            @"moduleHasInstantiated" : YES,
        },
]
复制代码

加入至临时字典后,字典中内容如下

    @{
        @"ModuleA" : 1,
        @"ModuleB" : 1,
    }
复制代码

5、过滤已有的模组内容,因为不允许处理系统事件的同名module存在,将对应的plist的module的信息字典对象,加入到self.BHModuleInfos中。 【注意】

  • annotation和load方式注册的module中,其对应的module信息字典中的是否已注册字段moduleHasInstantiated对应的值默认是YES的。而此时从plist中获取的module则是未创建的

  • plist中的module信息,还有一个独有的key值,即modulePriority,之前annotation和load方式注册的module时的排序都是通过该module的响应的该方法返回值作为排序依据的,这里则直接带有该排序权重的值

6、至此我们知道,self.BHModuleInfos中可能同时具备了annotation和load方式注册的module的信息,及plist中获得的未初始化过的module信息

继续说说[[BHModuleManager sharedManager] registedAllModules];逻辑

1、self.BHModuleInfos根据元素Module字典的两个key值进行排序

  • 分别是moduleLevel,modulePriority。实际上就是前面annotation方式注册流程中年说过的level层(basic或normal),以及priority(数值大小)排序。

  • 这里需要注意的点是,前面annotation和load方式注册的Module信息字典中是没有modulePriority这个key的(但有这个值的排序,只不多是通过响应方法来获取)。

  • 【从这点来看,我认为annotation和load一般都类为level层级的Module,而plist的都为priority层级。这是我的猜测,只不多目前这个框架已不更新多年了如果要继续考究的话,只能靠自己了😆】

2、将未初始化的Module进行初始化,这里指的也就是从plist方式注册的Module了,然后加入到临时数组中

3、将新初始化的Module实例数组并入至self.BHModules数组中

4、最后就是重复一次Module实例按事件类别进行归类了,这个逻辑在前面的annotation注册分析中也已经说过了

【至此可以清楚了解到BHModuleManager的内部内容都有啥了】

Service注册过程分析

1.Annotation方式注册

1、从内存段读取完 class-protocol 字典数组后,即遍历,然后逐个注册

2、class-protocol信息存储管理即为注册

3、存储信息时通过lock的方式防止多线程写入问题

  • 在Module的annotation中是没有的相关的线程问题的,为什么这里需要呢?

  • 答案就是:正因为这两者从概念上负责的范围层级不同造成的,Module是偏底层的,基本是程序启动时机会按各种依赖做好排序,并且分工明确。而Service则不同,它就是负责业务层的,可以同时具备多个Service实例也可以在运行过程中临时注册一个新的service,所以基于这种情况,出现线程问题就不足为奇了。

  • 既然有线程问题,那么lock的方式使用合理了吗?(后续再讨论😆)

4、注册逻辑是简单的,这里的事件触发都是在业务运行时发生的

2.Load方式注册

不存在load的注册方式

3.读取本地Plist文件

1、触发注册本地service

2、读取plist内容并注册(即存储class-protocol字典)

3、有重复可能的小问题

  • 这里的方式没有做防重处理,因为annotation和plist中是会存在class-protocol一样的情况的,其实如果是完全一样的话问题也是不大的,即使是重复,字典的读取时间复杂度也是O(1),但如果是classX-protocolX其中一个不一致则还会会有严重问题的

4、enableException 开启问题

  • BHServiceManager内部是有一个enableException的开关的,从上面代码截图可以看出,这是用于异常的提示的开关,这点但从BHServiceManager内看,我觉得无可厚非,但是纵观整个框架的看的话,为什么BHModuleManager中没有呢?这点就有点厚此薄彼的意思了,而且也对后续的异常处理层的扩展造成障碍,毕竟你看在只用于BHServiceManager内部的enableException开关接口竟然放到了BeeHive这个总管理类上来,从使用角度看,定义在总览的表头,应该表示用于全局的异常开关吧,可以目前的框架情况来看,只是局部使用而已。

【至此可以清楚了解到BHServiceManager的内部内容都有啥了】

Module的使用流程介绍

1、很明显,Module的使用基本都提现在了AppDelegate的生命周期函数中了,也就是系统层级事件的统一调度,触发对应的单个或多个Module执行对应的行为

2、从这里也可以看出,虽然从层面上做到了应用代理的解耦,但毕竟重写了整个AppDelegate的生命周期函数,就这一点的耦合性终究是无法避免的

3、具体的触发事件逻辑,在Module的Annotation注册流程分析中有说,这里就不再复述了

Service的使用流程介绍

1、我们知道,BHServiceManager中并没有管理对应的service实例,只是单纯的管理serviceClass-protocol这个键值字典而已,所以每一次需要使用对应的service实例是,就需要根据对应的protocol获取到对应的class然后创建该实例

2、创建流程分析

  • 第1:

    • 如果没有传入对应的protocol字符串,则从Protocol class中转出其字符串名称
    • 检查是否已注册,否则抛异常,也就是说service的使用时先注册后创建使用
  • 第2:

    • 根据方法参数shouldCache判断是否从缓存中读取。而这个缓存是在上下文单例[BHContext shareInstance] 只用维护的,这个也是一个字典,key:Protocol的字符串名,value:service实例。如果是通过缓存读取,则缓存中如果有该protocol对应的service实例则直接返回该实例

    • 问题来了,为啥是全局上下文context单例来维护这个service实例表呢?不仅如此,全局上下文context单例也同时维护一个module名称表,但并未启用而已

    • 我的猜测是:因为上下文是全局通用的,同时也是service间、module间以及service与module间的参数桥梁,很明显业务的层间的使用时复杂多变的,而将service实例表通过context来维护,明显是为后续各种交互提供便利,同样这一点也是耦合的原点,将来框架对业务的影响或者反过来业务对框架的影响都是无法预测的。

  • 第3:逻辑意图比较明显,如果用文字来说显得太冗余了,自己看图吧

    • service也有单例的概念,当然单例同样是可以被context缓存的

  • 其他问题

    • 代码风格不一致
    • -serviceImplClass方法感觉是多余的,protocol 从class转string又从string转class

BHAppDelegate

  • 让项目的 AppDelegate 类继承自 BHAppDelegate
  • BHAppDelegate 直接重载所有 AppDelegate 的方法
  • 在重载的方法里,通过 BHModuleManager 将该代理方法转发至,监听该事件的 BHModule 实例中

问题分析

应用代理解耦

对应操作对象是 BHModuleManager - BHModule

  • 虽然响应应用代理的模块具备代理事件调用顺序可控,但顺序不明显,需要一个一个类检查其排序等级

  • 模块间调用解耦 对应操作对象是 BHServiceManager - 遵守BHServiceProtocol的对象

  • url-ruouter解耦 对应操作对象是 BHRouter - BHServiceManager - 遵守BHServiceProtocol的对象

方向

可借鉴使用地方

1、在 AppDelegate 中启动时触发的三方库,通过用module方式进行封装

  • 增加debug难度,因为原来直观的代码逻辑被分散
  • 如果三方库间是具有信心的耦合交互的话,不容易实现。也就是BeeHive的module间交互是相对独立的

参考




原文地址:访问原文地址
快照地址: 访问文章快照