基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

Swift 的 ARC (自动引用计数) 机制是如何工作的?

知识点图片

Swift 的 ARC (Automatic Reference Counting,自动引用计数) 是 Swift 用于管理应用程序内存使用情况的机制。

简单来说,ARC 的工作原理是:跟踪并计算有多少个变量、常量或属性指向(引用)某个类的实例。当引用计数变为 0 时,ARC 会自动释放该实例所占用的内存。

以下是 ARC 机制的详细工作流程和核心概念:


1. 基本原理:计数器

ARC 仅适用于 引用类型 (Reference Types),即 Class(类)和闭包。结构体 (Struct) 和枚举 (Enum) 是值类型,不通过 ARC 管理。

  • 创建时 (+1):当你创建一个类的实例,并将其赋值给一个变量、常量或属性时,ARC 会为该实例分配内存,并将引用计数设为 1
  • 赋值时 (+1):如果你将该实例赋值给另一个变量,引用计数 +1
  • 断开时 (-1):当变量超出作用域、被赋值为 nil 或指向了其他对象时,原实例的引用计数 -1
  • 销毁时 (0):当引用计数归零时,ARC 判定该实例不再被需要,立即调用该实例的 deinit 方法并释放内存。

代码示例:

plaintext
class Person {
    let name: String
    init(name: String) { self.name = name; print("\(name) 初始化") }
    deinit { print("\(name) 被销毁") }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John") 
// 输出: "John 初始化"
// 引用计数: 1

reference2 = reference1 
// 引用计数: 2

reference3 = reference1 
// 引用计数: 3

reference1 = nil
reference2 = nil
// 引用计数: 1 (reference3 还在引用)

reference3 = nil
// 引用计数: 0
// 输出: "John 被销毁"

2. 强引用 (Strong Reference)

默认情况下,Swift 中的引用都是强引用

  • 只要强引用存在,对象就不会被销毁。
  • 上面的例子中,reference1 等都是强引用。

3. 循环引用 (Retain Cycles) —— ARC 的主要挑战

如果两个对象互相持有对方的强引用,就会产生循环引用

  • 对象 A 持有对象 B。
  • 对象 B 持有对象 A。
  • 即使外部没有变量指向它们,它们的引用计数永远不会降为 0(至少是 1)。
  • 结果:内存泄漏 (Memory Leak)。

解决办法:weakunowned

为了打破循环引用,Swift 提供了两种特殊的引用类型,它们不会增加引用计数。

A. 弱引用 (weak)

  • 特点:不会增加引用计数。
  • 类型:必须是 Optional (可选类型)var。因为对象被释放后,ARC 会自动将弱引用赋值为 nil
  • 适用场景:生命周期中,被引用的对象可能会比持有者先销毁(例如:Delegate 模式)。
plaintext
class Person {
    var apartment: Apartment?
}

class Apartment {
    // 使用 weak 打破循环
    weak var tenant: Person? 
}

B. 无主引用 (unowned)

  • 特点:不会增加引用计数。
  • 类型非 Optional,必须始终有值。
  • 适用场景:被引用的对象生命周期 大于或等于 持有者。也就是说,当持有者存在时,被引用的对象一定存在。
  • 风险:如果试图访问一个已经被释放的无主引用,程序会崩溃(类似野指针)。
plaintext
class Customer {
    var card: CreditCard?
}

class CreditCard {
    // 信用卡必须归属于某人,且人销毁了卡也没用了
    unowned let customer: Customer 
    init(customer: Customer) { self.customer = customer }
}

4. 闭包中的循环引用

闭包 (Closure) 也是引用类型。如果你在闭包内部访问了 self(例如 self.someProperty),闭包会捕获 self,从而持有 self 的强引用。如果 self 也持有这个闭包,就形成了循环引用。

解决办法:捕获列表 (Capture List)

plaintext
class HTMLElement {
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = { [weak self] in
        // 使用 [weak self] 将 self 变为弱引用
        // 在闭包内 self 变成了 Optional
        guard let self = self else { return "" }
        return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
}

5. ARC 与 垃圾回收 (GC) 的区别

这是面试中常见的问题。Swift 的 ARC 和 Java/C# 的 Garbage Collection (GC) 不同:

特性 ARC (Swift/Obj-C) Tracing GC (Java/C#)
执行时机 编译时 (Compile-time) 插入代码,运行时立即执行。 运行时 (Runtime) 不定期间歇性运行。
确定性 。对象不再被使用时立即销毁。 。你不知道对象何时会被回收。
性能开销 引用计数操作微小的 CPU 开销,平摊在运行过程中。 GC 触发时可能会导致程序短暂停顿 (Stop-the-world)。
循环引用 无法自动处理,需要开发者手动使用 weak/unowned 可以自动处理,GC 算法能检测到孤立的循环引用岛。

总结

  1. ARC 自动管理内存:通过计算引用数来决定何时释放对象。
  2. 引用计数为 0 即销毁
  3. 强引用 (Strong):默认,增加计数,保持对象存活。
  4. 弱引用 (Weak):不增加计数,对象销毁后自动变 nil,用于打破循环引用。
  5. 无主引用 (Unowned):不增加计数,假定对象始终存在,用于生命周期确定的循环引用。
  6. 闭包捕获列表[weak self][unowned self] 用于解决闭包引起的循环引用。
00:00
00:00