__block 修饰符的作用是什么?它是如何实现修改外部变量的?
__block 修饰符是 Objective-C 中 Block 编程的一个核心概念。它的主要作用是允许 Block 内部修改外部定义的局部变量,并且处理变量在内存中的生命周期管理。
以下是关于 __block 的作用及其底层实现原理的详细解析。
一、 __block 的作用
1. 允许在 Block 内部修改变量
默认情况下,Block 捕获外部局部变量是值拷贝(Capture by Value)。这意味着 Block 内部持有的是变量被捕获时的“快照”,是只读的。如果你尝试在 Block 内部修改一个没有 __block 修饰的局部变量,编译器会报错。
加上 __block 后,Block 捕获的是该变量的引用(地址),从而允许修改。
代码示例:
// ❌ 错误示范
int a = 10;
void (^block)(void) = ^{
a = 20; // 编译报错:Variable is not assignable (missing __block type specifier)
};
// ✅ 正确示范
__block int b = 10;
void (^block)(void) = ^{
b = 20; // 正常修改
};
block();
NSLog(@"%d", b); // 输出 20
2. 延长变量的生命周期
当 Block 从栈(Stack)拷贝到堆(Heap)时(例如 Block 被赋值给属性或作为返回值),它捕获的 __block 变量也会被移动到堆上。这样即使原来的作用域结束了(栈帧销毁),Block 依然可以安全地访问和修改这个变量。
二、 底层实现原理:它是如何修改外部变量的?
__block 的魔法在于:编译器会将 __block 修饰的变量包装成一个对象(结构体)。
1. 结构体的转换
当编译器看到 __block int val = 10; 时,它不仅仅是在栈上分配一个 int,而是生成了一个结构体(struct)。
大致的结构体定义如下(简化版):
struct __Block_byref_val_0 {
void *__isa; //以此表明它是一个对象
struct __Block_byref_val_0 *__forwarding; // 指向自身的指针(关键!)
int __flags;
int __size;
int val; // 变量的真实值
};
2. __forwarding 指针(核心机制)
这个 __forwarding 指针是实现“修改外部变量”以及“栈堆同步”的关键。
初始状态(在栈上):
当__block变量刚创建在栈上时,结构体里的__forwarding指针指向它自己(栈上的这个结构体地址)。Block 拷贝到堆上(Copy):
当 Block 被拷贝到堆上时,它持有的__block变量结构体也会被拷贝一份到堆上。
此时发生了一件非常重要的事情:栈上那个结构体的__forwarding指针,会被修改指向堆上的那个新结构体。 而堆上的结构体的__forwarding指向它自己。统一访问入口:
无论是在 Block 内部,还是在 Block 外部,代码中对该变量的读写,编译器都会转换成通过__forwarding指针来访问。转换后的访问逻辑大致如下:
plaintext// 访问 val (valStruct->__forwarding->val) = 20;
3. 为什么这样就能修改了?
通过 __forwarding 指针,保证了操作的一致性:
- 如果 Block 在栈上:
__forwarding指向栈上的结构体,修改的是栈上的值。 - 如果 Block 移到了堆上:
- Block 内部持有的是堆上的结构体,通过
__forwarding修改堆上的值。 - Block 外部(原来的栈作用域)持有的虽然是栈上的结构体,但因为栈上结构体的
__forwarding已经指向了堆,所以外部修改时,实际上也是顺着指针修改了堆上的那个值。
- Block 内部持有的是堆上的结构体,通过
结论: 无论你在哪里修改,最终修改的都是堆上那份唯一的内存数据,从而实现了变量的共享和修改。
三、 内存管理细节 (ARC 环境)
对象类型的
__block:
在 ARC 下,如果__block修饰的是一个对象(如__block NSObject *obj),Block 会对该对象进行强引用(Retain)。- 注意: 这与 MRC 时代不同(MRC 下
__block不会 retain 对象,常用来避免循环引用)。在 ARC 下,要避免循环引用,通常需要配合__weak使用,即__weak打破循环,__block负责修改(虽然现在很少为了修改对象指针本身而用__block,更多是修改基本数据类型)。
- 注意: 这与 MRC 时代不同(MRC 下
循环引用风险:
如果你在 Block 内部使用了__block修饰的self,在 ARC 下会导致循环引用(Self -> Block -> __block变量 -> Self)。- 解决方案: 使用
__weak。
plaintext__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf doSomething]; };- 解决方案: 使用
总结
- 作用:
__block允许 Block 内部修改外部变量,并负责将变量从栈拷贝到堆,延长其生命周期。 - 原理:
- 将变量包装成一个
__Block_byref结构体。 - 利用结构体内部的
__forwarding指针。 - 当 Block 发生 Copy 时,栈上的
__forwarding指向堆上的结构体,确保无论从哪里访问,操作的都是同一块内存地址。
- 将变量包装成一个