组件化分发生命周期 方案实现探索系列文章:
1、组件化分发生命周期
2、组件化分发生命周期 - AOP 方案
3、组件化分发生命周期 - AOP 方案(libffi 实现)
background
在上一篇 组件化分发生命周期 - AOP 方案 文章里,介绍了通过 Aspects 这个库,面向切面实现分发 UIApplicationDelegate
方法。
再简单重复介绍下分发思路:
- hook
UIApplication
的setDelegate:
方法,拿到AppDelegate
实例
2)遍历 AppDelegate
的方法列表,判断是否是 UIApplicationDelegate
的协议方法
3)对以上遍历得到的方法一一进行 hook
4)在被 hook 方法执行的之前和之后,调用我们想要分发的那些对象对应的方法
在以上第 3) 步里,是通过 Aspects
实现的 hook。Aspects
hook 方法可以返回 NSInvocation
对象,这个对象里包含着 hook 对象,hook 方法,方法参数个数、参数类型等信息,恰好可以拿这些信息去调用目标分发对象的同名方法。
Aspects
的实现原理是使方法调用(消息发送)立马进入消息转发阶段,在消息转发的最后阶段:forwardInvocation:
方法里拿到当前所 hook 方法的 NSInvocation
对象,并进行原方法调用和 block 的调用。
这种实现原理会导致原对象方法的调用要经历消息转发阶段,方法调用的性能差了一些。而且工程中如果全局 hook 了 NSObject
的 forwardingTargetForSelector:
、resolveInstanceMethod:
等方法,监听 unrecognized selector sent to instance
崩溃类型的话,则会捕获到 Aspects
的 hook 过程。
所以在这篇文章里,将采用 libffi 实现上述的第 3) 步的 hook 操作,替换 Aspects
的实现。 libffi
较 Aspects
的好处是,hook 操作是直接交换两个 IMP,和原生的 Method Swizzling 优势一样。而且同样能拿到 hook 方法的参数个数、参数类型等信息。
libffi preview
libffi 相当于 C 语言上的 runtime,拥有动态调用 C 方法及 OC 方法的能力。简单介绍下用法吧,因为本篇文章的重点不在解析 libffi
及用法,所以这里只做简单的使用说明。
例如,调用 C 函数:
1 | int c_func(int a , int b) { |
调用 OC 函数:
1 | - (int)oc_func:(int)a b:(int)b { |
函数调用 ffi_call
方法需要传入:1、函数模版(ffi_cif
),2、函数指针,3、返回值,4、参数数组 这四个参数字段。
重点在于 函数模版(ffi_cif
)。用 ffi_cif
生成一套函数模版,这个模版定义了调用一个函数时,这个函数的:1、参数个数(int
),2、返回值类型(ffi_type
),3、包含各个入参类型(ffi_type
)的数组
这样,libffi
的 ffi_call
方法就可以根据这个传入的 函数模版(ffi_cif
)去调用函数了。
其中,调用 OC 方法的话比较特殊,像 objc_msgSend
方法一样,第一位参数是函数调用者本身,第二位参数是调用的方法 SEL。
1 | void objc_msgSend(void /* id self, SEL op, ... */ ) |
是不是看起来 libffi
的使用和 NSInvocation
很像。
libffi hook
除了动态调用 C & OC 方法外,libffi
提供的另外一个关键的能力是:
通过 ffi_closure_alloc
方法创建一个关联着函数指针(IMP
)的闭包(closure
)
通过 ffi_prep_closure_loc
函数给 cif
关联上这个闭包,并传入一个函数实体(fun
)和刚才闭包里的 IMP
这样,当 IMP
被执行的时候,就会执行函数实体(fun
),并且能在这个函数里拿到:1、cif
,2、返回值,3、参数地址,4、自定义的关联数据(userdata
)
例如:
1 | void *newIMP = NULL; |
有了这样的能力,我们就可以将上述的 newIMP
与我们要 hook 的方法 IMP
交换,这样调用 hook 方法的时候,就会执行到 holo_lifecycle_ffi_closure_func
方法,在这个方法里可以拿到原方法的参数个数、参数类型能信息,我们只要在这个方法里再调用一下原方法的 IMP
,并在之前和之后分别调用要转发的目标对象的同名方法即可。
其中,还需要额外指出的一点是,在动态 hook 方法的时候,如何根据 C&OC 方法里的参数类型转换成 ffi_type
呢?
苹果的官方文档 Objective-C Type Encodings 给出了参数 type encoding 字符串和参数类型的对应关系,也可以参考 NSHipster 的这篇文章:Type Encodings。
其实每种类型,或者说相应字符,在 libffi
里也都存在着对应的 ffi_type
类型。这里直接拷贝了 Stinger 里的 ffi_type *_st_ffiTypeWithType(const char *c) 方法。
代码实现参见:
1 | @implementation UIApplication (HoloLifecycle) |
完整实现代码参见:HoloLifecycle