2-18.【OC】【内存管理】 __block 在 MRC 和 ARC 下行为是否一致?

1 阅读3分钟

不一致

虽然 __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. 总结

  1. MRC 下的 __block:是“非持有”的。修改变量的功能 + 打破循环的功能。
  2. ARC 下的 __block:是“强持有”的。仅保留修改变量的功能,打破循环需另寻 __weak

💡 进阶避坑指南

如果你在 ARC 下看到某些老代码通过 __block 来防止循环引用,那大概率是从 MRC 时代迁移过来的。在现代开发中,请记住: __block 用于修改值,__weak 用于打破环。