基于本文回答

播面 播面

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

Strong、Weak、Unowned 引用在 Swift 中的具体实现和区别?

知识点图片

在 Swift 中,StrongWeakUnowned 是自动引用计数(ARC)管理内存的三种主要方式。理解它们的区别以及底层的实现机制(特别是 Side Table 的概念)是掌握 Swift 内存管理的关键。

以下是详细的深度解析:


1. Strong 引用 (强引用)

概念

这是 Swift 的默认引用类型。只要有一个强引用指向一个对象,该对象就不会被销毁。

具体实现

  • 引用计数 (Reference Counting):
    • Swift 的对象在堆(Heap)上的内存布局通常以 HeapObject 结构开始,其中包含 Metadata(元数据指针)和 RefCounts(引用计数)。
    • 当创建一个强引用时,运行时会调用 swift_retain,将该对象的 refCounts 中的强引用计数 +1
    • 当强引用离开作用域或被置为 nil 时,调用 swift_release,计数 -1
    • 当强引用计数降为 0 时,对象会被销毁(调用 deinit)。

区别与问题

  • 默认行为: 不需要特殊关键字。
  • 循环引用 (Retain Cycle): 如果对象 A 强引用 B,B 也强引用 A,两者的计数永远不会归零,导致内存泄漏。

2. Weak 引用 (弱引用)

概念

弱引用不会增加对象的“强引用计数”。如果一个对象只剩下弱引用,它会被销毁,并且所有的弱引用会自动变为 nil

语法特征

  • 必须是 Optional 类型 (var object: MyClass?)。
  • 必须是 var,因为它的值会在运行时改变。

具体实现 (核心机制:Side Table)

这是面试中的高频考点。Swift 的弱引用实现经历过优化,现代 Swift (4.0+) 使用了 Side Table (散列表/辅助表) 机制。

  1. RefCounts 的变化:

    • 对象的 RefCounts 字段是一个 64 位的位域。
    • 当一个对象第一次weak 引用指向时,运行时会创建一个 Side Table
    • 原本存储在对象头部的引用计数,会被替换为一个指向 Side Table 的指针(同时设置一个标志位,表示现在使用了 Side Table)。
  2. Side Table 结构:

    • Side Table 独立于对象内存存在。它存储了:
      • Strong Count (强引用计数)
      • Weak Count (弱引用计数)
      • Unowned Count (无主引用计数)
      • 指向原对象的指针
  3. 弱引用的指向:

    • Weak 引用并不直接指向对象内存,而是指向这个 Side Table。
  4. 置 nil 的过程:

    • 当对象的强引用计数归零,对象调用 deinit 并销毁内存。
    • 但是,Side Table 不会立即销毁,因为弱引用还指向它。
    • 当我们在代码中访问这个 weak 变量时,运行时会通过 Side Table 检查对象是否存活。
    • 如果对象已销毁(Strong Count 为 0),运行时会将这个弱引用变量写入 nil

3. Unowned 引用 (无主引用)

概念

无主引用也不增加强引用计数。但它假定对象在引用期间一直存在。如果对象被销毁后,你尝试访问 unowned 引用,程序会崩溃(Crash)。

语法特征

  • 通常是非 Optional 类型 (unowned let/var object: MyClass)。
  • 不需要解包。

具体实现 (Safe vs Unsafe)

Swift 中的 unowned 其实分为两种:unowned(safe)(默认)和 unowned(unsafe)

  1. Unowned (Safe) - 默认:

    • 实现: 同样利用了引用计数机制(在 Side Table 或 Inline RefCounts 中维护一个 Unowned Count)。
    • 生命周期: 当强引用归零,对象销毁(deinit),但对象的内存可能还没完全释放(变为“僵尸”状态),直到 Unowned 引用计数也归零。
    • 检查: 当你访问 unowned 引用时,Swift 运行时会检查对象是否已被销毁。如果已销毁,运行时会触发 swift_abortRetainUnowned,导致程序崩溃(Trap)。这是一种安全机制。
  2. Unowned (Unsafe):

    • 实现: 类似于 C/C++ 的原始指针(Raw Pointer)或 Objective-C 的 __unsafe_unretained
    • 行为: 它直接指向内存地址,不进行任何引用计数检查。
    • 风险: 如果对象销毁了,它就变成了悬垂指针 (Dangling Pointer)。访问它可能读到垃圾数据,也可能崩溃,行为不可预测。性能极高,但极不安全。

总结与对比

特性 Strong Weak Unowned (Safe)
引用计数 强引用计数 +1 弱引用计数 +1 (在 Side Table) 无主引用计数 +1
对象销毁 阻止销毁 不阻止销毁 不阻止销毁
变为 nil 是 (自动变为 nil) 否 (保持原有值)
类型要求 任意 必须是 Optional var 通常是非 Optional
访问已销毁对象 不可能 (对象活着) 返回 nil 运行时崩溃 (Crash)
底层指向 直接指向对象 指向 Side Table 指向对象 (但在访问时检查状态)
适用场景 默认引用,拥有关系 代理 (Delegate),可能为空的引用 相互引用且生命周期一致 (如信用卡与客户)

对象的生命周期 (结合 Side Table)

理解 Side Table 后,一个 Swift 对象的完整生命周期如下:

  1. Live (存活): Strong Count > 0。
  2. Deiniting (正在析构): Strong Count = 0,deinit 开始执行。
  3. Deinited (已析构): deinit 完成。此时 weak 引用访问会得到 nil。但如果还有 unowned 引用,对象的内存头(包含引用计数的部分)还保留着。
  4. Freed (内存释放): Unowned Count = 0。对象本身的内存被完全释放。但如果还有 weak 引用,Side Table 依然存在。
  5. Dead (彻底死亡): Weak Count = 0。Side Table 被释放。

面试回答建议 (Key Takeaways)

  1. Strong 是默认,造成循环引用。
  2. Weak 必须是 Optional,不持有对象,底层通过 Side Table 实现,对象销毁后自动置 nil。
  3. Unowned 不持有对象,不置 nil,访问已销毁对象会 Crash
  4. 选择建议:
    • 如果引用对象的生命周期比当前对象短(可能先死),用 weak
    • 如果引用对象的生命周期 >= 当前对象(同生共死),用 unowned(例如:闭包捕获 self,且确定闭包执行时 self 一定还在)。
00:00
00:00