Blocks 的实现
Block 的实现是面试中高频出现的问题,背后的原因我想是希望借此考察面试者对 Block 的掌握程度,在日后的工作中能够用好它;同时能从侧面反映面试者有没有深入钻研技术,以及独立思考能力如何,可谓一举多得。
下面我们就来看看 ObjC 中的 Blocks 是如何实现。Clang 的 -rewrite-objc
选项可以将含有 Block 语法的源代码转换为 C++,说是 C++,其实也仅使用了 struct 结构,其本质是 C 语言。
下面我们先转换一个简单的文件试试:
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 |
|
可以看到持有 block 的变量实际上就是指针,而 block 本身则是结构体,在我们的例子中对应的是 __main_block_impl_0
,功能代码则是通过函数来实现的,block 结构体内有成员变量指向该函数,这样我们对 block 的实现渐渐清晰起来了。
Block 有一个重要的特性–自动捕获变量。这又是怎么实现的呢?我们同样可以使用上述的方法来得到答案。我们构造一个捕获变量的例子,然后来查看它的结果:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
可以看到自动捕获的标量数据是直接声明为 block 结构体的成员变量。
除了读取捕获自动变量的值,block 还支持使用 __block
修饰符来修改自动捕获的变量。我们同样来看个例子:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
|
可以看到使用 __block
修饰的变量实际上转换成了结构体变量,同样在 block 结构体中有成员变量指向它们。
上面我们看过了使用 block 时的几种情况,我们可以尝试来总结使用 block 的情况,然后查看各种情况转换之后的代码来进一步探索 block 的实现,进而得到比较完善的答案。
首先 block 可以按是否捕获变量分为两大类,其次捕获变量时根据是否支持修改又可以分为两类,最后捕获变量又可以分为程序的数据区域、栈上和堆上三种情况。综上,我们可以得到得到如下的 block 分类列表:
- 不捕获变量(1)
捕获变量
不修改捕获的变量
- 存在程序数据区的变量(2)
- 存在栈上的变量(3)
- 存在堆上的变量(4)
修改捕获的变量(
__block
修饰的变量)- 存在程序数据区的变量(5)
- 存在栈上的变量(6)
- 存在堆上的变量(7)
这样算下来应该是存在七种情况,我们可以分别构造各种情况的例子,然后得到 block 的实现全貌。
全局变量和 static 变量是程序数据区变量,block 中访问全局变量和在其他地方没有什么不同,所以 block 的实现中不需要对它进行特别考虑。Static 变量在捕获时会在 block 结构体中有对应的成员变量,可以用该成员变量来读写。由于它在程序的生命周期中一直存在,所以当 block 捕获并修改它时,不需要生成对应的结构体变量,这和其他 __block
修饰的变量不同。
情况三和四比较类似,它们都会在 block 结构体中增加相应的成员变量,不同之处是捕获堆上的变量, block 的描述结构体变量中会增加 copy 和 dipose 函数,用来管理对应的内存。
情况六和七也类似,它们都是将变量转换为结构体,然后在 block 结构体增加成员变量指向它们。捕获堆上的变量时,block 内的成员变量指向变量,而这个变量是指向堆上分配的一块内存的,也就是一个对象,对象就是一块内存区域嘛,用代码示例如下:
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 |
|
看到这里,我们有了 block 捕获变量出了作用域后还能存在原因的线索,当表示 block 的结构体从栈上拷贝到堆上,如果是只读变量,它的值赋值给 block 结构体的成员变量了;如果是 __block
修饰的变量,表示该变量的结构体也会一并拷贝到堆上,并由 block 持有和管理。
至此,我们应该对 block 的实现比较清晰了。
Reference
- Objective-C 高级编程