Swift 自动引用计数(ARC)(千字长文)

Swift 自动引用计数(ARC):内存管理的隐形守护者

在 Swift 开发中,我们常常会听到一个词——“自动引用计数(ARC)”。它就像一位看不见的管家,默默管理着程序运行时的内存资源。你不需要手动分配或释放内存,Swift 会通过 ARC 自动完成这项工作。但如果你不了解它的运作机制,就可能在某些场景下遇到内存泄漏或意外的引用问题。

今天我们就来深入聊聊 Swift 自动引用计数(ARC)的原理和实践,帮助你从“用得对”走向“用得懂”。


什么是自动引用计数(ARC)?

在 Swift 中,所有类(class)类型的实例都是通过引用(reference)来访问的。这意味着多个变量或常量可以指向同一个对象实例。那么问题来了:什么时候该释放这个对象的内存?如果没人用了,就得销毁它;但如果还有人在用,就不能随便删。

这就是 ARC 的核心任务:自动追踪并管理对象的引用数量,当引用计数变为 0 时,自动释放内存

你可以把 ARC 想象成图书馆的借书系统。每本书(对象)都有一个“被借出次数”的计数器。只要还有人借着,就不能还回去;当没人借了,系统就自动把书放回书架(释放内存)。


ARC 如何工作?引用计数的增减逻辑

ARC 通过维护一个引用计数器来记录当前有多少个变量或常量在引用某个对象。每当一个新变量引用该对象时,计数器加 1;当引用被移除或变量超出作用域时,计数器减 1。

来看一个简单例子:

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) 被创建")
    }
    
    deinit {
        print("\(name) 被销毁")
    }
}

var person1: Person? = Person(name: "Alice")
// 引用计数 +1:此时为 1
// 输出:Alice 被创建

var person2 = person1
// person2 引用同一个实例,引用计数 +1 → 变为 2

person1 = nil
// person1 失去引用,引用计数 -1 → 变为 1

person2 = nil
// person2 也失去引用,引用计数 -1 → 变为 0
// 此时 ARC 触发 deinit,输出:Alice 被销毁

注释说明:

  • Person 类中定义了 deinit,这是析构函数,对象被销毁前会自动调用。
  • var person1: Person? 使用可选类型,允许为 nil,避免野指针。
  • 每次赋值给变量,ARC 都会自动更新引用计数。
  • 当引用计数归零,Swift 立即调用 deinit 并释放内存。

弱引用与无主引用:打破循环引用

虽然 ARC 很聪明,但它也有“卡住”的时候。当两个对象互相持有对方的强引用时,就会形成循环引用(retain cycle),导致它们的引用计数永远无法降到 0,从而无法被释放。

举个生活中的例子:

你和朋友合伙开了一家咖啡馆。你们俩都把对方的联系方式存在自己的手机里,还都设为“必看”提醒。结果呢?谁也删不掉对方,因为只要对方还在,自己就不能删。这就像两个对象互相强引用,谁也走不了。

在 Swift 中,这种问题非常常见,尤其是在闭包(closure)和委托模式中。

如何解决?使用弱引用(weak)和无主引用(unowned)

弱引用(weak)

weak 引用不会增加引用计数。它通常用于可选类型,表示“我引用你,但你不一定要活着”。

class Teacher {
    let name: String
    var student: Student?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 被销毁")
    }
}

class Student {
    let name: String
    // 使用 weak 修饰,避免强引用循环
    weak var teacher: Teacher?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 被销毁")
    }
}

// 使用示例
var teacher: Teacher? = Teacher(name: "Mr. Smith")
var student: Student? = Student(name: "Lily")

// 建立双向引用
student?.teacher = teacher
teacher?.student = student

// 释放
teacher = nil
student = nil
// 输出:
// Mr. Smith 被销毁
// Lily 被销毁

注释说明:

  • student?.teacher = teacher 是强引用。
  • weak var teacher: Teacher? 是弱引用,不增加计数。
  • teacher 被设为 nilstudentteacher 也会自动变为 nil,打破循环。
  • 最终两个对象都能被正确释放。

无主引用(unowned)

unownedweak 类似,但不支持可选类型,且假定引用的对象始终存在。如果引用对象被释放,访问它会触发运行时错误。

适用于那些生命周期一定比引用者长的场景。

class Manager {
    let name: String
    var team: [Employee] = []
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 被销毁")
    }
}

class Employee {
    let name: String
    // 使用 unowned,因为 Employee 的生命周期不会超过 Manager
    unowned let manager: Manager
    
    init(name: String, manager: Manager) {
        self.name = name
        self.manager = manager
    }
    
    deinit {
        print("\(name) 被销毁")
    }
}

// 使用示例
var manager: Manager? = Manager(name: "Alice")
var employee = Employee(name: "Bob", manager: manager!)

manager?.team.append(employee)
manager = nil // manager 被释放
// employee 的 manager 引用仍然有效,但 manager 已被释放
// 此时访问 employee.manager 会崩溃!

注释说明:

  • unowned let manager: Manager 表示 manager 一定存在,但不会增加引用计数。
  • manager 先被释放,再访问 employee.manager,程序会崩溃。
  • 适合用于“强依赖”关系,比如视图控制器和其子视图。

闭包中的循环引用:常见陷阱

闭包是 Swift 中非常强大的特性,但也是循环引用的重灾区。

class Timer {
    var name: String
    var completion: (() -> Void)?
    
    init(name: String) {
        self.name = name
    }
    
    func start() {
        print("Timer \(name) 开始计时")
        
        // 闭包中捕获了 self,形成强引用
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            print("Timer \(self.name) 结束")
            self.completion?()
        }
    }
    
    deinit {
        print("Timer \(name) 被销毁")
    }
}

var timer: Timer? = Timer(name: "Alarm")
timer?.completion = {
    print("提醒:闹钟响了!")
}

timer?.start()
timer = nil
// 注意:这里 timer 被设为 nil,但闭包中仍持有 self,引用计数不为 0
// 所以 Timer 永远不会被销毁!

注释说明:

  • DispatchQueue.main.asyncAfter 的闭包捕获了 self,形成强引用。
  • 即使外部 timer = nil,闭包仍在,引用计数不为 0。
  • 造成内存泄漏!

如何修复?使用捕获列表(capture list)

func start() {
    print("Timer \(name) 开始计时")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        [weak self] in  // 使用 weak 捕获 self
        guard let self = self else { return }
        print("Timer \(self.name) 结束")
        self.completion?()
    }
}

注释说明:

  • [weak self] 表示在闭包中以弱引用方式捕获 self
  • guard let self = self else { return } 用于安全解包,避免空值。
  • 一旦外部 timer = nilself 就变成 nil,闭包执行后自动释放,打破循环。

ARC 与值类型:为什么结构体和枚举不需要 ARC?

在 Swift 中,结构体(struct)和枚举(enum)是值类型,它们不通过引用访问,而是直接拷贝数据。

struct Point {
    var x: Double
    var y: Double
}

var p1 = Point(x: 1.0, y: 2.0)
var p2 = p1  // p2 是 p1 的副本,不是引用
p1.x = 10.0
print(p2.x) // 输出 1.0,p2 与 p1 无关联

注释说明:

  • p2 = p1 是值拷贝,不是引用。
  • 每个变量拥有独立的内存空间。
  • 不涉及引用计数,所以不需要 ARC。

因此,只有类(class)类型才需要 ARC 管理内存。结构体和枚举由编译器自动管理,更高效且不易出错。


实践建议:写出安全的 ARC 代码

  1. 优先使用 weakunowned:当存在双向引用时,明确使用弱或无主引用。
  2. 闭包中务必检查捕获列表:使用 [weak self][unowned self] 防止循环引用。
  3. 避免过度使用可选类型weak 引用必须是可选类型,合理设计结构。
  4. 使用 deinit 做清理工作:比如取消订阅、释放资源等,确保资源回收。
  5. 用 Xcode 的 Instruments 检测内存泄漏:运行时观察对象是否被正确释放。

结语

Swift 自动引用计数(ARC)是 Swift 内存管理的核心机制,它让我们摆脱了手动管理内存的繁琐与风险。但理解其背后的原理,才能写出更安全、更高效的代码。

记住:ARC 是智能的,但不是万能的。它无法自动识别循环引用,尤其是闭包中的隐式捕获。只有当你主动干预(如使用 weakunowned),才能真正避免内存泄漏。

从今天起,每次写类、闭包时,都问问自己:这个引用会不会造成循环?我是否需要弱引用?当你开始有意识地思考引用关系,你就真正掌握了 Swift 的内存管理艺术。

记住,好代码不仅“能跑”,更要“不漏”。而 Swift 自动引用计数(ARC)正是你实现这一目标的坚实后盾。