这是 JSPatch 源码解析的第二部分。主要解释 JS 与 OC 的相互调用
执行流程
js 端调用
前面说过,所有 js 中定义的方法调用都会被预处理替换为 __c()
的形式。这是方法调用的起点。主要逻辑如下:
- 判断调用者是否是 bool 类型,是的话直接返回 false
- 如果调用的方法是重写的方法,那么绑定上下文
- 如果调用的是父类方法,那么将
__clsName
设置为父类方法名,从__ocCls
数组中取出父类的方法调用。 - 如果调用的方法不是 js 定义的方法那么走到 oc 调用
第一步判断是否是 bool 类型的原因在下面的注释中有细说,主要是为了能让 js 代码在 oc 返回的对象是 nil 的情况下也能调用,不报错。
1 | /// 调用 native 相应的方法 |
__realClsName
和__clsName
一般情况下是相等的,但是如果要调用的是父类的方法,那么__clsName
会被替换为父类名,而子类名仍然是__realClsName
调用 oc 方法通过 _methodFunc
执行:
1 | /// 执行 js 方法, 返回 js 对象 |
由于 OC 端的参数都在 defineClass 的时候做了处理,从 xxx_xxx
变为了 xxx:xxx:
,js 端则没有做任何处理仍然是 xxx_xxx
。因此,就需要在调用前将方法名进行修改。最终根据是否是类方法来决定调用 _OC_callI
还是 _OC_callC
。
OC 端执行
OC 端执行涉及到的几个方法是 JSPatch 最重要的几个方法了。涉及到方法的调用,方法签名的获取,方法的消息转发
OC 执行方法
js 端调用 _OC_callI
或者 _OC_callC
之后都会来到 OC 的 callSelector()
方法中。
- 特殊情况处理
- 处理调用父类方法
- 创建 NSInvocation
- 处理可变参数方法
- 设置 NSInvocation 参数
- 执行并返回结果给 js
处理特殊情况
这一步主要包含两种特殊情况:
- 执行方法的是实例对象还是类还是空对象
- 执行的方法是
toJS
方法
调用 formatJSToOC
方法,把调用者和参数都转为 OC 的对象,这个方法连同后面将OC结果转JS的方法 formatOCToJS
会在后面单独说。
首先判断 JS 传来的调用者的类型。如果 class_isMetaClass
得到的是 true,就表示是一个类对象。如果是 Bool 类型,或者是 _nilObj
那么直接返回 {isNil: true}
这样,JS 端就知道是空对象了。
之后判断调用方法是否是 toJS
。这个方法用来将 OC 对象转为形如 {__obj: xxx, clsName: xxx}
的 js 对象返回。
1 | static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper) |
toJS
方法的实现:
1 | // 把 oc 对象转为 js 对象 (增加了 __obj __clsName 等字段) |
处理调用父类方法
如果 js 端调用的是父类的方法,那么模拟 OC 调用父类的过程。OC 中调用父类,调用者还是当前子类,但是调用的 IMP 则是父类方法的 IMP。因此,要模拟 OC 中的方法调用,需要给 OC 添加和父类一样的 IMP 实现。以 SUPER_XXX
表示 SEL。
1 | static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper) |
创建 NSInvocation
这一段比较简单,就是创建 NSInvocation
,并且获取方法签名。其中将缓存存放在 _JSMethodSignatureCache
字典中的操作我认为意义不大。
1 | static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper) |
处理可变参数方法
方法签名的参数个数小于实际传入的参数就是可变参数方法。重写可变参数方法容易,但是直接调用可变参数方法却比较难。因为,通过 objc_msgSend
需要确定调用固定参数的个数。
因为编译的时候要根据调用的参数个数类型分配参数入栈空间,针对不同 CPU 来生成不同的汇编代码。所以不能再不知道入参个数和类型的情况下动态调用。因此,forwardInvocation 的时候才必须要有方法签名。
c 函数的动态调用也是一样,无法直接通过获取函数指针调用。因此参数的处理肯定是不一致的。如果想要动态调用,必须通过其他手段消除这个问题。可以使用 libffi 库。
既然 objc_msgSend
需要明确知道固定参数的个数,那么我们就把它确定参数个数的方法都实现一遍就好了。比如:
1 | id (*new_msgSend1)(id, SEL, id,...) = (id (*)(id, SEL, id,...)) objc_msgSend; |
上面就声明了一个 new_msgSend1
,实现了调用固定参数一个,后续是可变参数的方法。同样的,我们可以声明两个固定参数,乃至 n 个固定参数的方法。但是这样有一个缺陷就是只能调用所有参数都是 id 类型的方法,因为无法提前确定参数类型。
各个固定参数个数的 objc_msgSend
方法定义好后,就可以根据 JS 端传来的参数个数和方法签名的实际参数个数来决定调用哪个 objc_msgSend
了。
1 | static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper) |
(invokeVariableParameterMethod()
方法就省略了,用了大量的宏来定义和代用 objc_msgSend
,有兴趣可以自己查看)
设置 NSInvocation 参数
设置 NSInvocation 参数也是一个非常冗长的方法。这个方法的起因是 JS 端传来的 JSValue 类型的参数在最开始 formatJSToOC
的时候都变为了统一类型 id 类型。设置 NSInvocation 参数的时候要恢复其原来的类型。
以下方法做了详尽的注释:
1 | static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper) |
注意,如果期待的入参的 typeencoding 是 ^@
,即方法需要一个二级指针作为参数的时候。会创建一个 _TMPMemoryPool
字典,以及一个 _markArray
数组,并把参数 push 到数组中。这两个对象会在执行完成是用到,下面再说。
执行并返回结果给 js
执行的时候会判断是否是父类方法,如果执行的是父类方法,那么会把它预存在 _currInvokeSuperClsName
字典中。因为前面我们处理父类方法的时候是给子类添加 SUPER_XXX
的 SEL。在后面消息转发时,在 _JSOverideMethods
字典中保存的则是 XXX
的 SEL。因此,保存在 _currInvokeSuperClsName
字典中就是要说明这是调用的父类的方法,要把 SUPER_
前缀去掉,到父类的重写方法中找。
1 | static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription) |
在方法执行完成后,会对 _TMPMemoryPool
和 _markArray
进行处理。具体场景再补充中说,这里只是添加注释:
1 | static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription) |
最后对 OC 执行返回的结果 JS 化。这里存在一个内存泄漏的问题。即当动态调用的方法是 alloc
,new
,copy
,mutableCopy
时,ARC 不会在尾部插入 release
语句,即多了一次引用计数,需要通过 (__bridge_transfer id)
将 c 对象转为 OC 对象,并自动释放一次引用计数,以此来达到引用计数的平衡:
1 | char returnType[255]; |
OC 消息转发
jspatch 想要 hook 任意的方法,就需要让所有被 hook 的方法走一个统一的入口。这个统一的入口通过让所有被替换的方法走消息转发,并且 hook 消息转发的 forwardInvocation()
方法实现,无论这个方法是 JS 调用的还是 OC 调用的。
主要过程如下:
- 获取被重写的方法的 JS 实现
- 如果没有重写的 JS 方法,那么执行原来的方法实现
- 准备提供给 JS 的参数
- 执行上下文,方法的实际调用者 self
- 方法的实际参数
- 针对调用父类方法的特殊处理
- 执行 JS 重写方法,并对将返回结果设回 NSInvocation
- 针对 dealloc 方法的特殊处理
获取被重写方法的 JS 实现
JPForwardInvocation
是一个静态方法,它的前两个参数分别是 self
和 selector
。主要通过 getJSFunctionInObjectHierachy()
方法获取 JS 替换方法的实现
1 | // 自己的替换方法, 可以看到调用方法前两个参数一个是 self,一个是 selecter, 对应于方法签名的 @: |
getJSFunctionInObjectHierachy()
方法查找当前类是否有 JS 实现,如果没有,就从父类中查找。如果父类中也都没有实现,那么就要走原本的消息转发逻辑了。
1 | // 判断这个方法是否有 js 方法的实现。 后续通过这个判断结果走原始转发流程还是走 js 方法的调用 |
原本的 forwardInvocation:
指向了 SEL ORIDforwardInvocation:
。拿到它对应的函数指针,并且执行:
1 | // 方法的原本的 forward 流程 |
准备传给 JS 的参数
先要把 self
取出来,放到参数数组中。类对象就用一个包含 __clsName
的对象表示,实例对象则用 JPBoxing
包裹。另外,对于 dealloc
方法要注意不能使用 weak 修饰。在 dealloc
期间,不能使用 weak:
1 | static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) |
之后就是取出 NSInvocation
中的各个参数了。这个过程是 callSelector
中设置 NSInvocation
的逆过程。基本一致,所以不重复贴代码了。
调用父类方法的特殊处理
1 | static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) |
执行 JS 方法,并将结果设置到 NSInvocation
准备好参数后,只要调用 callWithArguments
就可以同步获取到执行结果。由于是消息转发,因此要将得到的结果还要通过 setReturnValue:
设置给 NSInvocation。
1 | static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) |
针对 dealloc 方法的特殊处理
如果是一般的方法,用 JS 替换了原方法执行就完事了。但是 dealloc 方法则有点特殊,它的原始方法必须要执行,不然怎么完成资源回收。因此,在方法执行的最后会判断当前执行的是不是 dealloc 方法,如果是,那么默认调用它的实现:
1 | static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) |
至此,OC 端的消息转发过程结束。