这是 JSPatch 源码解析的第三部分,主要对之前遗漏的方法做一些补充,以及提供一些思考问题
补充
js 方法补充
将 OC 对象转为 js _formatOCToJS
这个方法用于将 js 端接收到的 OC 对象转换为 js 对象:
1 | /// 把 oc 转化为 js 对象 |
具体的过程做了详尽的注释。这里还要提一点,关于空对象的判断。由于 OC 的动态性,OC 中的 nil 发送消息会不响应任何方法。而 js 端的 undefined 或者 null 调用方法时则会直接报错。
因此,为了能让 js 端在接收到 oc 端的 nil 对象时也能调用,而不报错,针对 undefined, null 以及某些 OC 方法在返回空对象时手动返回的 {__isNil: true}
, 这个方法都会将其解析为 bollean 值 false。
js 端空对象的调用在不报错后,还要阻止其针对 bollean 值 false 的消息转发。因此,之后在执行 __c()
调用 oc 方法时就会提前判断是否是 bollean 类型。如果是就说明是空对象,直接返回 false,而不进行消息转发。
定义 js 中的类 defineJSClass
defineClass
会在 oc 生成对应的类。而一些不需要继承 OC,和 OC 没有联系,只会在 JS 中使用的类,比如数据层的 dataSource/manager,直接使用 JS
原生类就可以。因此添加了一个 defineJSClass
方法,可以减少转化为 OC 类时的性能损耗。
1 | global.defineJSClass = function(declaration, instMethods, clsMethods) { |
代码实现很简单,还是在全局域下创建一个相应类名的对象。然后把所有类方法和实例方法添加至对象中。由于是通过原型实现的,不了解 js 原型的同学可能稍难理解。
这个 defineJSClass
方法和 defineClass
创建的类对象基本一致。但是有两点不同:
- 不用 self 代替 this。
- 没有 property
由于是纯 js 实现,因此不需要用 self 代替 this,也不需要通过关联属性定义 property,直接把属性放在 js 端就可以了。
OC 方法补充
将 js 对象转为 OC 对象 formatJSToOC
针对几种特殊情况做处理,包括类型为 JPBoxing
类型,包含 __obj
,__clsName
,__isBlock
字段:
1 | static id formatJSToOC(JSValue *jsval) |
将 OC 对象转为 js formatOCToJS
将 OC 对象转为 JS 对象,为了方便 JS 调用,提前将 OC 对象的类取出,以 {__obj: xxx, __clsName: xxx}
的形式包裹:
1 | // 把 oc 对象转为 js 对象 (增加了 __obj __clsName 等字段) |
这里通过 formatOCToJS
转换后是一个包裹对象,传到 JS 端后还需要通过 _formatOCToJS
拿到 JS 真正想要的东西。
获取 protocol 中的某个方法的签名 methodTypesInProtocol
可以说是一个方法模板了,没有特别的技巧:
1 | // 获取 protocol 中相应方法的函数签名 |
新建协议
在 JS 端定义协议非常简单,和创建类差不多,传入类名,类方法和实例方法:
1 | global.defineProtocol = function(declaration, instProtos , clsProtos) { |
来到 OC 中,调用的是 defineProtocol
方法。动态创建 protocol,向其中添加方法。
1 | // 动态创建一个 protocol |
从 JS 中传来的 Protocol 是一个字典类型。键是方法名,值是参数返回值的类型,如下:
1 | { |
添加方法这一步最重要的就是要获取方法的 typeEncode
,如果注册者能够写出正确的 typeEncode
那么就可以直接注册了。但是由于 typeEncode
很多开发者不能正确写出,因此,又提供一套保底方案,通过 paramsType
和 returnType
解析出类型:
1 | // 为 protocol 添加方法 |
至此,协议就添加完成了。之后就可以给类添加这个协议了。
执行block
block 本身就是对象,执行 block 也就是执行 block 下的函数指针指向的方法。因此,block 也是可以进行消息转发的。可以通过替换 forwardInvocation:
方法执行 js 部分的方法。
js 端定义 block
当 OC 端需要 js 提供一个回调函数的时候。不能简单的只是提供一个 function。对于 OC 来说,它们需要知道函数的 typeEncode
。因此,JSPatch 在 JS 端提供了一个 block
方法给使用者,使用者需要提前给出参数的类型:
1 | // Obj-C |
1 | // JS |
block
方法的定义方式如下:
1 | global.block = function(args, cb) { |
它最终返回的任然是一个对象。并且在对象中提供了一个标识 __isBlock
。这个标识在之前的源码解析中都有涉及,只不过当时都略过了。下面来看看哪些地方对这个标识做了判断,且进行了处理。
OC 定义 block
在将 js 对象转为 OC 对象的时候会调用 OC 的 formatJSToOC
方法,其中会判断传来的是否是 js 的 block。如果是,则要生成回调 block:
1 | // js 转为 oc 的对象 |
普通的 block 走的是 genCallbackBlock
方法。它主要做了五件事:
- 创建空的 block 实例
- 根据参数类型,生成 block 函数的签名,并设置给空 block
- 通过关联对象将函数实现保存到空的 block 中
- 替换
NSBlock
的函数指针 invoke 为msgForwardIMP
- 替换
NSBlock
的消息转发方法forwardInvocation:
和获取签名方法methodSignatureForSelector:
1 | static id genCallbackBlock(JSValue *jsVal) |
JSPatch 中的 block 的处理应该是参考了 Aspects 中对于 block 的处理方式。将 block 的 IMP 指向 objc_msgForward
,并且替换了 NSBlock 的消息转发方法,所有相关 block 的执行都会走到 JPForwardInvocation
方法中:
1 | static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) |
判断如果是一个 block,就从关联属性中拿到 callback,然后执行。
函数签名的获取方法 methodSignatureForSelector
也被替换为从 NSBlock 中直接获取:
1 | NSMethodSignature *block_methodSignatureForSelector(id self, SEL _cmd, SEL aSelector) { |
因为 JSPatch 中的 block 都是没有用到外部变量的,所以是全局 block,不会存在 copy 和 dispose 函数,这和 Aspects 中的不同,Aspects 中的 block 时可能引用外部变量的,因此需要多做一步判断,通过 block 的 flag 判断 block 的类型。
总结
再来总结一下 JSPatch 的执行流程。
定义方法
- 创建一个 JSContext 并把各种方法通过 JSContext 提供给 js
- 将 js 重写的方法方法通过正则表达式改写为 __c() 的形式
- 预处理提供给 OC 调用的各个方法,hook每一个方法,将方法的第一个参数取出,并赋给
global.self
。这就是 OC 调用提供的上下文。OC 将会把这些方法保存在_JSOverideMethods
字典中 - 预处理提供给 OC 调用的各个方法,hook每一个方法,将 this 赋给 global.self 。这就是 JS 调用时的上下文。JS 会把这些方法保存在 _ocCls 中
- OC 端解析 js 传来的类和方法,将方法实现替换为
_objc_msgForward
,替换定义类的消息转发方法为JPForwardInvocation:
执行方法
分为两种情况:
- js 调用 js 重写的方法。这种情况下,在
_ocCls
字典中找到相应的方法直接执行 - oc 调用 js 重写的方法。分为两步:
- oc 调用方法,主要是创建
NSInvocation
,并根据 SEL 的函数函数签名将 js 传来的参数根据类型设置到NSInvocation
上。然后执行 - 来到
JPForwardInvocation
实际执行方法,在_JSOverideMethods
中找到对应的 js 实现,如果没有走原始的消息转发。将结果设置到NSInvocation
的 returnValue 中
- oc 调用方法,主要是创建
思考题
require(xxx)
做了什么?- JS 中的链式调用时如何实现的?
- JS 端和 OC 端如何通信的?
- JS 端如何调用 OC 的某个对象的实例方法?
- OC 端拿到 JS 传来的对象方法名参数之后要如何调用?
- 如何实现方法替换?
- 为什么JS端新增方法要在 OC 中
class_addMethod()
再走一遍完整转发流程,而不是直接通过消息转发? - 如何给类添加属性?
- self 关键字如何处理?
- OC 中无法动态获取 super,那么 super 方法如何调用的?
- JSBoxing 的作用是什么?
- OC 中的 nil 到 JS 中就变成了 null,就无法链式调用,那么要怎么解决?
- JS 如何支持自定义 struct 的?
- 可变参数方法如何调用?
- dealloc hook 的时候做了哪些特殊处理?
- JS 端重写的方法调用另一个 JS 端重写的方法,如何做到不用 JS -> OC -> JS 这样周转?
defineJSClass
作用是什么?实现原理是怎么样的?- 如何 hook 带有 block 的 OC 方法?
- JS 方法中如何调用 C 函数?
参考链接
探究Block之MethodSignature(文章中关于 block 方法签名处有结论性的错误,堆 block 中含有 copy 和 dispose 方法,而全局 block 没有这两个方法,它的验证试验的结论也是错的。)