Swift 进阶: 自动引用计数原理

379 阅读4分钟

引言

大家好,我是一牛,今天我想和大家分享的是自动引用计数(ARC)在 Swift 中是如何工作的。在一些比较老的一些OC项目,大家或多或少都使用过手动引用计数(MRC),那个时候相信大家都很痛苦,不是这边忘记release,就是那边忘记retain。因为工作的需要,我有时候也会维护写MRC的项目,最佳实践就是逐个文件给它转换成ARC。自动引用计数极大的减少了需要为内存管理编写的样板代码,使得我们能够更加专注业务端代码,生产力进一步提高。然而,内存泄漏和循环引用问题依然存在,掌握自动引用计数的原理会让你在处理这类问题更加得心应手。

自动引用计数原理

我们先回顾下自动引用计数是如何工作的。

class C {
      deinit {
        print("instance is being deinitialized")
    }
}
var c1: C = nil
var c2: C = nil
var c3: C = nil
c1 = C() // 引用计数 +1
c2 = c1  // 引用计数 +1
c3 = c2  //	引用计数 +1
c2 = nil // 引用计数 -1
c3 = nil // 引用计数 -1
c1 = nil // 引用计数 -1, 引用计数为 0, C 的实例被销毁,系统回收内存

Screenshot 2025-01-16 at 14.05.26.png

  • 当一个强引用指向类的实例,引用计数 +1 。
  • 当一个强引用不再指向类的实例(变量出作用域),引用计数 -1 。
  • 当引用计数为 0 时, 对象被销毁,内存被回收。

循环引用

考察以下代码

class Person {
    var phone: Phone?
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("instance of Person is being deinit")
    }
}

class Phone {
    var owner: Person?
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("instance of Phone is being deinit")
    }
}

let phone = Phone(name: "Apple")
let someone = Person(name: "Jobs")
phone.owner = someone
someone.phone = phone

Screenshot 2025-01-16 at 16.33.25.png

实例phoneowner 属性强引用了实例 someone, 而实例someone 的属性phone 强引用了实例phone,这就形成了一个引用环。 尽管phone 和 变量 somephone出了作用域会使得对应的实例的引用计数 -1,根据自动引用计数规则,只要实例的引用计数不为0 就不会被释放。所以 实例phone和实例someone 不会被销毁,这就造成了内存泄漏。

如何打破引用环

正是因为存在引用环,内存才会泄漏,所以解决方案就是避免形成引用环。打破引用环,主要有两种方法,弱引用和无主引用。

弱引用

weak var phone: Phone?
//instance of Phone is being deinit
//instance of Person is being deinit

Screenshot 2025-01-17 at 14.51.36.png

简单添加weak关键字后,使用weak关键字引用的对象的引用计数不会 +1,引用环被打破, 实例someone的属性phone不再持有实例phone。此时只有变量phone强引用实例phone,当变量phone 出作用域时,实例phone的引用计数为 0, 实例phone被销毁。由于实例someone引用计数也为 0,实例someone被销毁。需要注意的是,weak 关键字只能使用在可选属性,这是因为,当weak属性指向的实例被销毁后,weak属性会被只为nil, 可以被安全访问。可以看到实例phone先于实例someone销毁。

无主引用

为了方便演示,我们稍稍改造下源程序。类 Person不变。

class Phone {
    unowned let owner: Person
    var name: String
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }
    deinit {
        print("instance of Phone is being deinit")
    }
}
let someone = Person(name: "Jobs")
let phone = Phone(name: "Apple", owner: someone)
someone.phone = phone

Screenshot 2025-01-17 at 14.37.15.png

和弱引用类似,无主引用的对象的引用计数不会 +1, 也就打破了循环引用。一般来说引用的实例生命周期和宿主对象一样或者更长时,才考虑使用无主引用。也就是在宿主对象没有被销毁之前,无主引用的实例不会被销毁,在宿主对象的生命周期访问引用的实例是安全的。

结语

虽然自动引用计数给程序员带来了极大的便利,让我们更能够专注业务开发,但是我们也要认识到掌握ARC原理的重要性, 在可能出现循环引用的时候,需要我们特别留心。

创作不易,点赞和收藏是我持续创作的动力,谢谢大家。