Block 概述
Block 是 Apple 提供的一种闭包实现,比较方便实现一些函数嵌套实现的功能。Block 分为三种类型:
- NSConcreteGlobalBlock
- NSConcreteStackBlock
- NSConcreteMallocBlock
这三种类型分别对应了三种不同的 Block 类型,值得注意的是,在启用了 ARC 之后,NSConcreteStackBlock 会转换类型为 NSConcreteMallocBlock。
换种比较容易的理解的说法:
在非 ARC 下,LLVM 编译下没有访问局部变量的 Block 应该是 NSConcreteGlobalBlock 类型的,访问了局部变量的 Block 是 NSConcreteStackBlock 类型的。
在 ARC 下,访问了局部变量的 Block 是 NSConcreteMallocBlock 类型的,未访问局部变量的 Block 是 NSConcreteGlobalBlock 类型的。
具体的实现代码可以参考 llvm 的 BlockRuntime。
对于研究 block 的具体代码翻译,则可以使用 clang 的 rewrite-objc 功能,将 OC 文件转换成 cpp 文件。比如:
clang -rewrite-objc blocktest.c
这样就可以生成对应的 blocktest.cpp 文件。
从源码看 NSConcreteGlobalBlock
首先先看一下 NSConcreteGlobalBlock 的代码,从简单的开始:
#include <stdio.h>
int main()
{^{printf("Hello World!\n");}();
return 0;
}
翻译之后的代码(精简仅包含所有必须代码):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Hello World!\n");}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {0, sizeof(struct __main_block_impl_0)};
int main()
{(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)();
return 0;
}
主要的 Block 代码被翻译成为了一个指针函数调用,__main_block_impl_0 是一个定义过的结构,每个 Block 的类型是固定的。其中 isa 是指明的 Block 类型,FuncPtr 则是函数指针。值得注意的是,虽然 impl.isa 填写的是 NSConcreteStackBlock,但是实际在编译过程中,这里还是会被处理成为 NSConcreteGlobalBlock。
从反汇编看 NSConcreteGlobalBlock
(x86_64):
__text:0000000100000EC0 _main proc near
__text:0000000100000EC0
__text:0000000100000EC0 var_8 = dword ptr -8
__text:0000000100000EC0 var_4 = dword ptr -4
__text:0000000100000EC0
__text:0000000100000EC0 push rbp
__text:0000000100000EC1 mov rbp, rsp
__text:0000000100000EC4 sub rsp, 10h
__text:0000000100000EC8 mov eax, 0
__text:0000000100000ECD lea rcx, ___block_literal_global
__text:0000000100000ED4 mov [rbp+var_4], 0
__text:0000000100000EDB mov rdi, rcx
__text:0000000100000EDE mov [rbp+var_8], eax
__text:0000000100000EE1 call cs:off_100001050
__text:0000000100000EE7 mov eax, [rbp+var_8]
__text:0000000100000EEA add rsp, 10h
__text:0000000100000EEE pop rbp
__text:0000000100000EEF retn
__text:0000000100000EEF _main endp
(ARM):
__text:00002F70 PUSH {R7,LR}
__text:00002F72 MOV R7, SP
__text:00002F74 SUB SP, SP, #8
__text:00002F76 MOVS R0, #0
__text:00002F7C MOV R1, #(___block_literal_global - 0x2F88) ; ___block_literal_global
__text:00002F84 ADD R1, PC ; ___block_literal_global
__text:00002F86 MOV R2, R1
__text:00002F88 STR R0, [SP,#0x10+var_C]
__text:00002F8A LDR R1, [R1,#(off_3028 - 0x301C)]
__text:00002F8C STR R0, [SP,#0x10+var_10]
__text:00002F8E MOV R0, R2
__text:00002F90 BLX R1 ; ___main_block_invoke
__text:00002F92 LDR R0, [SP,#0x10+var_10]
__text:00002F94 ADD SP, SP, #8
__text:00002F96 POP {R7,PC}
会发现在调用 block 时是采用的直接调用的方式 (call/blx),由于 NSConcreteGlobalBlock 没有传入参数,因此这个也就是关键在参数处理方式上。
在 X64 平台上,off_100001050 中保存的就是 main_block_invoke(也就是我们使用的 block)的地址,而在 ARM 平台上,R1(main_block_invoke)地址是在 LDR R1, [R1,#(off_3028 - 0x301C)] 这一句赋值而来,其中 off_3028 指向的就是 main_block_invoke 的地址。
<未完待续>