组件化之组件生命周期管理

在没有实行组件化的项目中,经常会在 AppDelegate 看到各类初始化代码,这一部分代码一般用以配置某些 key 以及 secret ,或者开启某些服务,常见的有第三方推送、统计分析、IM服务等。当然,也有可能是开启一些自身的服务,比如 log 日志、 数据库初始化等。当一个 App 达到一定体量后, 未经整理的 AppDelegate 可能会变得臃肿。那么在实行组件化之后,该如何处理这部分代码呢?

不管理组件生命周期

不对组件生命周期进行管理,那么只能继续将这些初始化代码放在主工程的 AppDelegate 中,而针对上文所说的 AppDelegate 臃肿的问题,也可以通过简单的封装来优化。

但是,这种做法会引发组件独立性问题。比如存在能独立运行的组件 A、B,B 依赖 A, A 生效需要在 App Launch 时调用配置代码 Code-A。如果采用上述做法,那么组件 A 所在示例工程的 AppDelegate 中,需要调用 Code-A 进行配置,而组件 B 因为依赖了 组件 A ,要使组件 B 能成功运行,也需要在 B 的示例工程添加 Code-A 进行配置。同样主工程的 AppDelegate 中也存在一份 Code-A 配置代码。可以看到,这种重复手动配置的做法是比较繁琐和难看的,这也是为什么要对组件生命周期进行管理的原因。

现有实现管理方案

从组件和主工程的关系切入,既然组件需要在 App 生命周期的某些阶段处理特定的事务,那么就提供特定的回调方法供组件使用。 App 生命周期各个阶段产生的事件,可以通过让 AppDelegate 遵守 UIApplicationDelegate 协议并实现不同的代理方法进行捕获。

要想把当前阶段 App 产生的事件分发给各个组件,最简单的方案就是如 limboy 所说,在 AppDelegate 的各个代理方法里,手动调一遍组件的对应方法,如果组件实现了对应的代理方法,就执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[MGJApp startApp];

[[ModuleManager sharedInstance] loadModuleFromPlist:[[NSBundle mainBundle] pathForResource:@"modules" ofType:@"plist"]];
NSArray *modules = [[ModuleManager sharedInstance] allModules];
for (id<ModuleProtocol> module in modules) {
if ([module respondsToSelector:_cmd]) {
[module application:application didFinishLaunchingWithOptions:launchOptions];
}
}

[self trackLaunchTime];
return YES;
}

不过这种方式缺点也很明显,组件需要依赖主工程的 AppDelegate 是否实现了 UIApplicationDelegate 的代理方法,如果没有的话,即使组件方实现了对应的代理方法,依然无法捕获到事件。

再来看下 caojun 的处理方案 YTXModule
这个方案主要思路是通过 runtime method swizzling,替换 AppDelegate 中实现的 UIApplicationDelegate 代理方法,然后在 swizzled method 中,执行事件分发。 YTXModule 提供了一些宏定义,精简了方法替换流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@implementation UIApplication (YTXModule)
- (void)module_setDelegate:(id<UIApplicationDelegate>) delegate
{

static dispatch_once_t delegateOnceToken;
dispatch_once(&delegateOnceToken, ^{
SWIZZLE_DELEGATE_METHOD(applicationDidFinishLaunching:);
...
});
[self module_setDelegate:delegate];
}
@end

@implementation YTXModule
...
+ (BOOL)ytxmodule_application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
DEF_APPDELEGATE_METHOD_CONTAIN_RESULT(application, launchOptions);
}
...
@end

这里需要注意的是,由于 method swizzling 是在不同类型载体(AppDelegate对象 <-> YTXModule类)间交换的方法,所以会造成在 +ytxmodule_applicationDidFinishLaunching: 中调用 self 时,获取的并不是 YTXModule类,而是 AppDelegate对象,因为方法替换实际上替换了 IMP,并没有改变实参,参照 objc_msgSend(id self, SEL op, ... ) 的参数排列,可以明确第一个参数是消息接收者,也就是 AppDelegate对象。通过上述分析可以知道,如果直接进行方法替换,不做特殊处理,使用以下代码将会抛出 unrecognized selector 异常 :

1
2
3
4
5
+ (BOOL)ytxmodule_application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
// self is AppDelegate instance
[self ytxmodule_application:application didFinishLaunchingWithOptions:launchOptions];
}

而以下代码,是可以正常运行的:

1
2
3
4
+ (BOOL)ytxmodule_application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
[YTXModule ytxmodule_application:application didFinishLaunchingWithOptions:launchOptions];
}

所以 caojun 在方法替换时,给 AppDelegate 添加了相同命名的实例方法,规避了这个异常 :

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
void Swizzle(Class class, SEL originalSelector, Method swizzledMethod)
{
Method originalMethod = class_getInstanceMethod(class, originalSelector);
SEL swizzledSelector = method_getName(swizzledMethod);

BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod && originalMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
// 这一步给 AppDelegate 添加相同命名的实例方法,并且其 IMP 是 AppDelegate 自身方法的原实现
class_addMethod(class,
swizzledSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
}

虽说这种方案也能实现事件的分发,但是在不同类型载体间使用 method swizzling 还是应该避免的,对其他开发者不是很友好。并且这种方案也存在依赖 YTXModule 是否替换了 UIApplicationDelegate 的代理方法问题,如果没有,组件方是无法捕获事件的。

一种更加优雅的方案

分发、代理

看到这两个关键词,可以直接联想到 runtime 的另一重要组成部分,消息转发。以下是我结合消息转发实现的组件生命周期管理方案。

先看下 UML 类图:

首先是 TDFModule ,模块基类,所有想要捕获 App 生命周期事件的模块都需要创建一个继承 TDFModule 的类,并且遵守 TDFModuleProtocol 协议:

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

/**
模块子类必须遵守此协议
*/
@protocol TDFModuleProtocol <UIApplicationDelegate>
@end

/**
模块优先级
- TDFModulePriorityVeryLow: 极底
- TDFModulePriorityLow: 低
安排给弱业务,业务模块

- TDFModulePriorityMedium: 中
- TDFModulePriorityHigh: 高
- TDFModulePriorityVeryHigh: 极高
安排给基础模块(有些基础模块每次依赖都需要手动调用初始化方法,建议分成 Core / Initializer subspec,后者中只有一个类继承TDFModule,这样直接依赖模块时,初始化代码的编写就可以去掉了)
这种情况下,TDFModule子类中,最好不要存在硬编码,使用变量或配置文件配置,这样才能让各业务线通用
*/
typedef NS_ENUM(NSInteger, TDFModulePriority) {
TDFModulePriorityVeryLow = 25,
TDFModulePriorityLow = 50,
TDFModulePriorityMedium = 100,
TDFModulePriorityHigh = 150,
TDFModulePriorityVeryHigh = 175,
};

@interface TDFModule : NSObject
+ (instancetype)module;

/**
在 load 中调用,以注册模块
*/
+ (void)registerModule;

/**
模块优先级

主工程模块的调用最先进行,剩余附属模块,
内部会根据优先级,依次调用 UIApplicationDelegate 代理
默认是 TDFModulePriorityMedium

@return 优先级
*/
+ (TDFModulePriority)priority;
@end



@implementation TDFModule
- (instancetype)init {
if (self = [super init]) {
if (![self conformsToProtocol:@protocol(TDFModuleProtocol)]) {
@throw [NSException exceptionWithName:@"TDFModuleRegisterProgress" reason:@"subclass should confirm to <TDFModuleProtocol>." userInfo:nil];
}
}

return self;
}

+ (instancetype)module {
return [[self alloc] init];
}

+ (void)registerModule {
[TDFModuleManager addModuleClass:self];
}

+ (TDFModulePriority)priority {
return TDFModulePriorityMedium;
}

子类需要在 +load 方法中调用 registerModule 才能让模块具备捕获 App 事件的能力。因为 TDFModuleProtocol 直接遵守的 UIApplicationDelegate 协议,子类可以和 AppDelegate 一样,直接实现自己感兴趣的代理方法即可:

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
@interface TDFAModule : TDFModule <TDFModuleProtocol>
@end

@implementation TDFAModule
+ (void)load {
[self registerModule];
}

+ (TDFModulePriority)priority {
return TDFModulePriorityHigh;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"%@, %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
return YES;
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
NSLog(@"%@, %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
NSLog(@"%@, %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
}

- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(@"%@, %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
}

@end

以上就是一个简单的使用示例。

接下来是 TDFModuleManager ,模块管理类。这个单例类主要负责模块的储存,以及在 UIApplication 的 -setDelegate: 中,把原来 delegate 替换成自己的 delegate proxy 。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@interface TDFModuleManager : NSObject {
@package
TDFApplicationDelegateProxy *_proxy;
}
@property (strong, nonatomic, readonly) TDFApplicationDelegateProxy *proxy;
@property (strong, nonatomic, readonly) NSArray <TDFModule *> *modules;

+ (instancetype)shared;
+ (void)addModuleClass:(Class)cls;
+ (void)removeModuleClass:(Class)cls;
@end


static NSMutableArray const * TDFModuleClassArray = nil;

@implementation TDFModuleManager
+ (instancetype)shared {
static dispatch_once_t onceToken;
static TDFModuleManager *singleton = nil;
dispatch_once(&onceToken, ^{
singleton = [[self alloc] init];
});
return singleton;
}

+ (void)addModuleClass:(Class)cls {
NSParameterAssert(cls && [cls isSubclassOfClass:[TDFModule class]]);

if (!TDFModuleClassArray) {
TDFModuleClassArray = [NSMutableArray array];
}

if (![TDFModuleClassArray containsObject:cls]) {
[TDFModuleClassArray addObject:cls];
}
}

+ (void)removeModuleClass:(Class)cls {
[TDFModuleClassArray removeObject:cls];
}

- (void)generateRegistedModules {
[self.mModules removeAllObjects];

[TDFModuleClassArray sortUsingDescriptors:@[[[NSSortDescriptor alloc] initWithKey:@"priority" ascending:NO]]];

for (Class cls in TDFModuleClassArray) {
TDFModule *module = [cls module];
NSAssert(module, @"module can't be nil of class %@", NSStringFromClass(cls));

if (![self.mModules containsObject:module]) {
[self.mModules addObject:module];
}
}
}

- (TDFApplicationDelegateProxy *)proxy {
if (!_proxy) {
_proxy = [[TDFApplicationDelegateProxy alloc] init];
}

return _proxy;
}

- (NSArray<TDFModule *> *)modules {
return (NSArray<TDFModule *> *)self.mModules;
}

- (NSMutableArray<TDFModule *> *)mModules {
if (!_mModules) {
_mModules = [NSMutableArray array];
}

return _mModules;
}
@end

static void MCDSwizzleInstanceMethod(Class cls, SEL originalSelector, Class targetCls, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(targetCls, swizzledSelector);
BOOL didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

@implementation UIApplication (TDFModule)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
MCDSwizzleInstanceMethod(self, @selector(setDelegate:), self, @selector(mcd_setDelegate:));
});
}

- (void)mcd_setDelegate:(id <UIApplicationDelegate>)delegate {
TDFModuleManager.shared.proxy.realDelegate = delegate;
[TDFModuleManager.shared generateRegistedModules];

[self mcd_setDelegate:(id <UIApplicationDelegate>)TDFModuleManager.shared.proxy];
}
@end

最后是这个方案的重点,也就是 TDFApplicationDelegateProxy 类:

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
78
79
80
81
82
83
84
85
@interface TDFApplicationDelegateProxy : NSObject
@property (strong, nonatomic) id <UIApplicationDelegate> realDelegate;
@end

@implementation TDFApplicationDelegateProxy
- (Protocol *)targetProtocol {
return @protocol(UIApplicationDelegate);
}

- (BOOL)isTargetProtocolMethod:(SEL)selector {
unsigned int outCount = 0;
struct objc_method_description *methodDescriptions = protocol_copyMethodDescriptionList([self targetProtocol], NO, YES, &outCount);

for (int idx = 0; idx < outCount; idx++) {
if (selector == methodDescriptions[idx].name) {
return YES;
}
}
free(methodDescriptions);

return NO;
}

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

for (TDFModule *module in [TDFModuleManager shared].modules) {
if ([self isTargetProtocolMethod:aSelector] && [module respondsToSelector:aSelector]) {
return YES;
}
}

return [super respondsToSelector:aSelector];
}

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
struct objc_method_description methodDescription = protocol_getMethodDescription([self targetProtocol], aSelector, NO, YES);

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

return [NSMethodSignature signatureWithObjCTypes:methodDescription.types];;
}

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

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

for (TDFModule *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 (TDFModule *module in allModules) {
if ([module respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:module];
}
}
}
}

- (void)doNothing {}
@end

先说 -respondsToSelector: ,由于系统内部会调用这个方法,判断是否实现了对应的 UIApplicationDelegate 代理方法,所以这里结合 AppDelegate 以及所有注册的 Module 判断是否有相应实现。

-respondsToSelector: 返回 YES 后,程序来到消息转发第二步 Fast forwarding path ,对应方法 -forwardingTargetForSelector:,在这一步,我们判断转发的方法是否为 UIApplicationDelegate 的代理方法,如果不是,并且 realDelegate(也就是 AppDelegate) 能响应,就直接把消息转发给 realDelegate。

如果在上一步中没有把消息转发给 realDelegate,那么就到了消息转发的最后一步 Normal forwarding path ,对应方法 -methodSignatureForSelector:-forwardInvocation:,在这一步我们首先根据协议直接返回代理方法的签名,然后在 -forwardInvocation: 方法中,按照优先级,依次把消息转发给注册的模块。

在不做额外操作的前提下, -forwardInvocation: 中只有最后一次调用的返回值会成为实际返回值,当实现类似 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options 等返回 BOOL 值的代理方法时,就会出现问题。所以这里通过判断返回值是否为 BOOL 类型,去执行不同的操作。如果为 BOOL 类型,则对所有返回值执行逻辑或操作,并将结果设置成实际返回值。

总结起来,流程如下:

经过上面几步,就可以把 App 的事件分发给各个组件了,而且组件对事件的捕获是不依赖于外界(AppDelegate)实现的,只要进行注册就可以了,做到了真正的“开箱即用”,个人认为还是比较优雅的。

由于这种方式需要每个模块实现 +load 方法以注册自身,对启动时间也会有影响,不过实际测量之后,发现大部分耗时都是微秒级别,也就是说 1000 个模块注册耗时可能几十毫秒,这个种程度的影响还是可接受的。

Demo地址

TDFModuleKit

参考

iOS App组件化开发实践

蘑菇街 App 的组件化之路

更新

最近看到了 sunnyxx 的 Notification Once 文章,利用一次通知来实现应用 Launch 的监测 (__block 会将局部变量从栈拷贝至堆),可以说是非常巧妙了,如果对生命周期的回调时间点不做特别精细的要求,可以使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
+ (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];
}];
}