组件化分发生命周期 - AOP 方案

在之前的文章( 组件化分发生命周期 )里已经介绍过了 组件化分发生命周期 的概念及理由,并总结了几种分发生命周期的方案,最后一节的方案本以为已经很完美了,但是在项目中实际应用的时候还是发现了问题。

问题

因为我们工程里用了 Firebase 打点,其中 Firebase 依赖的一个库 GoogleUtilities 也对 UIApplication 的 delegate 里的一些方法做了 hook 操作,对 APP 做了一些监听操作。以下是 GoogleUtilities 产生影响的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
+ (GULApplication *)sharedApplication {
if ([GULAppEnvironmentUtil isAppExtension]) {
return nil;
}
id sharedApplication = nil;
Class uiApplicationClass = NSClassFromString(kGULApplicationClassName);
if (uiApplicationClass &&
[uiApplicationClass respondsToSelector:(NSSelectorFromString(@"sharedApplication"))]) {
sharedApplication = [uiApplicationClass sharedApplication];
}
return sharedApplication;
}

其中 kGULApplicationClassName 就是 @"UIApplication"。因为在上一种方案里我们将系统的 delegate 重设成了我们自己的类:GHLApplicationDelegate,并通过消息转发的方式统一处理了原 AppDelegate 里的所有方法。
这样一来,Firebase 通过以上代码得到的 GULApplication 就变成了 GHLApplicationDelegate,再去 hook application:didFinishLaunchingWithOptions: 等方法的时候就找不到这些方法了,即 Firebase 失去了相应的功能。

这里是 Firebase 的场景,其他第三方库有相应 hook 操作的话也会有相同的问题,而我们在实际开发过程中没法避免其他库的 hook 操作,甚至可能察觉不到问题的存在,所以之前提到的方案是有问题的。

分析

这样一想,原本的方法的确存在设计上的问题,我们只顾着自己的 hook,影响了其他第三方对 AppDelegate 的 hook 操作,这显然不合理。

为了解决以上的问题,不得不再次寻找方案。重新梳理下我们的需求:

在主工程的 AppDelegate 里执行 UIApplicationDelegate 的一系列生命周期的方法的时候,相应的各个 Pod 里也能有对应方法同时被执行,且不影响其他第三方对 AppDelegate 的 hook 操作。例如:主工程 AppDelegate 里的 application:didFinishLaunchingWithOptions: 方法被执行时,各个 Pod 里也能有对应的 application:didFinishLaunchingWithOptions: 方法被执行。

那我们也可以 hook AppDelegate 的各阶段方法,遍历执行各个 Pod 里的对应方法之后,再执行下原本的方法。这样大家都保证各自的 hook 链不会断掉就 ok 了。

解决方案:AOP

想起 Aspects 这个库。或许,AOP 是一种不错的解决方案。思路如下:

1)首先还是采用上一篇文章(组件化分发生命周期)的方案,在各个 Pod 里通过继承 HoloLifecycle 类的方式让其子类拥有执行 UIApplicationDelegate 生命周期方法的能力。DEBUG 模式下收集 HoloLifecycle 的所有子类并存到 UserDefaults 里,RELEASE 模式下从 UserDefaults 里直接取出该数组

2)hook UIApplicationsetDelegate: 方法,拿到 AppDelegate

3)遍历 AppDelegate 的方法列表,判断是否是 UIApplicationDelegate 的协议方法

4)使用 AOP 的方法,对以上遍历加判断筛选拿到的方法一一进行 hook

5)在被 hook 的每个方法执行前,遍历 HoloLifecycle 的所有子类,执行同名方法

以上就是根据需求想到的解决步骤,唯一的关键就在最后一步,执行 HoloLifecycle 类里的方法时,因为入参不确定,performSelector:withObject: 方法肯定是不适用了,可以用 NSInvocation 的方法转发消息,参考 Aspects

大体思路就是:

1)通过 Aspects hook AppDelegate 方法,让被 hook 的方法不会被正常执行,而是走消息转发流程(Aspects 的思路)

2)在消息转发的最后一步 forwardInvocation: 方法里获取当前所 hook 方法的 NSInvocation 对象 A,在这个对象里存储着转发的方法名,参数值等信息

3)根据 HoloLifecycle 类对象(target)和想要分发的方法的方法名(selector),创建一个 NSInvocation 对象 B

4)遍历 NSInvocation 对象 A 的所有参数,把它们依次赋值给 NSInvocation 对象 B,并发送对象 B

遍历所有的 HoloLifecycle 类对象,循环执行以上步骤就可以了,这样就做到了 hook AppDelegate 各生命周期方法,在各方法执行前遍历执行 HoloLifecycle 类对象的对应同名方法,也就是:组件化分发生命周期 的最终效果。

一开始以为 Aspects 暴露的方法里没法获取所 hook 方法的 NSInvocation,所以花了些力气参考 Aspects 的源码实现了一套方案。能够正常工作之后才发现 Aspects 的 hook 方法的 block 里可以拿到 AspectInfo 这个对象,而这个对象持有的 originalInvocation 属性,就是所 hook 方法走 forwardInvocation: 方法时拿到的 NSInvocation。所以,最后借助 Aspects 的实现简单了很多,Aspects 已经完成了 hook 和返回 NSInvocation 的功能,我们主要完成以上 3)、4)步骤即可,以下是关键代码:

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
@implementation UIApplication (HoloLifecycle)

+ (void)load {
[self jr_swizzleMethod:@selector(setDelegate:) withMethod:@selector(_holo_setDelegate:) error:nil];
}

- (void)_holo_setDelegate:(id <UIApplicationDelegate>)delegate {
unsigned count = 0;
Method *methods = class_copyMethodList([delegate class], &count);
for (int i = 0; i < count; i++) {
Method method = methods[i];
SEL sel = method_getName(method);
struct objc_method_description methodDesc = protocol_getMethodDescription(@protocol(UIApplicationDelegate), sel, NO, YES);
if (methodDesc.name != NULL && methodDesc.types != NULL) {
[self _aspect_hookSelectorWithDelegate:delegate sel:sel];
}
}

[self _holo_setDelegate:delegate];
}

- (void)_aspect_hookSelectorWithDelegate:(id <UIApplicationDelegate>)delegate sel:(SEL)sel {
[(NSObject *)delegate aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {

for (HoloLifecycle *lifecycle in [HoloLifecycleManager sharedInstance].subClasses) {
if ([lifecycle respondsToSelector:sel]) {
NSMethodSignature *signature = [lifecycle methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = lifecycle;
invocation.selector = sel;

NSUInteger numberOfArguments = signature.numberOfArguments;
if (numberOfArguments > info.originalInvocation.methodSignature.numberOfArguments) {
NSLog(@"lifecycle has too many arguments. Not calling %@", info);
continue;
}

void *argBuf = NULL;
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
const char *type = [info.originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);

if (!(argBuf = reallocf(argBuf, argSize))) {
NSLog(@"Failed to allocate memory for lifecycle invocation.");
break;
}

[info.originalInvocation getArgument:argBuf atIndex:idx];
[invocation setArgument:argBuf atIndex:idx];
}

[invocation invoke];

if (argBuf != NULL) {
free(argBuf);
}
}
}
} error:nil];
}

@end

补充功能

1、参考青木的 TDFModuleKit,也给 HoloLifecycle 类添加了优先级方法,在子类里重写该方法返回合适的优先级即可决定执行顺序,
并且 HoloLifecycle 支持在 AppDelegate 方法之前、之后两阶段执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// 调用优先级
typedef NS_ENUM(NSInteger, HoloLifecyclePriority) {
// 晚于 AppDelegate 执行
HoloLifecyclePriorityAfterLow = 50,
HoloLifecyclePriorityAfterMedium = 150,
HoloLifecyclePriorityAfterHigh = 250,
// 早于 AppDelegate 执行
HoloLifecyclePriorityBeforeLow = 500,
HoloLifecyclePriorityBeforeMedium = 750, // default
HoloLifecyclePriorityBeforeHigh = 1000,
};


/// 调用优先级
/// 子类重写该方法 return 合适的优先级
/// AppDelegate 的优先级为 300,若 HoloLifecycle 子类同样定义为 300,则先于 AppDelegate 执行
/// @return 合适的优先级,默认为 HoloLifecyclePriorityBeforeMedium
+ (HoloLifecyclePriority)priority;

2、使用 HoloLifecycleManager 类的以下方法可以添加 log 功能,方便监控各个方法的调用顺序和耗时:

1
2
3
/// 打印所有 HoloLifecycle 子类执行方法及耗时
/// 在 + load 方法里调用该方法,以保证该方法早于 UIApplicationDelegate 方法调用
- (void)logSelectorsAndPerformTime;

代码已开源至:HoloLifecycle,欢迎使用。