不一致。
虽然 __block 的核心目标(允许 Block 内部修改外部变量)在两个环境下是一样的,但它在内存管理语义上存在一个巨大的、甚至足以导致程序崩溃的差异。
1. 核心差异:是否会触发 Retain?
这是两者最本质的区别,直接决定了你会不会写出循环引用。
| 特性 | MRC (Manual Reference Counting) | ARC (Automatic Reference Counting) |
|---|---|---|
| 对对象的持有 | 不触发 Retain | 触发强引用 (Strong Retain) |
| 所有权语义 | 类似于 __unsafe_unretained | 类似于 __strong |
| 解决循环引用 | 可以直接用 __block 打破 | 必须配合 __weak 使用 |
在 MRC 下:
当你用 __block 修饰一个对象时,Block 从栈拷贝到堆的过程,不会增加该对象的引用计数。
- 副作用:如果该对象被释放了,Block 内部的变量会变成野指针。
- 妙用:在 MRC 时代,开发者经常利用
__block这个特性来打破循环引用(因为不 Retain 就不会形成环)。
在 ARC 下:
当你用 __block 修饰一个对象时,Block 拷贝到堆上时,会强引用该对象。
- 逻辑:ARC 认为既然你要修改这个变量,我就必须保证它活着。
- 后果:如果你在 Block 内部使用
__block变量,且self持有 Block,依然会发生循环引用。
2. 内存结构的演进:__forwarding 指针
尽管语义不同,但两者在底层结构上保持了一致性,即引入了 __forwarding 指针。
-
栈到堆的迁移:当 Block 被拷贝到堆时,
__block变量也会被拷贝到堆。 -
指针重定向:
- 栈上的
__forwarding指向堆上的结构体。 - 堆上的
__forwarding指向自己。
- 栈上的
-
意义:这保证了无论你在 Block 内部还是外部修改变量,操作的都是堆上那份唯一的数据。
3. 代码行为对比
场景:试图打破循环引用
MRC 写法:
Objective-C
// 在 MRC 下,这样是安全的,不会循环引用
__block typeof(self) blockSelf = self;
self.block = ^{
[blockSelf doSomething];
};
ARC 写法:
Objective-C
// 在 ARC 下,这会导致循环引用!因为 __block 会强引用 self
__block typeof(self) blockSelf = self;
self.block = ^{
[blockSelf doSomething];
};
// 正确做法是使用 __weak
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething];
};
4. 为什么 ARC 要改变这个行为?
苹果在 ARC 中让 __block 触发强引用,是为了内存安全性。
在 MRC 时代,__block 不 Retain 虽然能打破循环引用,但也极易导致野指针崩溃(对象在 Block 执行前被销毁了)。ARC 的哲学是“安全优先”,所以它让 __block 默认强持有,而将打破循环引用的任务交给了专门的 __weak。
5. 总结
- MRC 下的
__block:是“非持有”的。修改变量的功能 + 打破循环的功能。 - ARC 下的
__block:是“强持有”的。仅保留修改变量的功能,打破循环需另寻__weak。
💡 进阶避坑指南
如果你在 ARC 下看到某些老代码通过 __block 来防止循环引用,那大概率是从 MRC 时代迁移过来的。在现代开发中,请记住: __block 用于修改值,__weak 用于打破环。