Block的底层实现原理

前言

Block其实是一种匿名函数,它是一种可以捕获其上下文中自由变量的匿名函数。Block 在别的语言中又称为闭包或者 lamda表达式,在 Swift 中它就已经叫闭包了。

C 语言本身是不支持匿名函数的,也就是说 Block 这个东西是扩展了C语言的功能。通过支持 Block 的编译器,含有 Block 语法的源代码转换为一般 C 语言编译器能够处理的源代码,并作为普通的 C 语言源代码被编译。

在这个过程中会涉及到函数指针,由于 Block 与函数指针有很大的关系,所以在开始讲 Block 之前,我们先回顾一下什么是函数指针。

函数指针

函数指针是指向函数的指针,也就是说,其本质上是个指针,指向函数的首地址。

下面有个例子:

1
2
3
4
5
6
7
// 声明了一个名为 func 的函数;直接调用该函数
int func(int count);
int result = func(10)

// 定义了一个名为 funcPtr 的函数指针,将其指向函数 func 的首地址,通过该函数指针来调用函数 func
int (*funcPtr)(int) = &func;
int result = (*funcPtr)(10);

上面这个例子表明了直接调用函数与使用函数指针来调用函数的区别。使用函数指针的情况下,我们可以不直接使用函数的名字,就可以调用函数了。在这里先记住函数指针调用函数的形式,然后看一下我们的 Block 的使用:

1
2
3
// 定义了一个 Block 类型的变量 blk,并把一个 Block 赋值给它,通过 blk 变量来调用该 Block
int (^blk)(int) = ^(int count){ return count + 1;};
blk(10)

是不是觉得两者很相像?事实上,Block 就是转化为函数指针来实现的。

Block 的实现

不捕获变量的 Block

首先从最简单的 Block 开始,即不捕获其上下文作用域中变量的 Block。

我们先新建一个简单的含有 Block 的源文件 block_1.c

1
2
3
4
5
6
7
#include <stdio.h>

int main(void) {
void (^blk)(void) = ^{ printf("Hello!");} // 定义一个 Block 类型的变量,并将一个简单地 Block 赋值给它
blk(); // 执行该 Block
return 0;
}

要分析其实现,我们就要想办法查看其被编译转换后的普通 C 语言源码。clang (LLVM编译器,类似 GCC,一般在 Mac 下安装了 Xcode 的 Command Line Tools 话就会有)可以帮我们将上面的代码转换为可阅读的 C++ 源代码。在终端执行下面的命令:

1
clang -rewrite-objc block_1.c

命令执行后,会生成 block_1.cpp,这就是上述代码转换后的 C++ 源码文件,这个源代码文件非常的长,有500多行,这里截取最重要的部分来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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!");
}

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) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

首先看上述源码中的 main 函数部分,对比一下可以看到,原先我们的 Block

1
void (^blk)(void) = ^{ printf("Hello!");};

被转化为了

1
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

前半部分很熟悉吧,就是一个函数指针,后半部分看起来很乱,其实很多看起来乱的都是类型的强制转换,比如(void (*)())(void *),把这些东西去掉之后,就变成了

1
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

__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;
}
};

也就是说,我们的 Block 被转换成了一个函数指针,该指针指向一个结构体的实例。

下面我们认真分析一下这个结构体__main_block_impl_0

可以看到这个结构体__main_block_impl_0有两个成员变量,各自分别也是结构体类型,还有一个构造函数。

先看__block_impl结构体:

1
2
3
4
5
6
struct __block_impl {
void *isa; // isa指针,指向 Class
int Flags; // 标志位,Block 被 Copy 时用到
int Reserved; // 保留位
void *FuncPtr; // 指针,指向真正的 Block 实现
};

__block_impl这个结构体保存了 Block 的 isa 指针以及其具体实现等东西。

再看另一个结构体__main_block_desc_0:

1
2
3
4
static struct __main_block_desc_0 {
size_t reserved; // 保留位
size_t Block_size; // Block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

源码中给我们提供了一个该结构体的默认实例__main_block_desc_0_DATA,可以看到它的 Block_size 的值是最主要的那个结构体__main_block_impl_0的大小。

回到结构体__main_block_impl_0,还剩下一个构造函数,构造函数就不细说了,就是将传入的值一一赋值给那两个成员变量结构体中的成员。

这个时候,我么再回过头去看看之前 main 函数中的函数指针那部分

1
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

可以看到,实例化结构体__main_block_impl_0的时候,我们给构造函数传入的是 __main_block_func_0&__main_block_desc_0_DATA。后者我们刚刚分析过,那么前面那个是什么呢?回到之前的源码,我们就会发现,原来__main_block_func_0就是我们 Block 的具体函数实现,原来的匿名函数被转化成了一个实实在在的函数:

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

在实例化结构体__main_block_impl_0的时候,函数__main_block_func_0被传入,并被赋值给__block_impl结构体实例的成员指针FuncPtr

最后看一下刚刚没讲的 main 函数中的另一句代码:

1
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

同样去掉强制类型转换的那些东西:

1
(*blk->FuncPtr)(blk);

可以看到我们原先调用 block 的代码,变成了使用函数指针调用函数。到这里,这个简单地 Block 的实现就分析完了。

再总结一下:

首先我们写的 Block 匿名函数被转化成一个普通的 C 语言函数__main_block_func_0。我们将 Block 赋值给 Block 变量 blk 的过程被转换成了将结构体__main_block_impl_0的实例指针赋值给函数指针 blk 的代码。创建__main_block_impl_0实例的时候将 Block 具体实现的函数__main_block_func_0__main_block_desc_0_DATA传入,并分别赋值给 __main_block_impl_0结构体实例的结构体成员变量 impl 和 Desc 的成员,其中具体实现__main_block_func_0被赋值给了__block_implFuncPtr指针。最终真正调用的时候,就是利用函数指针 blk 指向的结构体实例的 impl 成员的 FuncPtr 指针来调用函数。

这里还有一个问题,就是我们看到函数指针 blk 调用的时候传入了 blk 它本身,这是为什么呢?这个我们下面再说。

捕获变量的 Block

重新建一个含有 Block 的源文件 block_2.c,该 Block 捕获上下文变量 name 和 age:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void) {
int not_used = 0;
const char * name = "Jack";
int age = 10;
void (^blk)(void) = ^{printf("%s is %d years old.", name, age);};
blk();
return 0;
}

利用 clang 转换后的代码如下(依然去除了其他代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
const char *name;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_name, int _age, int flags=0) : name(_name), age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *name = __cself->name; // bound by copy
int age = __cself->age; // bound by copy
printf("%s is %d years old.", name, age);
}

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) {
int not_used = 0;
const char * name = "Jack";
int age = 10;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name, age));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

基本结构和不捕获变量时差不多,不一样的地方有下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *name; // 持有的变量
int age; // 持有的变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_name, int _age, int flags=0) : name(_name), age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
1
2
// 构造函数中传入了捕获的变量 name 和 age
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name, age));

结构体__main_block_impl_0中追加了捕获的那几个变量,同样构造函数也接受变量的传入。这里需要注意的是,Block 语法表达式中没有使用的变量不会被追加进来,也就不会被传入,例如变量 main 函数中的变量not_used

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *name = __cself->name; // bound by copy
int age = __cself->age; // bound by copy
printf("%s is %d years old.", name, age);
}

可以看到,具体实现函数里面使用到的捕获的变量是通过传入的 __main_block_impl_0 结构体参数带进来的。这就回答了我们之前提出的问题:函数指针调用的时候为什么要把自身作为参数传入。因为函数指针 blk 指向的是 __main_block_impl_0 结构体实例,而__main_block_impl_0 结构体实例中保存了捕获到的变量。传入该__main_block_impl_0 结构体实例,就可以使用捕获到的变量了。所以这个参数名称就叫 __cself,十分类似于 OC 类中的 self,跟 Python 类中方法显式的将 self 作为第一个参数更像。

另外我们还可以看到,两个变量都是以拷贝的形式赋值给具体实现函数的变量的,因此,就像我们知道的那样,普通的 Block 不能修改捕获的变量值。那么如果想要修改该怎么修改呢?我们都知道只要变量之前加上 __block,Block 捕获该变量后就可以修改了,那么这个的内部原理又是什么呢?这就是下面我们要讲的。

修改捕获变量的 Block

继续创建一个新的文件 block_3.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(void) {
int not_used = 0;
const char * name = "Jack";
__block int age = 10;
void (^blk)(void) = ^{
int next_age = age + 1;
printf("%s is %d years old. He will be %d years old next year.", name, age, next_age);
};
blk();
return 0;
}

clang 转换后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *name;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_name, __Block_byref_age_0 *_age, int flags=0) : name(_name), age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
const char *name = __cself->name; // bound by copy

int next_age = (age->__forwarding->age) + 1;
printf("%s is %d years old. He will be %d years old next year.", name, (age->__forwarding->age), next_age);
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src {
_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(void) {
int not_used = 0;
const char * name = "Jack";
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

这段源码明显比之前的多了很多代码,首先就是多了个结构体__Block_byref_age_0:

1
2
3
4
5
6
7
struct __Block_byref_age_0 {
void *__isa; // 指向 Class
__Block_byref_age_0 *__forwarding; // 指向自身
int __flags; // 标志位,Block 被 Copy 时用到
int __size; // 结构体大小
int age; // 捕获的变量
};

这个结构体是给变量加上 __block后生成的,用来保存变量的值。

看一下 main 函数,原先的

1
__block int age = 10;

就变成了

1
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

即 age 的类型变成了__Block_byref_age_0结构体类型

1
__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

可以看到实例化这个结构体的时候,__forwarding指向自身,至于为什么这么做,下面会再说。

再来看一下具体实现的函数:

1
2
3
4
5
6
7
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
const char *name = __cself->name; // bound by copy

int next_age = (age->__forwarding->age) + 1;
printf("%s is %d years old. He will be %d years old next year.", name, (age->__forwarding->age), next_age);
}

上面 age 变量使用指针来接收的,事实上,在结构体__main_block_impl_0中,age 成员变量也是一个指针,我们将 main 函数中 age 变量的 __forwarding 成员赋值给它。

1
__Block_byref_age_0 *age; // by ref

因为是指针,所以,我么你就可以在 Block 中愉快地修改捕获的变量了。

我们看到实际上它修改变量的语句是:

1
int next_age = (age->__forwarding->age) + 1;

age->__forwarding->age这个估计会让很多人很不理解。为什么之前不直接把 age 变量自身赋值给 __main_block_impl_0中的 age 成员,而是将 age->__forwarding 赋给 age 成员呢?虽然现在他们指向的都是同一个东西,即自由变量 age 自身。这个 __forwarding看起来十分多此一举。

事实肯定不是这样。我们刚刚用到的 Block 都是设置在栈上的,这也是为什么__block_impl结构体的 isa一直是 &_NSConcreteStackBlock的原因。但并不总是如此,比如说 Block 执行 copy 操作的时候,就会将 Block 从栈拷贝到堆上。此时,Block所持有的 __block 变量也会一并拷贝到堆上。

这个时候可以解释使用 __forwarding的原因了,即“不管 __block变量分配在栈上还是堆上,都能够正确地访问该变量”。

具体来说,就是最开始,Block__block 变量都在栈上,则 __forwarding也指向自身,即栈上的自己。到这里没问题吧?现在,Block 执行copy 操作。造成的结果就是 Block 被拷贝到堆上,同时 __block 变量也被拷贝到堆上 。这时问题来了,假入没有__forwarding,那么原先堆上的 __block变量还在堆上,它在 main 函数中使用,而在 Block 中捕获的 __block变量,则在堆上,那么,两者指向的,不再是同一个__block变量了。现在因为有__forwarding,随着 Block 和__block变量被拷贝到堆上,原先栈上的__block变量的__forwarding指针也开始指向堆上的__block变量,而不再指向自己,而已经拷贝到堆上的__block变量还是指向自己。那么,栈上和堆上的 __block变量指向的还是同一个变量,这样就能保证他们修改的是一个变量了。

解释完上面的问题,回过头来继续看源码,还会看到多出这么两个函数:

1
2
3
4
5
6
7
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src {
_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
}

在当前源码中,这两个函数都没有被调用。事实上,他们是在 Block 从栈复制到堆时以及堆上的 Block 被废弃时会被调用的。

到这里,我要讲的 Block 的底层实现原理大致讲完了,当然我这里讲的肯定不是很全面,也只是分析了 Block 的三种情况对应的实现源码。

要想更完全地了解 Block,大家可以看一下我在 参考 中提到的那本书。

参考

《Objective-C高级编程:iOS与OS X多线程和内存管理》

Block实现原理