组件化分发生命周期 方案实现探索系列文章:
1、组件化分发生命周期
2、组件化分发生命周期 - AOP 方案
3、组件化分发生命周期 - AOP 方案(libffi 实现)
是什么
组件化分发生命周期是什么?就是将主工程的生命周期分发到各个组件里去。直观些的介绍则是:AppDelegate 遵循并实现了 UIApplicationDelegate 代理,其中包括 willFinishLaunchingWithOptions:
、didFinishLaunchingWithOptions:
、applicationWillEnterForeground:
、applicationDidEnterBackground:
等等方法,包含了主工程的各个阶段将执行的方法,我们要做的就是在主工程的这些阶段方法被执行的时候,各个组件里相对应的阶段方法同时会被执行,这样,主工程和各个组件便共享了生命周期,
为什么
至于为什么要将主工程的生命周期分发到各个组件中,原因有以下几点:
1、替换 load 方法
因为 load 方法时机较早,所有很多时候会在 load 方法里执行注册,初始化等操作,但这也会导致 load 方法的滥用,将一些本可以靠后执行的操作提前执行了,可能引发 APP 启动耗时过长的问题,需要做 load 耗时监测,治理起来困难,所以很多团队是禁用 load 方法的。
将这些操作方法放到生命周期方法里去做显然更好,寻找合理的时机执行相应的操作,耗时检测功能也比较好做。
2、解决 AppDelegate 臃肿问题
工程中难免有一系列的注册、初始化操作,比如:APP 性能检测、bug 收集、打点等一系列工具的注册;各种基础组件涉及的初始化或重置操作。
将这些操作放到组件自己的生命周期方法里去执行,避免了 AppDelegate 的臃肿,而且各基础组件与主工程解耦,开发维护更方便。
3、Debugger 类组件可插拔
某些 Debugger 类组件在工作前可能需要注册操作,将注册操作放在 Pod 自己的生命周期里。这样一来,对于 Debugger 类组件只需要在 Podfile 里控制加载形式,即可做到 Debug/Release 环境组件可插拔,如:
1 | pod 'DebuggerKit', :configurations => ['Debug'] |
怎么做
相比于将 AppDelegate 里的所有阶段方法分发出去,先介绍两种相对轻量的做法,也能做到和分发生命周期类似的能力:
1、sunnyxx 的 Notification Once
巧妙的通知注册
1 | + (void)load { |
很巧妙的方法,优点很明显:轻量!虽然侵入了 load 方法,不过如果没有 load 的滥用的话也可以接受,毕竟只是在 load 里执行了注册行为,具体的执行时机还是 UIApplicationDidFinishLaunchingNotification
缺点是该方法的响应是在 - application:didFinishLaunchingWithOptions:
调用完成后发送,时机没法精确控制,因为有的时候因为时机问题,我们想让各种 Pod 里的注册操作在 AppDelegate 的 didFinishLaunchingWithOptions:
方法靠前执行,即先执行组件里的注册操作再执行 AppDelegate 里的操作。
参考原文:Notification Once
2、美团的 Kylin 注册函数
在编译时把数据(如函数指针)写入到可执行文件的 __DATA 段中,在运行时的某个阶段(如 willFinishLaunch)再从 __DATA 段取出数据进行相应的操作(调用函数)。
Kylin实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个 {key(key代表不同的启动阶段), *pointer} 对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。
这种方案呢,最好去写个专门的工具,如 Kylin,去实现 {key, *pointer} 对的注册和调用操作。对于使用方来说,添加一种函数执行时,使用 Kylin 注册,并在 AppDelegate 的合理阶段调起方法。
参考原文:美团外卖iOS App冷启动治理
除了以上方法之外还有一些比较“大型”的做法就是把 AppDelegate 的生命周期完整的分发出去:
3、手动注册、遍历分发
这是我之前公司的做法,提前注册 Lifecycle 类(可实现 AppDelegate 的各阶段方法),在 AppDelegate 各阶段方法执行的同时遍历 lifecycle 类执行相应方法。具体的做法是,
1)项目中存在一份配置文件,文件里配置着各个 pod 的 Lifecycle 类名,该类里实现了 AppDelegate 的某几个阶段方法。
2)项目启动的时候加载这份配置文件,根据类名反射成 Lifecycle 类,将所有的类添加到一个数组中(LifecycleArray)。
3)在 AppDelegate 和 UIResponder 的继承中间加一个 MyAppDelegate 类(GHLAppDelegate : MyAppDelegate : UIResponder),该类拥有 AppDelegate 的所有方法,在每个阶段的方法里遍历 LifecycleArray 数组,调用各个 Lifecycle 类的本阶段方法。
4)在 AppDelegate 的各阶段方法里首先调用一下 super 方法。
这样,在 AppDelegate 各阶段执行的时候就会执行父类方法,遍历所有 pod 里的 Lifecycle 类,执行相应方法,从而实现生命周期的分发。
这种做法的优点是没什么骚操作(姑且算优点吧),都是基本方法遍历调用,就一个反射操作也算常用吧。弊端就显而易见了:
1)需要注册行为。每添加一个 pod,想要为该 Pod 配置生命周期管理类的话都要去配置文件里注册一次。虽然项目稳定下来后 pod 基本不会变动,但使用起来总归不够理想,而且因为配置文件的存在,这种中心化的写法会导致代码臃肿,阅读维护困难。
2)侵入 AppDelegate 类。需要更改 AppDelegate 的父类,并且在 AppDelegate 的各阶段里调用 super。
4、Category 覆盖、追加方法⚠️
因为上一种方案中存在的问题,所以我在想怎么做既可以不用注册,又不用侵入 AppDelegate 呢?Category!我想到这种方案:
1)新建 Lifecycle 类,用于向相应的 pod(第三步提到的建过分类的 Pod) 分发生命周期。该类拥有 AppDelegate 的所有生命周期方法。
2)为了不侵入 AppDelegate,给 AppDelegate 添加分类(AppDelegate+Lifecycle),用于在相应阶段调用 Lifecycle 相应方法。在该分类里重写 AppDelegate 的各阶段方法,在各个方法里分别调用 Lifecycle 类的对应方法(这里其实也可以在分类里 hook 本类的方法来实现,但是为了写法方便 Demo 里的做法是使用了第四步提供的方法:遍历方法列表找到最后一个方法执行)。
3)对使用方来说,只需要在自己的 pod 里新建 Lifecycle 类的分类(如:Lifecycle + Home、Lifecycle + Deatil 等),复写本类的方法,即 AppDelegate 的生命周期方法,这些 pod 里的这些分类的这些方法会被 Lifecycle 类全部执行。
4)怎么全部执行呢?多个分类会根据加载顺序互相覆盖方法,正常情况下只执行最后加载的那个分类的方法,因为最后加载的分类的方法被最后加到方法列表里,消息发送过程中最先被找到。解决思路是:找到那些被覆盖的方法去执行对应的 IMP:
4.1)在 Lifecycle 本类里去遍历本类的方法列表,为了避免无限循环,除了本类的方法(即方法列表的最后一个),对其他的同名 SEL 都执行对应 IMP:
1 | + (void)execCategorySelector:(SEL)selector forClass:(Class)class withParam1:(id)param1 param2:(id)param2 { |
4.2)为了写法方便和统一,这里给第二步也提供了执行 AppDelegate 本类方法的实现。在 AppDelegate 的方法列表里寻找同名的的最后一个 SEL,执行对应的 IMP。
1 | + (void)execClassSelector:(SEL)selector forClass:(Class)class withParam1:(id)param1 param2:(id)param2 { |
这样就能调用到所有 Category 的生命周期方法,起到分发的效果。
优点即是:
1)对使用方来说不必注册,只需创建 Lifecycle 的分类
2)不侵入 AppDelegate 代码
缺点是:
1)要手动在主工程创建 AppDelegate 分类。
2)所添加的分类里的同名方法不会被覆盖,有反常识。
3)还有个缺点,也是为什么在标题上加一个 ⚠️ 的原因,是因为给一个类添加多个 Category,并分别覆盖本类方法时 Xcode 会提示 warning
Category is implementing a method which will also be implemented by its primary class
5、消息转发
因为上一种方案中存在的问题,我在想还有什么更好的方案,只提供一个 Pod 组件,就可以完成所有操作的方案。然后找到了青木同学的 组件化之组件生命周期管理 这篇文章,实现方案在文章里已经讲的很详细了,思路就是:
1)新建 Module 类,提供注册功能,并且可以设置优先级。使用方在自己的 Pod 继承该类创建自己的生命周期管理类,并且在 load 方法调用 Module 类的注册方法。
2)新建 UIApplication 的分类:UIApplication (Module)
,hook 掉 setDelegate: 方法,将代理设置给自己创建的类:ApplicationDelegateProxy。同时对包含所有注册类的数组根据优先级进行排序。
3)ApplicationDelegateProxy 类里不会实现 AppDelegate 里的那些方法,所以当系统来调用这些方法的时候,因为找不到 SEL 会进入消息转发过程:
3.1) -respondsToSelector:
:系统内部会调用这个方法。
判断是否实现了对应的 UIApplicationDelegate 代理方法。重写该方法结合 AppDelegate 以及所有注册的 Module 判断是否有相应实现。
3.2)-forwardingTargetForSelector:
: -respondsToSelector:
返回 YES ,便进入消息转发阶段,消息转发的第二步就是该方法。
判断要转发的方法是否为 UIApplicationDelegate 的代理方法,如果不是,并且 AppDelegate 能响应,把消息转发给 AppDelegate 去处理。
3.3)-methodSignatureForSelector:
和 -forwardInvocation:
:如果消息没有发给 AppDelegate,由自己来处理,将会这执行这些方法。
在这一步首先根据协议直接返回代理方法的签名,然后在 -forwardInvocation:
方法中,按照优先级,依次把消息转发给注册的模块。
3.4)消息转发中处理返回值为 BOOL 类型的情况。
这样就通过消息转发完成了生命周期的分发。已经是很不错的实现了,对外部文件没有侵入,唯一的缺点就是需要一个注册操作,而且还是在 load 方法里。
6、最后的优化
我在想有什么方法可以去掉这个注册操作?如果我们让所有组件里控制生命周期的类都继承自 Lifecycle 类,那么我们通过获取 Lifecycle 的所有子类就能够完成注册操作了。思路很简单:通过 runtime 获取所有注册的类,遍历这些类判断其父类是否是 Lifecycle 类。
照着这样的思路又实现了一份优化过的代码,收集注册子类:
1 | - (instancetype)init { |
消息转发过程:
1 | - (BOOL)_containsProtocolMethod:(SEL)selector { |
无侵入、无注册,个人感觉还是比较完美的。虽然最后只是基于青木的方案做了免注册的优化,但是思考过程中的其他方案也是值得分享的!
以上,就是总结的所有组件化分发生命周期的方案了。如果你还有其他更好方案,欢迎讨论!
所有的实践都在 Demo 里了: GHLShareLifecycle
2020.03.08 更新:
本以为上一种方案已经很完美了,但是在项目中应用的时候还是发现了问题。最新解决方案见:组件化分发生命周期 - AOP 方案