ARC (自动引用计数) 的工作原理
ARC (Automatic Reference Counting,自动引用计数) 是 Apple 在 iOS 和 macOS 开发中(主要用于 Swift 和 Objective-C)使用的一种内存管理机制。
它的核心目标非常简单:确保当一个对象不再被需要时,它的内存会被自动释放;而当它还在被使用时,绝不会被释放。
以下是 ARC 工作原理的通俗详解:
1. 核心概念:引用计数器
你可以把每个对象想象成一个带有计数器的房间。
- 有人进入房间 (引用对象):计数器 +1。
- 有人离开房间 (放弃引用):计数器 -1。
- 房间空了 (计数器为 0):关灯,锁门,拆除房间 (释放内存)。
在代码层面:
- 每当你创建一个新对象,或者将一个对象赋值给一个变量、常量或属性时,引用计数会 +1。
- 每当这些变量离开作用域、被赋值为
nil或指向其他对象时,引用计数会 -1。 - 一旦引用计数变为 0,系统会立即销毁该对象(调用
deinit或dealloc),释放内存。
2. ARC 是如何“自动”工作的?
ARC 与 Java 或 C# 中的“垃圾回收 (Garbage Collection, GC)”不同。
- GC (垃圾回收):是在程序运行时,有一个后台进程定期扫描内存,查找不再使用的对象并清除。这可能会导致程序偶尔卡顿。
- ARC (自动引用计数):是在编译时工作的。
当你写代码时,编译器会分析你的代码,自动在合适的地方插入 retain (计数+1) 和 release (计数-1) 的代码。
例子:
plaintext
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) 被销毁了") }
}
// 1. 创建对象
var reference1: Person? = Person(name: "John")
// 此时 "John" 对象的引用计数 = 1
// 2. 赋值给另一个变量
var reference2 = reference1
// 此时 "John" 对象的引用计数 = 2 (因为 reference2 也指向它)
// 3. 断开第一个引用
reference1 = nil
// 此时 "John" 对象的引用计数 = 1 (reference2 还在用)
// 4. 断开第二个引用
reference2 = nil
// 此时 "John" 对象的引用计数 = 0 -> 内存立即释放,打印 "John 被销毁了"
3. 强引用与弱引用 (关键规则)
为了防止内存泄漏,ARC 引入了三种引用类型:
A. 强引用 (Strong Reference) - 默认
- 关键字:
strong(Objective-C), Swift 中默认就是强引用。 - 作用:只要强引用存在,对象就绝不会被销毁。它会使引用计数 +1。
- 比喻:你牵着一只狗的绳子,只要绳子在你手里,狗就跑不掉。
B. 弱引用 (Weak Reference)
- 关键字:
weak - 作用:它指向一个对象,但不拥有它。它不会增加引用计数。
- 特性:如果对象被销毁了,弱引用会自动变成
nil。因此,weak变量必须是可选类型 (Optional)。 - 比喻:你看着那只狗,但没牵绳子。如果牵绳子的人(强引用)放手了,狗跑了,你也就看不到了(变成 nil)。
C. 无主引用 (Unowned Reference)
- 关键字:
unowned - 作用:和
weak类似,不增加引用计数。 - 区别:它假设对象永远存在。如果对象被销毁了,你还去访问
unowned引用,程序会崩溃。通常用于“对象A存在时,对象B一定存在”的关系中。
4. 最大的坑:循环引用 (Retain Cycle)
ARC 虽然自动化了,但它处理不好一种情况:两个对象互相强引用。
场景:
- 对象 A 有一个属性指向对象 B (强引用)。
- 对象 B 有一个属性指向对象 A (强引用)。
结果:
当你把外部变量设为 nil 时,A 的计数器是 1 (因为 B 指向它),B 的计数器也是 1 (因为 A 指向它)。它们互相“抓着”对方,谁的计数器都变不成 0,导致内存泄漏(内存永远无法释放)。
解决方案:
将其中一端的引用改为 weak 或 unowned。通常是“子对象”对“父对象”的引用设为 weak(例如 Delegate 模式或闭包)。
5. 总结:ARC vs 垃圾回收 (GC)
| 特性 | ARC (iOS/Swift) | Garbage Collection (Java/Android) |
|---|---|---|
| 机制 | 引用计数 | 根对象可达性分析 |
| 时机 | 编译时插入代码,运行时实时释放 | 运行时不定时扫描 |
| 性能 | 平滑,无卡顿,内存占用较低 | 可能出现 "Stop the World" (卡顿),内存占用较高 |
| 确定性 | 对象何时销毁是确定的 | 对象何时销毁是不确定的 |
| 缺点 | 无法自动处理循环引用,需开发者注意 | 可以处理循环引用 |
一句话总结:
ARC 就是编译器帮你写了内存管理代码,只要你遵循“谁拥有谁”的规则(合理使用 strong 和 weak),内存管理就是全自动且高效的。