• Swift引用计数:为什么weak引用会比strong引用慢?
  • 发布于 1天前
  • 61 热度
    0 评论
前言
昨天在 Jacob's Tech Tavern 看到一篇关于 Swift 引用计数底层实现的文章,之前一直以为 weak 和 strong 就是简单得计数差别,没想到背后还有这么多门道。今天来聊聊 Swift 引用计数到底怎么工作的,为什么 weak 引用比 strong 引用慢,unowned 引用为什么会崩溃。

Swift 引用计数机制
Swift 用 ARC (Automatic Reference Counting) 来管理内存,原理是给每个对象记一个数字,表示有多少个地方在引用它。
class Person {
    let name: String
    init(name: String) { self.name = name }
}

var person1: Person? = Person(name: "张三") // 引用计数 = 1
var person2 = person1 // 引用计数 = 2  
person1 = nil // 引用计数 = 1
person2 = nil // 引用计数 = 0,对象被回收
看起来挺简单,但底层实现其实挺复杂。

Strong 引用
每个 Swift 对象在内存里都有个 HeapObject 头部,里面存着引用计数。
[对象类型信息] [引用计数] [对象实际数据...]
写 let obj = MyClass() 时:
1.创建对象,引用计数 = 1
2. obj 变量指向对象
3. obj 超出作用域,引用计数 - 1
4. 引用计数变 0,对象被销毁
这个过程比较快,因为引用计数就在对象旁边,CPU 一次就能读到。

Weak 引用慢在哪
weak 引用比 strong 引用慢,原因是需要额外的数据结构。
比如这种情况:
class Parent {
    var child: Child?
}
class Child {
    weak var parent: Parent? // 避免循环引用
}
如果 Parent 对象被销毁了,所有指向它的 weak 引用都要自动变成 nil。问题是系统怎么知道哪些 weak 引用指向这个对象?答案是 Side Table。

Side Table 机制
从 Swift 4 开始,系统引入了 Side Table。每当有 weak 引用指向某个对象时,系统会创建一个额外的数据结构来记录。
对象: [类型] [引用计数] [数据...]
       ↓
Side Table: [weak引用列表] [unowned引用列表] [其他信息]
这导致三个性能问题:
1. 多一次内存访问
访问 weak 引用时,CPU 要先读对象引用计数,再读 Side Table 信息,最后确定对象是否还活着。而 strong 引用一次就够了。
2. 更多内存分配
创建 weak 引用时,如果对象没有 Side Table,系统要额外分配内存。比直接操作引用计数慢。
3. 原子操作更复杂
多线程下,操作 Side Table 需要更复杂地锁机制。

Unowned 引用
unowned 引用的设计思路是:我确定被引用得对象生命周期比我长,不用担心它提前没掉。
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
}

class CreditCard {
    let number: UInt64
    unownedlet customer: Customer// 信用卡肯定属于某个用户
    init(number: UInt64, customer: Customer) { 
        self.number = number //堆代码 duidaim.com
        self.customer = customer
    }
}
unowned 引用不会增加引用计数,也不像 weak 那样自动变 nil。如果访问一个已经被销毁的对象得 unowned 引用——直接崩溃!

苹果这样设计是有原因的:
性能好:不需要 Side Table,不需要检查对象还活不活着
暴露问题:如果访问了已销毁的对象,说明代码逻辑有问题

强制正确使用:用 unowned 就得保证对象生命周期关系正确


实际使用建议
一般用 strong 引用
.性能好,不会崩溃

.注意避免循环引用


避免循环引用用 weak
.delegate 模式
.父子关系的反向引用

.闭包捕获 self


确定生命周期关系用 unowned
.子对象引用父对象
.不想写 if let 检查
// Delegate 用 weak
protocol MyDelegate: AnyObject {}
class MyClass {
    weak var delegate: MyDelegate?
}

// 父子关系用 unowned  
class Order {
    unowned let customer: Customer // 订单肯定属于某个客户
}
写在最后
Swift 内存管理看起来简单,底层其实挺复杂。了解这些原理地好处是知道什么时候用什么引用类型,遇到内存问题知道怎么处理。你在面试中被问到过内存管理相关的问题吗?欢迎在评论区分享你的经历!
用户评论