通过 clang 来研究 Block 的实现
clang 是 Objective-C 的编译器前端,用它可以将 Objective-C 代码转换为 C/C++ 代码,然后可以来分析 Objective-C 中的一些特性是怎么实现的。
首先创建一个命令行应用程序,在 main.m 中,修改 main 函数的内容:
1 | int main() { |
把上面代码用:
1 | clang -rewrite-objc main.m |
可以在 main.cpp 中找到生成后的代码,可以看到生成后的 main 函数是这样的:
1 | int main() { |
Block 的内存布局和创建过程
先看第一行:
1 | void (*task)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); |
= 号左边是声明了一个名为 task 的函数指针。
= 号右边创建了一个 __main_block_impl_0
类型的结构体,并通过 __main_block_impl_0
的初始化函数来初始化,简写如下:
1 | _main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) |
第一个参数是 __main_block_func_0
指针,第二个参数取了 __main_block_desc_0_DATA 的地址。
现在再看看 __main_block_func_0
是什么:
生成的代码中 __main_block_func_0
的定义如下,是一个静态全局函数:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
可以看到它有一个名为 __cself 的参数,是一个 __main_block_impl_0 结构体类型的指针。
于是我们继续来看 __main_block_impl_0 结构体,它的定义如下:
1 | struct __main_block_impl_0 { |
它拥有两个成员变量,impl 和 Desc,还带有一个初始化函数。impl 的类型为 __block_impl 结构体,可以找到它的定义如下:
1 | struct __block_impl { |
__block_impt 就是 Block 背后的数据结构:
- isa 字段代表 block 对象是由哪一个类实例化来的。
- Flags 标识符。
- Reserved 是为未来保留的字段。
- FuncPtr 是一个函数指针。
再看看 __main_block_impl_0 结构体的另一个成员变量 Desc,是一个 __main_block_desc_0 结构体类型的指针。
在生成的代码中:
1 | static struct __main_block_desc_0 { |
__main_block_desc_0 有两个字段,一个保留字段 reserved,和一个保存 Block 占用内存大小的字段 Block_size。并且同时创建了一个 __main_block_desc_0_DATA 的变量,初始化为 { 0, sizeof(struct __main_block_impl_0) }
。
通过 __main_block_impl_0
的定义可知,__main_block_impl_0
的初始化方法的第一个参数是一个函数指针。再回到 main 函数中,在初始化 _main_block_impl_0 时,可以看到传入的函数指针为 __main_block_func_0
,因此 Block 在调用的时候就是调用的 __main_block_func_0
这个函数。__main_block_func_0
函数接收一个参数 __cself
,也是 __main_block_impl_0 结构体类型的指针,它代表这个 Block 自身。
__main_block_impl_0 初始化函数的第二个参数则传入了 __main_block_desc_0_DATA 变量,它的 reserved 变量为 0,Block_size 为 __main_block_impl_0 的大小。
再回到:
1 | void (*task)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); |
可以看到创建好的 __main_block_impl_0
结构体,通过 & 操作符获取了它的地址,然后转换为 void (*)() 这个函数指针类型,最后赋值给 task 这个函数指针。
通过分析 main 函数的第一行代码,大概知道了 Block 在内存中的布局是什么样的,也知道了 Block 的创建过程,现在来看看 Block 是怎么被调用的。
Block 的调用过程
现在开始研究 main 函数的第二行代码:
1 | ((void (*)(__block_impl *))((__block_impl *)task)->FuncPtr)((__block_impl *)task); |
由于代码中涉及到很多类型强转,看上去比较乱,把它分解如下:
1 | __block_impl *taskImpl = (__block_impl *)task; |
可见 Block 的调用就是通过 __block_impl
中的函数指针 FuncPtr 来调用函数。Block 的调用就是函数的调用。
通过上面的分析可知,FuncPtr 指向的函数为 __main_block_func_0
,上面的例子中 __main_block_func_0
函数的内容为空,现在给 Block 中添加一句代码:
1 | int main() { |
通过 clang -rewrite-objc main.m
重新生成后,会发现 __main_block_func_0 函数的内容就是 Block 的内容:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
所以 Block 的调用就是函数的调用。
Block 截获自动变量
现在来看看 Block 截获自动变量是怎么实现的,把 OC main 函数中的代码改为:
1 | int main() { |
查看生成的代码,会发现 __main_block_impl_0
中增加了一个成员变量 i:
1 | struct __main_block_impl_0 { |
在 __main_block_impl_0
也增加了 _i 参数用于初始化 i 变量。这里的 i 的初始化时值传递的,因此 i 是保存了 Block 外面的变量 i 的值的副本。
再看看 Block 截获对象的情况,把 main 函数改为:
1 | int main() { |
生成的代码中的 __main_block_impl_0
同样是增加了一个成员变量。
1 | struct __main_block_impl_0 { |
由于这里 _object 也是一个 Block 外 object 对象的指针的复制值,同样无法用 Block 中保存的 object 来修改 Block 外的 object 的值。
__block 的实现
未完待续。