进行 iOS 开发时,总有一件事会让人好奇。
“为什么旧的 Objective-C 代码里写满了 retain 和 release?”
如果你是从 Swift 入门的,打开旧代码时可能也曾感到困惑。
今天我们来梳理 Objective-C 内存管理的历史,看看它如何从 MRC 发展到 ARC。
先说结论。
Objective-C 的内存管理从“开发者手动计数的时代(MRC)”进入了“由编译器代为计数的时代(ARC)”。
期间也曾短暂尝试垃圾回收,但 ARC 最终在 2011 年出现,确立了方向。
下面按时代逐一说明。
什么是引用计数?内存管理的基本概念
在进入历史之前,先掌握一个概念。
那就是引用计数(reference count)。
可以把每个对象想象成带有一个数字,用来统计“有多少人正在持有我”。
数字大于等于 1 时对象仍然存活,变成 0 时就会从内存中释放。
因此,持有对象时增加数字(retain),释放对象时减少数字(release)。
这个简单规则就是 Objective-C 内存管理的基础。
MRC 时代:开发者手动计数的时期
在 ARC 之前,也就是 MRC(Manual Retain Count)时代,这个数字需要人工管理。
规则很明确,大家常用“NARC”来记忆。
- 使用 New、Alloc、Retain、Copy 创建的对象由我负责
- 负责的对象使用完后必须执行
release
实际代码大概是这样。
// 创建对象后,引用计数会变成 1
NSObject *obj = [[NSObject alloc] init];
[obj retain]; // 计数 2
[obj release]; // 计数 1
[obj release]; // 计数 0 → 释放内存
问题就出在这里。
如果忘记 release,内存就会泄漏;如果释放得太早,访问已经消失的对象就会导致应用崩溃。
也就是所谓的“僵尸对象”崩溃。
你必须一直在脑中计算当前持有对象的计数,实在是一件让人费心的工作。
autorelease:“不是现在,稍后再释放”
MRC 时代还有一个棘手的情况。
那就是方法创建新对象并将其返回给外部时。
- (NSString *)greeting {
NSString *msg = [[NSString alloc] initWithString:@"你好"];
return msg; // 是我 alloc 创建的,所以应该 release ,但什么时候?
}
按照规则,调用 alloc 的一方必须执行 release。
但如果在 return 前 release,接收方还没来得及使用对象它就消失了;不 release 又会造成泄漏。
为了解决这个两难,autorelease应运而生。
它会预约“不要现在,而是稍后 release”。
autoreleased 对象会进入名为 autorelease pool 的等待列表,并在 pool 清空时(通常是 run loop 完成一轮时)统一执行 release。
这样,接收方就能安全地接收并使用对象。
像 [NSString stringWithFormat:] 这样无需 alloc 就能直接获取对象的便捷构造方法,返回的对象都采用了这种方式。
这个 autorelease pool 在 ARC 时代也以 @autoreleasepool 代码块的形式保留下来。它是处理循环中内存暴增的实用工具,因此我在另一篇文章中单独介绍了它。
垃圾回收为什么失败?
Apple 也知道这种不便。
因此,2007 年 Mac OS X 10.5 Leopard 引入了像 Java 一样自动清理内存的“垃圾回收(GC)”。
开发者不再需要编写 retain、release,看起来似乎很方便。
但结果并不理想。
GC 在后台运行,可能导致应用在难以预测的时刻短暂卡顿。更重要的是,对于重视性能和电池续航的 iPhone 来说负担太大。
因此它始终没有被引入 iOS。
最终,Mac 版 GC 也从 2012 年的 OS X 10.8 开始被弃用(deprecated)。
ARC 的出现:由编译器代为计数
随后在 2011 年,Apple 提出的解决方案就是 ARC(Automatic Reference Counting)。
它与 Xcode 4.2、iOS 5 和 OS X 10.7 Lion 一同发布。
ARC 的思路非常巧妙。
保留引用计数方式,但让编译器自动插入
retain·release,而不是交给开发者处理。
它不像 GC 那样在运行时额外执行清理任务,而是在编译时将释放代码自动插入到需要的位置。
这样几乎没有运行时性能开销,就实现了内存管理自动化。
将两个时代放在表格中比较如下。
| 分类 | MRC | ARC |
|---|---|---|
| 出现时间 | 早期~2011 年 | 2011 年(iOS 5) |
| retain/release | 手动编写 | 编译器自动插入 |
| autorelease | 直接调用 | 由编译器管理 |
| 内存泄漏风险 | 高 | 大幅降低 |
| 性能开销 | 无 | 几乎没有 |
不过,ARC 也不是万能的。
由于它无法自动解除相互持有的“循环引用”,开发者必须使用 weak 或 unowned 等弱引用将其断开。
这个概念也原样延续到了我们今天使用的 Swift。
常见问题(Q&A)
问:现在还需要学习 MRC 吗?
实际工作中几乎不会再用 MRC 编写新代码。不过,旧库或面试中可能会问到相关概念,因此了解原理仍然有帮助。
问:ARC 中也会使用 autorelease 吗?
虽然不会直接调用,但这个机制在 ARC 下仍然运行。因此,@autoreleasepool代码块现在仍然是有效工具。
问:Swift 也使用 ARC 吗?
是的,Swift 也基于 ARC。因此,循环引用和 weak 概念在 Swift 中同样重要。
简而言之,Objective-C 内存管理从“手动计数的 MRC → 短暂的 GC 实验 → 编译器计数的 ARC”一路发展而来。
如今理所当然使用的自动内存管理,其实是在无数次崩溃和试错中逐渐完善的结果。
希望这段演进能成为正在学习 iOS 开发者的一张小地图。祝你今天也编程愉快!

