JSPatch 最近新增了扩展 JPBlock ,解决之前 block 使用上的一些限制:
接入 JPBlock 后,就可以:
这篇文章说说这里的实现原理。
先看看要让 JSPatch 支持 block,我们需要做什么,我们在 JS 用 Function 表示 OC 上的 block,这里涉及两个语言两种类型的转换问题,JS Function 和 OC Block 的互相转换:
第一点 JavaScriptCore 已经做了处理,通过 JavaScriptCore 的接口把一个 block 返回给 JS,JavaScriptCore 会自动把它转为一个 JS Function,不过这里有个坑,待会再说。
对于第二点 JavaScriptCore 是没有处理的,一个 JS Function 通过 JavaScriptCore 传入 OC 后仍是一个 JSValue 类型,需要我们自己做处理。具体使用场景:
@implementation JPDemoClass + (void)callBlock:(void(^)(NSString *str))blk { blk(@"string from oc"); } @end
require('JPDemoClass').callBlock(block("NSString *", function(str){ console.log(str); });
在 JS 用一个 Function 传进去给 OC,OC 经过 JSPatch 引擎处理后,可以当 block 调用它。这里 JSPatch 做了什么处理呢?JSPatch 创建了一个 block 作为转接:
static id genCallbackBlock(JSValue *jsVal) { id cb = ^id(void *p0, void *p1, void *p2, void *p3, void *p4, void *p5) { //强制转换参数 //调用 jsVal 里的 JS 函数 }; return cb; }
上述例子中 blk(@”string from oc”) 调用的是这里 JSPatch 动态创建的一个 block,这个 block 里保存着 JS 传进来的 Function,提取参数去调用 JS Function,再返回 JS Function 执行的结果,整个转接就完成了。
这里一个比较麻烦的问题是,怎样动态创建不同参数类型的 block?在原来的实现中,JSPatch 使用了一种比较取巧简单的方式,固定创建一个返回类型是id,有六个参数并且类型都是 void * 的 block 去表示所有 block。
void* 是无类型指针,可以表示任何数据类型, NSObject 本来就是一个指针, void * 可以强制转换成 NSObject 类型,也可以强制转为 int / BOOL 等类型,另外你强制把一个参数个数多的 block 当成参数个数少的 block 去调用也是没问题的,因为它们内存结构是一样的,只要在 block 里不去取没有传的参数就没事。
于是这里一个 block 就可以表示所有返回值类型是 id ,参数个数是 0-6 个的 block。
到这里也明白为什么原来的实现方式会有上述第1/2点的限制,第一点因为这里只声明了6个参数,参数个数再多的就处理不了了,当然这里也可以继续往上加到十几个。第二点因为 void* 无法表示 double 类型,无法强制转换, struct / union 类型也不行。
那第三个问题(不支持 JS 封装的 block 传到 OC 再传回 JS 去调用)是为什么呢?首先这里 JS 封装的 block 传到 OC 后就被包装成上述 6 个 void* 参数的 block 了,这个 block 再返回给 JS,JavaScriptCore 并没有自动把它转成 JS Function,这也是我们刚才说到的一个坑,为什么呢,幸好 JavaScriptCore 是有源码可以看的,并不是一个黑箱,我们可以看到确切的原因,JavaScriptCore 对 block 的转换,可以从 objCCallbackFunctionForBlock 这个函数看起,追溯到最后,可以在 parseObjCType() 这里发现如果 block 参数里有指针/Class/union 等类型,是不会自动转换的。我们这里生成的 block 参数类型全是 void * 指针,自然不会被转换。
终于说完了原来的实现方式以及三条限制的原因,接下来就是怎样改进解决,很简单,只要可以动态创建各种不同参数类型的 block,就可以一举解决上述三个问题。so 怎样创建呢,首先需要了解下 block 究竟是什么,网上已经有很多 block 原理解析的文章,可以看看,就不详细说明了,这里简单说下:
block 的复杂在于对变量的持有处理,如果抛开这部分处理,block 的结构体是很简单的,可以直接通过结构体创建一个 block:
struct JPSimulateBlock { void *isa; int flags; int reserved; void *invoke; struct JPSimulateBlockDescriptor *descriptor; }; struct JPSimulateBlockDescriptor { unsigned long int reserved; unsigned long int size; }; void blockImp(){ NSLog(@"call block succ"); } void genBlock() { struct JPSimulateBlockDescriptor descriptor = {0, sizeof(struct JPSimulateBlock)}; struct JPSimulateBlock2 simulateBlock = { &_NSConcreteStackBlock, 0, 0, blockImp, &descriptor }; void *blockPtr = &simulateBlock; void (^blk)() = ((__bridge id)blockPtr); blk(); //output "call block succ" }
一个存有函数指针的特定结构体就是一个 block,调用这个 block 就是调用里面函数指针指向的函数。block 的参数类型和个数是跟这个结构体没多大关系的,无论 block 的参数类型和个数是怎样,都可以用这个结构体表示这个 block,不同的就是函数指针需要指向对应参数类型的函数。
所以如果我们要动态创建任意参数类型的 block,问题就变成了如何创建支持任意参数类型的 C 函数,怎样创建呢,两种方法:
显然第二种才是正道,问题就变成了如何动态定义一个函数。
如果你看过 《如何动态调用 C 函数》 ,对这问题可能会有种熟悉的感觉,这正是 libffi 擅长做的事情,原理跟动态调用 C 函数是一样的,只是这里把调用变成定义,这里就不再复述原理了。libffi 同样支持在运行时动态定义 C 函数,在调用时 libffi 会模拟函数参数的入栈出栈去完成调用。来看看是怎样使用的:
svoid JPBlockInterpreter(ffi_cif *cif, void *ret, void **args, void *userdata) { //① //函数实体 //通过 userdata / args 提取参数 //返回值赋给 ret } void main() { //② ffi_type *returnType = &ffi_type_void; NSUInteger argumentCount = 2; ffi_type **_args = malloc(sizeof(ffi_type *)*argumentCount) ; _args[0] = &ffi_type_sint; _args[1] = &ffi_type_pointer; ffi_cif *_cifPtr = malloc(sizeof(ffi_cif)); ffi_prep_cif(_cifPtr, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args); //③ void *blockImp = NULL; //④ ffi_closure *_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&blockImp); ffi_prep_closure_loc(_closure, _cifPtr, JPBlockInterpreter, (__bridge void *)self, blockImp); }
上述例子中,这一系列处理后 blockImp 就可以当成一个指向返回值类型是void,参数类型是 (int a, NSString *b) 的函数去调用,调用后会去到 JPBlockInterpreter 这个函数实体,在这个函数里面可以通过 args 提取传进来的参数,通过userdata 取上下文进行处理。这里可以根据参数类型的不同动态创建不同的 cif 对象,生成不同的 blockImp 函数指针。
到这里问题都解决了,我们可以用结构体动态创建 block,又可以通过上述流程用 libffi 提供的接口动态定义任意参数类型函数的,也就可以动态创建任意类型的 block 了,剩下的就是手尾和细节,定义接口,接入 JSPatch 流程等工作了。
本来到这里就结束了,但有个签名的问题还是说一下。在实现过程发现,按上述方式创建的 block 传出去给 JS 时,JavaScriptCore 并不会自动把它转为 JS Function,为什么呢,还是得到 JavaScriptCore 源码去找原因,可以在 objCCallbackFunctionForBlock() 这个方法看到:
if (!_Block_has_signature(target)) return 0;
如果传进来的 block 没有 signature,这里就会不会去走转换的逻辑,那怎样能让 block 有 signature 呢?我们再去 runtime 源码 看看 _Block_has_signature() 的实现,可以得到答案。我们刚才定义的 block 结构体是这样:
struct JPSimulateBlock { void *isa; int flags; int reserved; void *invoke; struct JPSimulateBlockDescriptor *descriptor; }; struct JPSimulateBlockDescriptor { unsigned long int reserved; unsigned long int size; };
其中 descriptor 结构体只有 reserved 和 size 两个数据,实际上 descriptor 会根据需要去追加数据,runtime 里定义的 descriptor 结构体有三组:
struct JPSimulateBlockDescriptor { //Block_descriptor_1 struct { unsigned long int reserved; unsigned long int size; }; //Block_descriptor_2 struct { // requires BLOCK_HAS_COPY_DISPOSE void (*copy)(void *dst, const void *src); void (*dispose)(const void *); }; //Block_descriptor_3 struct { // requires BLOCK_HAS_SIGNATURE const char *signature; const char *layout; }; };
block 的 flags 有两个位 BLOCK_HAS_COPY_DISPOSE(1 << 25) / BLOCK_HAS_SIGNATURE(1 << 30) 分别表示这个 block 的 descriptor 有没有 Block_descriptor_2 和 Block_descriptor_3 这两组数据。block 的签名就保存在 Block_descriptor_3 结构体里。所以如果我们要让 block 有 signature 签名,就需要:
signature 数据就是表示这个block 返回类型/参数类型的数据,类似这样的:”i8@?@8″。于是只要按它的规则照做就可以了,可以看到 JPBlock 里最终生成 descriptor 和 block 是这样写的:
struct JPSimulateBlockDescriptor descriptor = { 0, sizeof(struct JPSimulateBlock), [self.signature.types cStringUsingEncoding:NSUTF8StringEncoding], //signature数据 NULL }; .... struct JPSimulateBlock simulateBlock = { &_NSConcreteStackBlock, BLOCK_HAS_SIGNATURE, 0, blockImp, &descriptor }; ....
有了 signature,JavaScriptCore 也可以自动把这里创建的 block 转为 JS Function了,到这里问题就都解决了,其他细节可以看看 JPBlock 源码 。
有 JPBlock 扩展后,JSPatch 对 block 的限制少了,但还没达到完备的状态,目前不支持的还有:
第一个问题只是还未处理 struct 类型,C 函数调用也一样,struct 的支持会有点麻烦,后续再找时间实现。第二个问题是我们用 JavaScriptCore 的接口去做 block -> JSFunction 的转换,JavaScriptCore 就是不支持有这几个参数类型的 block 的转换,如果要做就需要抛弃 JavaScriptCore 的自动转换,由 JSPatch 自己做转换,后续再看情况支持。