组件化分发生命周期

组件化分发生命周期 方案实现探索系列文章:
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
2
3
4
5
6
7
8
9
10
11
+ (void)load {
__block id observer =
[[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationDidFinishLaunchingNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
[self setup]; // Do whatever you want
[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];
}

很巧妙的方法,优点很明显:轻量!虽然侵入了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (void)execCategorySelector:(SEL)selector forClass:(Class)class withParam1:(id)param1 param2:(id)param2 {
BOOL isFirst = NO;
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(class, &methodCount);
for (int i = methodCount - 1; i >= 0; i--) {
Method method = methods[i];
SEL sel = method_getName(method);
if ([NSStringFromSelector(sel) isEqualToString:NSStringFromSelector(selector)]) {
if (!isFirst) {
isFirst = YES;
} else {
IMP imp = method_getImplementation(method);
void (*func)(id, SEL,id,id) = (void *)imp;
func(self, sel, param1, param2);
}
}
}
free(methods);
}

4.2)为了写法方便和统一,这里给第二步也提供了执行 AppDelegate 本类方法的实现。在 AppDelegate 的方法列表里寻找同名的的最后一个 SEL,执行对应的 IMP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (void)execClassSelector:(SEL)selector forClass:(Class)class withParam1:(id)param1 param2:(id)param2 {
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(class, &methodCount);
for (int i = methodCount - 1; i >= 0; i--) {
Method method = methods[i];
SEL sel = method_getName(method);
if ([NSStringFromSelector(sel) isEqualToString:NSStringFromSelector(selector)]) {
IMP imp = method_getImplementation(method);
void (*func)(id, SEL,id,id) = (void *)imp;
func(self, sel, param1, param2);
break;
}
}
free(methods);
}

这样就能调用到所有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (instancetype)init {
self = [super init];
if (self) {
NSArray *stringArray = [self _findAllSubClass:[GHLLifecycle class]];
self.subClasses = [self _classArrayWithStringArray:stringArray];
}
return self;
}

- (NSArray *)_classArrayWithStringArray:(NSArray *)stringArray {
NSMutableArray *classArray = [NSMutableArray new];
[stringArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
Class cls = NSClassFromString(obj);
if (cls) [classArray addObject:[cls new]];
}];
return [classArray copy];
}

- (NSArray *)_findAllSubClass:(Class)class {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
NSMutableArray *array = [NSMutableArray new];
// 获取所有已注册的类
Class *classes = (Class *)malloc(sizeof(Class) * count);
objc_getClassList(classes, count);

for (int i = 0; i < count; i++) {
if (class == class_getSuperclass(classes[i])) {
[array addObject:[NSString stringWithFormat:@"%@", classes[i]]];
}
}
free(classes);
return array;
}

消息转发过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
- (BOOL)_containsProtocolMethod:(SEL)selector {

unsigned int outCount = 0;
struct objc_method_description *methodDesc = protocol_copyMethodDescriptionList(@protocol(UIApplicationDelegate), NO, YES, &outCount);
for (int idx = 0; idx < outCount; idx++) {
if (selector == methodDesc[idx].name) {
free(methodDesc);
return YES;
}
}
free(methodDesc);
return NO;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
if ([self.realDelegate respondsToSelector:aSelector]) {
return YES;
}

for (GHLLifecycle *module in self.subClasses) {
if ([self _containsProtocolMethod:aSelector] && [module respondsToSelector:aSelector]) {
return YES;
}
}

return [super respondsToSelector:aSelector];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
if (![self _containsProtocolMethod:aSelector] && [self.realDelegate respondsToSelector:aSelector]) {
return self.realDelegate;
}
return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
struct objc_method_description methodDesc = protocol_getMethodDescription(@protocol(UIApplicationDelegate), aSelector, NO, YES);

if (methodDesc.name == NULL && methodDesc.types == NULL) {
return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
}

return [NSMethodSignature signatureWithObjCTypes:methodDesc.types];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSMutableArray *allModules = [NSMutableArray arrayWithObjects:self.realDelegate, nil];
[allModules addObjectsFromArray:self.subClasses];

// BOOL 型返回值特殊处理
if (anInvocation.methodSignature.methodReturnType[0] == 'B') {
BOOL realReturnValue = NO;

for (GHLLifecycle *module in allModules) {
if ([module respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:module];

BOOL returnValue = NO;
[anInvocation getReturnValue:&returnValue];

realReturnValue = returnValue || realReturnValue;
}
}

[anInvocation setReturnValue:&realReturnValue];
} else {
for (GHLLifecycle *module in allModules) {
if ([module respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:module];
}
}
}
}

- (void)doNothing {

}

无侵入、无注册,个人感觉还是比较完美的。虽然最后只是基于青木的方案做了免注册的优化,但是思考过程中的其他方案也是值得分享的!

以上,就是总结的所有组件化分发生命周期的方案了。如果你还有其他更好方案,欢迎讨论!
所有的实践都在 Demo 里了: GHLShareLifecycle

2020.03.08 更新:

本以为上一种方案已经很完美了,但是在项目中应用的时候还是发现了问题。最新解决方案见:组件化分发生命周期 - AOP 方案