在没有实行组件化的项目中,经常会在 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 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
不过这种方式缺点也很明显,组件需要依赖主工程的 AppDelegate 是否实现了 UIApplicationDelegate 的代理方法,如果没有的话,即使组件方实现了对应的代理方法,依然无法捕获到事件。
再来看下 caojun 的处理方案 YTXModule。
这个方案主要思路是通过 runtime method swizzling,替换 AppDelegate 中实现的 UIApplicationDelegate 代理方法,然后在 swizzled method 中,执行事件分发。 YTXModule 提供了一些宏定义,精简了方法替换流程:
1 | @implementation UIApplication (YTXModule) |
这里需要注意的是,由于 method swizzling 是在不同类型载体(AppDelegate对象 <-> YTXModule类)间交换的方法,所以会造成在 +ytxmodule_applicationDidFinishLaunching:
中调用 self
时,获取的并不是 YTXModule类,而是 AppDelegate对象,因为方法替换实际上替换了 IMP,并没有改变实参,参照 objc_msgSend(id self, SEL op, ... )
的参数排列,可以明确第一个参数是消息接收者,也就是 AppDelegate对象。通过上述分析可以知道,如果直接进行方法替换,不做特殊处理,使用以下代码将会抛出 unrecognized selector
异常 :->
1 | + (BOOL)ytxmodule_application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions |
而以下代码,是可以正常运行的:
1 | + (BOOL)ytxmodule_application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions |
所以 caojun 在方法替换时,给 AppDelegate 添加了相同命名的实例方法,规避了这个异常 :
1 | void Swizzle(Class class, SEL originalSelector, Method swizzledMethod) |
虽说这种方案也能实现事件的分发,但是在不同类型载体间使用 method swizzling 还是应该避免的,对其他开发者不是很友好。并且这种方案也存在依赖 YTXModule 是否替换了 UIApplicationDelegate 的代理方法问题,如果没有,组件方是无法捕获事件的。
一种更加优雅的方案
分发、代理
看到这两个关键词,可以直接联想到 runtime 的另一重要组成部分,消息转发。以下是我结合消息转发实现的组件生命周期管理方案。
先看下 UML 类图:
首先是 TDFModule ,模块基类,所有想要捕获 App 生命周期事件的模块都需要创建一个继承 TDFModule 的类,并且遵守 TDFModuleProtocol 协议:
1 |
|
子类需要在 +load
方法中调用 registerModule
才能让模块具备捕获 App 事件的能力。因为 TDFModuleProtocol 直接遵守的 UIApplicationDelegate 协议,子类可以和 AppDelegate 一样,直接实现自己感兴趣的代理方法即可:
1 | @interface TDFAModule : TDFModule <TDFModuleProtocol> |
以上就是一个简单的使用示例。
接下来是 TDFModuleManager ,模块管理类。这个单例类主要负责模块的储存,以及在 UIApplication 的 -setDelegate:
中,把原来 delegate 替换成自己的 delegate proxy 。
1 | @interface TDFModuleManager : NSObject { |
最后是这个方案的重点,也就是 TDFApplicationDelegateProxy 类:
1 | @interface TDFApplicationDelegateProxy : NSObject |
先说 -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地址
参考
更新
最近看到了 sunnyxx 的 Notification Once 文章,利用一次通知来实现应用 Launch 的监测 (__block 会将局部变量从栈拷贝至堆),可以说是非常巧妙了,如果对生命周期的回调时间点不做特别精细的要求,可以使用以下代码:
1 | + (void)load |