分析 Block 的实现方式

通过 clang 来研究 Block 的实现

clang 是 Objective-C 的编译器前端,用它可以将 Objective-C 代码转换为 C/C++ 代码,然后可以来分析 Objective-C 中的一些特性是怎么实现的。

首先创建一个命令行应用程序,在 main.m 中,修改 main 函数的内容:

1
2
3
4
5
6
7
8
int main() {
void (^task)(void) = ^{
};

task();

return 0;
}

把上面代码用:

1
clang -rewrite-objc main.m

可以在 main.cpp 中找到生成后的代码,可以看到生成后的 main 函数是这样的:

1
2
3
4
5
6
7
int main() {
void (*task)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

((void (*)(__block_impl *))((__block_impl *)task)->FuncPtr)((__block_impl *)task);

return 0;
}

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
2
3
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

}

可以看到它有一个名为 __cself 的参数,是一个 __main_block_impl_0 结构体类型的指针。

于是我们继续来看 __main_block_impl_0 结构体,它的定义如下:

1
2
3
4
5
6
7
8
9
10
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;
}
};

它拥有两个成员变量,impl 和 Desc,还带有一个初始化函数。impl 的类型为 __block_impl 结构体,可以找到它的定义如下:

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

__block_impt 就是 Block 背后的数据结构

  • isa 字段代表 block 对象是由哪一个类实例化来的。
  • Flags 标识符。
  • Reserved 是为未来保留的字段。
  • FuncPtr 是一个函数指针。

再看看 __main_block_impl_0 结构体的另一个成员变量 Desc,是一个 __main_block_desc_0 结构体类型的指针。

在生成的代码中:

1
2
3
4
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)};

__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
2
3
__block_impl *taskImpl = (__block_impl *)task;
void (*func)(__block_impl *) = (void (*)(__block_impl *))(taskImpl->FuncPtr);
func(taskImpl);

可见 Block 的调用就是通过 __block_impl 中的函数指针 FuncPtr 来调用函数。Block 的调用就是函数的调用。

通过上面的分析可知,FuncPtr 指向的函数为 __main_block_func_0,上面的例子中 __main_block_func_0 函数的内容为空,现在给 Block 中添加一句代码:

1
2
3
4
5
6
7
8
9
int main() {
void (^task)(void) = ^{
printf("Hello");
};

task();

return 0;
}

通过 clang -rewrite-objc main.m 重新生成后,会发现 __main_block_func_0 函数的内容就是 Block 的内容:

1
2
3
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello");
}

所以 Block 的调用就是函数的调用。

Block 截获自动变量

现在来看看 Block 截获自动变量是怎么实现的,把 OC main 函数中的代码改为:

1
2
3
4
5
6
7
8
9
10
11
int main() {
int i = 10;

void (^task)(void) = ^{
printf("%d", i);
};

task();

return 0;
}

查看生成的代码,会发现 __main_block_impl_0 中增加了一个成员变量 i:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int i; // 增加了一个成员变量 i
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

__main_block_impl_0 也增加了 _i 参数用于初始化 i 变量。这里的 i 的初始化时值传递的,因此 i 是保存了 Block 外面的变量 i 的值的副本。

再看看 Block 截获对象的情况,把 main 函数改为:

1
2
3
4
5
6
7
8
9
10
11
int main() {
NSObject *object = [[NSObject alloc] init];

void (^task)(void) = ^{
NSLog(@"%@", object);
};

task();

return 0;
}

生成的代码中的 __main_block_impl_0 同样是增加了一个成员变量。

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *object; //
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_object, int flags=0) : object(_object) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

由于这里 _object 也是一个 Block 外 object 对象的指针的复制值,同样无法用 Block 中保存的 object 来修改 Block 外的 object 的值。

__block 的实现

未完待续。