__block修饰符探究
Contents
前言
我们知道,在 block 里面能读取外部变量,但是如果需要修改外部变量的值,需要给变量加上 __block 修饰符才行。
接下来让我们带着2个问题来研究一下:
- 为什么不加
__block就只能读取,不能修改(即:block的实现,是怎么达到不能修改的) - 加了
__block为什么就能修改
block 的实质
先举个例子:
1 | int main(int argc, char * argv[]) { |
使用 clang -rewrite-objc 重写后,得到以下代码:
1 | struct __Block_byref_valB_0 { |
看 main 函数里面的代码:
可以看到,block 被转化成了一个 __main_block_impl_0 结构体对象;
上图中,调用该结构体的构造函数时,传入的第一个参数是一个 __main_block_func_0 类型的变量(这个 __main_block_func_0 是最终 block 里要执行的代码)将会由结构体里的 FuncPtr 成员接收,可以看到这是一个 函数指针;
看上图箭头处,之后 调用 block 就是通过这个 函数指针 去调用的,并且也会传入这个结构体对象:
1 | ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); |
以上简单介绍了一下 main 函数里的相关代码,附上该 block 的结构体构成:
1 | struct __main_block_impl_0 { |
其中结构体成员包括 valA 及 valB,不过 valB 的类型不是 int,这个我们后面会再说到。
小结
定义 block 后,实质会生成一个结构体对象;调用定义好的 block,实际就是通过这个结构体对象的函数指针(FuncPtr)去找到具体的实现。
不加 __block 修饰符
让我们先将之前 main 方法里的 valB 去掉:
1 | int valA = 1; |
首先,让我们看一下转换后的 main 方法相关代码:

可以看到,调用构造函数时,第三个参数是直接使用 valA 的值的。
再看结构体的声明及其 block 里的实现

看图中上面的箭头处,构造函数初始化参数时,是直接使用 val 的值;
我们将 block 看做一个函数,看上图中下面的箭头处,valA 在 block 函数 里也有定义,其值跟 外部的 valA 一样;
此时 block 函数 及 main 函数 在内存中的分布大致如下:

可以看到,valA 是在不同作用域的,这一点很关键。
这里谨记 函数调用的内存机制:
一个函数有一个函数自己的栈;如果函数和函数之间要共享内存,那这块被共享的内存不能在某一个函数的栈(stack)上,要在堆(heap)上
虽然在这里直接看转换后的代码, struct 里其实是可以修改内部的 valA 的,但是因为 valA 这个变量名,在内部和外部都存在,作用域是不同的,所以苹果在编译器层面就已经是禁止修改的。
小结
不加 __block 修饰时,valA 存在于多个不同的栈中,也就是说作用域不同,所以编译器才会禁止修改。
值得一提的是,我们上面举的例子是 基本类型 int,当换成 对象类型(如 NSString)时,转换后是 const 类型的,这是为什么?

因为结构体的构造函数,只是把调用者(如上面的
main函数)在栈上的valStr的指针地址传给了被调用者(如上面的block函数);如果不加const,那被调用者完全可能通过这个地址来修改这个捕获的对象的值,这个时候,如果被调用者的栈已经被回收了,那这个修改,就是在修改一处已经被回收的内存的内容,那就可能崩溃了。
PS: 这里为了演示方便,
block的定义跟调用都是写在main里。
实际上多数情况下,block是作为参数传递来做回调的;
(如在类 A 的实例方法 B中调用block,如果B 方法的栈被回收了,这时再修改里面的局部变量就会崩溃了)。
还值得一提的是,如果修改的是这个指针所 指向的对象的内容,不加 __block 也是可以的,如:
1 | NSMutableString *str = [NSMutableString stringWithString:@"DaGou"]; |
加 __block 修饰符
分析
根据上面相关说明,因为作用域不同,如果要能修改 block 里的值,有两种方法:
调用者(如
类 A 的实例方法 B)在调用block时,把自己的栈里的val的值通过地址的方式传进去;这种情况有一个问题,那就是
block没办法保证它自己存在的期间,B 方法也是存在的;也就是说,当block跑到一半的时候,B 方法的栈可能已经不在了;为了解决第一种带来的这种调用者和被调用者生命周期上的矛盾,于是引入了第二种方案:
把参数拷到heap上,这样一来,参数的存在与否,就不和调用者(B 方法)的栈的存在与否挂钩了;换言之就算B 方法的栈被回收了,这个时候heap上的val还是存在的。
验证
为了验证 block 里的 valB 到底是不是在堆里了,可以打印出地址算一下:

定义前的地址(16fd77a68)转成十进制: 6171359848
block 内的地址(170220bb8)转成十进制: 6176246712
由此可知两者地址之间相差:(6176246712 - 6171359848) / (1024 * 1024) = 4.66M
由于 iOS 里主线程的栈是 1M (非主线程是 512K,OS X 的主线程是 8M)(官方文档),所以定义前是在栈中,block 内及定义后就是在堆内了。
PS: 还可通过
pthread相关方法得到栈大小,可参考以下文章:
苹果官方文档
线程堆栈大小的使用介绍
说明
下面我们结合代码来具体说明一下第二种方案:
现在我们只使用带有 __block 修饰符的 valB
1 | __block int valB = 11; |
看下图,可以发现 __block int valB = 11 转换成了一个结构体 __Block_byref_valB_0,注意下图中上面的箭头,构造函数里传递的第二个参数是 &valB(即 valB 变量的首地址):
再看其它部分代码:
__Block_byref_valB_0 结构体声明如下:
1 | struct __Block_byref_valB_0 { |
可以看到第二个参数是 __Block_byref_valB_0 *__forwarding,而刚才已经说了 main 函数里 valB 的构造函数里传递的第二个参数是 &valB,所以 __forwarding 这是该实例自身的引用,内存结构大致如下:

根据我们之前的分析,经过 block 后,编译器会将 valB 拷贝至堆中,这时内存结构大致如下:

其中栈中(包括 block 及 main )的 __forwarding 指向堆中 valB 实例 首地址,堆中的 valB 实例 的 __forwarding 指向自身首地址。
再来看一下 block 里的具体代码实现,可以看到代码里已经是对堆里的内容进行修改了(使用 valB->__forwarding->valB):
1 | // block 里的具体定义 |
所以 block 里修改的实际是 堆 里的东西了。
最后还有一个问题,变量是怎样被 copy 到堆里的?
看下图,可以看到 clang -rewrite-objc 后生成的是 _NSConcreteStackBlock 类型的,是存在栈上的,而最终我们打断点会发现类型变成了 _NSConcreteMallocBlock ,这是因为在 ARC 环境下,编译器会自动将 block copy 到堆里,所以变量也会随之 copy 到堆里;如果是 MRC 环境,就需要手动 copy 了。

小结
加了 __block 后的变量,生成的是一个结构体变量,在经过 block (也就是定义 block )时,会将该结构体拷贝至堆中,栈内的 __forwarding 指向堆中的地址,之后对该变量的操作,实际上是对堆里的变量的操作。
另外,该结构体变量的 isa 指针有三种情况:
_NSConcreteStackBlock保存在栈中的block,出栈时会被销毁_NSConcreteGlobalBlock全局的静态block,不会访问任何外部变量_NSConcreteMallocBlock保存在堆中的block,当引用计数为0时会被销毁
上面我们生成的是 _NSConcreteStackBlock,像这样的就是 _NSConcreteGlobalBlock:
1 | void (^block)(void) = ^{NSLog(@"This is a Global Block");}; |
当使用 copy 操作时,生成的就是 _NSConcreteMallocBlock,并且我们可以看到转换后的代码还有 __main_block_copy_0、__main_block_dispose_0 这两个静态函数。
当 block 从栈中拷贝到堆中时,会调用前者持有该变量,当堆上的 block 回收时,会调用后者释放该变量。
以下是 ARC 环境下,block 有无强引用,内部有无使用外部变量生成的类型(测试时间是 2017-01-11):
| 有强引用 | 无强引用 | |
|---|---|---|
| 使用外部变量 | _NSConcreteMallocBlock | _NSConcreteStackBlock |
| 无外部变量 | _NSConcreteGlobalBlock | _NSConcreteGlobalBlock |

即:block 内没有使用到外部变量,生成的是 _NSConcreteGlobalBlock;
如果有使用外部变量,有强引用的是 _NSConcreteMallocBlock,无强引用的是 _NSConcreteStackBlock;
看完以上三种类型的 block 后,可以做一下这里的 题目 巩固一下
总结
在这里要非常感谢 @刘煌旭 解答了困扰我多日的一些问题!
这里最后简单说明一下:
一、不加
__block为什么不能修改(即苹果是如何让block里变量不能修改的):
外部的 valA 在 main 的栈 上有一份内存,同时 block 的栈 上也会拷贝 valA 的一份内存;
由于函数调用的内存机制,valA 在不同的栈上,作用域不同,所以是不能修改的。
并且如果是 对象类型 的,在 block 内部是 const 的,不能修改。
二、加了
__block为什么能修改:
加了 __block,会把变量拷贝到堆里,修改的实际是堆里的内容。
参考资料:
黑幕背后的__block修饰符
谈Objective-C block的实现
2017-01-09 21:34
Aevit
深大轻茶馆
