Swift 与 Objective-C

Objective-C 内存管理史:从 MRC 到 ARC 全面总结

进行 iOS 开发时,总有一件事会让人好奇。

6 分钟阅读
Objective-C 内存管理史:从 MRC 到 ARC 全面总结 封面图

进行 iOS 开发时,总有一件事会让人好奇。

“为什么旧的 Objective-C 代码里写满了 retainrelease?”

如果你是从 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)时代,这个数字需要人工管理。

写满 retain 和 release 的旧代码,第一次看到确实会让人困惑
写满 retain 和 release 的旧代码,第一次看到确实会让人困惑

规则很明确,大家常用“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)”。

开发者不再需要编写 retainrelease,看起来似乎很方便。

但结果并不理想。

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 也不是万能的。

由于它无法自动解除相互持有的“循环引用”,开发者必须使用 weakunowned 等弱引用将其断开。

这个概念也原样延续到了我们今天使用的 Swift。


常见问题(Q&A)

问:现在还需要学习 MRC 吗?

实际工作中几乎不会再用 MRC 编写新代码。不过,旧库或面试中可能会问到相关概念,因此了解原理仍然有帮助。

问:ARC 中也会使用 autorelease 吗?

虽然不会直接调用,但这个机制在 ARC 下仍然运行。因此,@autoreleasepool代码块现在仍然是有效工具。

问:Swift 也使用 ARC 吗?

是的,Swift 也基于 ARC。因此,循环引用和 weak 概念在 Swift 中同样重要。


简而言之,Objective-C 内存管理从“手动计数的 MRC → 短暂的 GC 实验 → 编译器计数的 ARC”一路发展而来。

如今理所当然使用的自动内存管理,其实是在无数次崩溃和试错中逐渐完善的结果。

希望这段演进能成为正在学习 iOS 开发者的一张小地图。祝你今天也编程愉快!


参考资料

延伸阅读