Swift 作为一门现代的高级语言,自然少不了自动内存管理。我们知道,JAVA 中的内存管理是通过垃圾回收完成的。作为从 Objective-C 时代演变过来的 Swift,自然也继承了很多 OC 的特性。OC 中的内存管理分为两种:一种是垃圾回收,这种内存管理方式只被运用于 OS X(现在应该叫 macOS)中;另一种就是我们经常提到的 ARC(自动引用计数),这种管理方式被同时用在 iOS 和 macOS 上。在 Swift 中,内存管理依然是通过 ARC 进行的,下面我们就深入地看一看。
什么是 ARC?
ARC,中文名叫自动引用计数,全名叫 Automatic Reference Counting。顾名思义,意思就是“自动的”引用计数。
在我们深入了解 ARC 之前,我们先看一看什么叫做引用计数。我们先通过一个类来看一看 Swift 中对象在内存中的各个阶段。
假如我们有一个 Person 类:
class Person {
var name: String
init(name: String) {
self.name = name
print("Person \(name) has been initialized.")
}
deinit {
print("Person \(name) has been deallocated.")
}
}
这个简单的 Person 类,我们给它指定了一个 name 的属性,同时提供了 init 构造方法和 deinit 析构方法。在构造方法中,我们让控制台输出一条语句,提醒我们某个对象已经初始化。同时在析构方法中,我们同样让控制台输出某个对象的内存已经释放。
首先我们创建一个 person1 对象,然后看看控制台的输出:
do {
let person1 = Person(name: "Kenneth")
}
可以看到在 do 语法块内,我们创建的对象先进行了初始化,然后运行完毕后释放了内存。
Swift 中一个对象的生存周期也正如此,包含:
- 内存分配 (从栈或者堆中获得内存)
- 初始化 (执行 init 函数)
- 使用 (对象的使用)
- 析构(执行析构函数)
- 内存释放 (内存返回到栈或者堆内)
ARC 正是基于这个周期帮助我们管理内存的。当我们创建一个对象,保存一个对这个对象的引用,ARC 就一直监视我们对于该对象的引用,并记下当前有多少个对于该对象的引用。当引用数量 >0 时,内存将不会被释放。当引用数量等于 0 时,ARC 认为该对象不再被需要,于是便自动地帮助我们销毁该对象,并释放该对象的内存。
ARC 失效?
在大多数情况下,ARC 工作地很好,这也让我们不需要对内存管理花太多心思。ARC 遵循的原则简单而且有效,但它并不是万能的。在 ARC 的管理下,内存泄漏仍然可能发生。
让我们想象这样一个情景:我们有两个对象,这两个对象不再被需要,但是这两个对象互相引用了对方。
在上述情境中我们实际上不需要这两个对象,但是因为这两个对象互相引用了对方,这两个对象的引用计数都是 1。因为这两个对象的引用计数都是 1,ARC 将永远不会释放这两个对象的内存。
在这种情况下,内存泄漏就不可避免地发生。我们把这种情况叫做引用循环,更准确的说,是强引用循环。
让我们用一段代码来演示这个场景。
首先我们新建一个简单的 iPhone 类,这个类有两个属性,一个是型号,一个是主人:
class iPhone {
let model: String
var owner: Person?
init(model: String) {
self.model = model
print("iPhone \(model) has benn initialized.")
}
deinit {
print("iPhone \(model) has benn deallocated")
}
}
然后我们在 do 语法块里面新建一个 iPhone 对象:
do {
let person1 = Person(name: "Kenneth")
let iphone1 = iPhone(model: "7 Plus")
}
然后我们看控制台的输出语句,这个时候 ARC 工作正常,两个对象都在运行完成后被释放了内存:
下面我们在 Person 类中新建一个属性,这个属性的类型是一个 iPhone 类的数组,表示该人拥有的所有 iPhone。同时我们将该属性的 setter 方法设为私有,这样强制了使用我们自定义的方法进行 set。
private(set) var iphones: [iPhone] = [iPhone]()
func add(iphone: iPhone) {
iphones.append(iphone)
iphone.owner = self
}
然后我们在 do 语法块中添加一条语句:
do {
let person1 = Person(name: "Kenneth")
let iphone1 = iPhone(model: "7 Plus")
person1.add(iphone: iphone1)
}
这时候我们再看控制台:
可以看到这个时候的控制台少输出了两天释放内存的提示。这说明我们刚才的这两个对象并没有被释放。
为什么?
就像我们上面所说的一样,这个时候发生了强引用循环。我们的 person1 对象中的 iphones 属性保有了 iphone1 对象,而我们 iphone1 中的 owner 属性又保有了 person1。这个就是我们所说的强引用循环。
破解之道?
弱引用(Weak Reference)
为了打破所谓的强引用循环,我们开始思考,有没有一种办法能够让我们同时可以进行引用但是又不增加引用计数呢?弱引用就是这样诞生的。
所谓弱引用,就是很弱的引用——它引用一个对象而不增加该对象的引用计数。
上图中,虚线箭头表示一个弱引用。可以看到对象 2 弱引用对象 1 时,对象 1 的引用计数并没有增加。
当变量 1 和变量 2 不存在后,对象 1 的引用计数变成 0,对象 1 会被 ARC 销毁,销毁后对象 1 对对象 2 的引用也不存在,对象 2 也会被 ARC 销毁。强引用循环就这样被打破了。
针对上一段的代码,我们只要在 iPhone 类的 owner 属性前加一个 weak,即可以把该引用改为弱引用:
weak var owner: Person?
这时候我们再看控制台:
可以看到这个时候两个对象的内存都会被正常释放,弱引用起作用了。
你可能会问,那我们保存的弱引用这个时候指向了什么呢?我们回过头来看一看 owner 属性的类型,实际上是一个 Optional。聪明的你肯定能猜到,弱引用指向的对象如果内存被释放,会自动变为空引用,也就是 nil。
无主引用(Unowned Reference)
弱引用 weak 作为解决强引用循环的一个关键字,它将会在被引用对象的内存释放后变为 nil。Swift 中还提供了另外一个关键字——unowned,它也能达到引用对象而不增加其引用计数的效果。和 weak 关键字不同的是,weak 关键字会在对象内存释放后变为 nil,而 unowned 会继续保有对该对象的引用,即使该引用已经变为无效。
在上一段,我们知道 weak 关键字描述的属性必须申明为可变变量并且是 Optional 类型,这是因为它可能会变成 nil。
unowned 关键字则必须不是 Optional 类型,也就是说我们在访问 unowned 变量时,不需要进行对于 Optional 类型的解包操作,可以直接访问。然而这也造成了如果我们的 unowned 变量所指向的对象内存被释放后,我们再访问这个变量会出现运行时错误。
三种引用可使用的情景:
| | var | let | Optional | 非 Optional |
| ------------- |:-------------:|
| 强引用(Strong) | ✔️ | ✔️|✔️|✔️|
| 弱引用(Weak) | ✔️ | ❌|✔️|❌|
| 无主引用(Unowned) | ✔️ | ✔️|❌|✔️|
我们用一段代码来演示 unowned 关键字使用的情景。假设我们现在有两个类,一个 Customer 类,和一个 CreditCard 类。我们知道,一张信用卡一定有一个主人,而一个消费者不一定有一张信用卡。因而我们需要在 Customer 类设计一个 Optional 的信用卡属性,然后在 CreditCard 类设计一个非 Optional 的消费者类。同时我们为了避免强引用循环的产生,我们把 CreditCard 类中的消费者属性设计为 unowned。
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
然后我们创建两个对象,看一看控制台是否正常输出:
do {
let cus1 = Customer(name: "Kenneth")
let cus2 = CreditCard(number: 6222136579841254, customer: cus1)
}
在这种情况下,CreditCard 类和 Person 类相互引用了对方,但是因为我们把 CreditCard 类中的 customer 属性设计成 unowned,就不会出现强引用循环。
闭包中的循环引用
TBD...