首页 Swift中的自动引用计数
文章
取消

Swift中的自动引用计数

自动引用计数(Automatic Reference Counting,简称 ARC)是 Swift 中一种内存管理机制。它能够自动追踪和释放不再需要的实例所占用的内存,从而使开发者专注于编写高效的 Swift 代码,而无需手动管理内存。

在本文中,我们将深入探讨 ARC 的工作原理、如何避免强引用循环以及其他相关内容。

引用计数

为了了解 ARC 工作原理,我们首先需要了解引用计数的概念。引用计数是一种跟踪一个对象被多少个变量、常量和其他属性所持有的技术。当一个对象被创建时,它的引用计数为 1。每当一个新的变量、常量或属性引用该对象时,引用计数都会增加 1。当一个对象的引用计数为 0 时,该对象将被释放。

这里有个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

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

reference1 = Person(name: "John")
// 输出 "John is being initialized"
reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil
reference3 = nil
// 输出 "John is being deinitialized"

在上面的示例中,我们定义了一个 Person 类。当我们分别将 reference1reference2reference3 赋值为指向 Person 实例的引用时,Person 实例的引用计数增加了 3。当我们将这些引用设置为 nil 时,Person 实例的引用计数减少了 3。因此,当 Person 实例的引用计数为 0 时,deinit 方法被调用并打印 "John is being deinitialized"

对于复杂的程序,手动管理引用计数可能会非常困难,甚至是不可行的。这就是 ARC 出现的原因。

ARC 工作原理

ARC 是一种编译时技术,它可以自动计算一个实例被多少变量、常量和属性所持有,并且在实例没有任何强引用时释放它们所占用的内存。ARC 通过以下方式来完成这项工作:

  • 当一个类的实例被创建时,它的引用计数自动设置为 1。
  • 当一个实例被赋值给一个变量、常量或属性时,该实例的引用计数增加 1。
  • 当一个变量、常量或属性不再引用该实例时,该实例的引用计数减少 1。
  • 当一个实例的引用计数为 0 时,它将被自动释放。

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

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

reference1 = Person(name: "John")
// 输出 "John is being initialized"

reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil

// 输出 "John is being deinitialized"

在上面的示例中,当我们第一次将 Person 实例赋值给 reference1 时,它的引用计数为 1。接着,我们将 reference2reference3 分别赋值为 reference1,此时 Person 实例的引用计数变为 3。当我们将 reference1reference2 设置为 nil 时,引用计数减少为 1。最后,当我们将 reference3 设置为 nil 时,Person 实例的引用计数变为 0,从而触发了 deinit 方法并输出”John is being deinitialized”`。

需要注意的是,ARC 只处理实例对象的内存管理,而不处理其他类型的内存管理,如值类型(例如结构体和枚举)和函数等。

强引用和弱引用

在上面的示例中,reference1reference2reference3 都是强引用。一个强引用是指一个变量、常量或属性对实例的引用,该引用会增加实例的引用计数。只要强引用存在,实例就无法释放。强引用通常用于保持一个对象在内存中的唯一有效引用。

除了强引用外,Swift 还提供了弱引用和无主引用,它们可以帮助避免强引用循环(也称为“循环引用”)。强引用循环是指两个或多个对象彼此持有强引用并因此都无法被释放。这种情况下,ARC 无法自动释放这些对象,因为它们的引用计数永远不会降为 0。

弱引用

弱引用(weak reference)是一种非强制性的引用,它不会增加实例的引用计数。当引用的实例被释放时,弱引用会自动设置为 nil。弱引用通常用于避免强引用循环。

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Person {
    var name: String
    var apartment: Apartment?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let number: Int
    weak var tenant: Person?
    init(number: Int) {
        self.number = number
    }
    deinit {
        print("Apartment #\(number) is being deinitialized")
    }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John")
unit4A = Apartment(number: 4)

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
// 输出 "John is being deinitialized"
// 输出 "Apartment #4 is being deinitialized"

在上面的示例中,我们定义了 PersonApartment 类。我们创建了一个弱引用 tenant,并将其设置为指向 Person 的引用。当我们将 john 设为 nil 时,由于 tenant 是一个弱引用,Person 实例被释放并打印 "John is being deinitialized"。同时,与 Person 关联的 Apartment 实例也被释放,并打印 "Apartment #4 is being deinitialized"

无主引用

无主引用(unowned reference)也是一种非强制性的引用,但是它假定引用的实例始终存在。与弱引用不同,无主引用不会自动设置为 nil。如果尝试访问已释放的无主引用,程序将崩溃。

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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")
    }
}

var john: Customer?
john = Customer(name: "John")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// 输出 "John is being deinitialized"
// 输出 "Card #1234567890123456 is being deinitialized"

在上面的示例中,我们定义了 CustomerCreditCard 类。我们创建了一个无主引用 customer,并将其设置为指向 Customer 的引用。当我们将 john 设为 nil 时,由于 card 是一个无主引用,与 Customer 关联的 CreditCard 实例被释放并打印 "Card #1234567890123456 is being deinitialized"。同时,Customer 实例也被释放并打印 "John is being deinitialized"

需要注意的是,如果尝试访问已释放的无主引用,程序会崩溃。因此,在使用无主引用时,必须确保引用的实例始终存在。

循环引用

循环引用(reference cycle)是指两个或多个对象彼此持有强引用并因此都无法被释放。这种情况下,ARC 无法自动释放这些对象,因为它们的引用计数永远不会降为 0。

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let number: Int
    var tenant: Person?
    init(number: Int) {
        self.number = number
    }
    deinit {
        print("Apartment #\(number) is being deinitialized")
    }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John")
unit4A = Apartment(number: 4)

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil
// 此时没有任何输出

在上面的示例中,我们定义了 PersonApartment 类。我们创建了一个强引用循环,Person 持有 Apartment 的强引用,而 Apartment 持有 Person 的强引用。当我们将 johnunit4A 都设置为 nil 时,由于强引用循环,它们的引用计数永远不会降为 0,因此不会触发任何 deinit 方法。

解决循环引用

为了解决这个问题,我们可以使用弱引用或无主引用来打破强引用循环。在上面的示例中,我们可以将 tenant 设置为一个弱引用或无主引用。下面是一个使用弱引用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let number: Int
    weak var tenant: Person?
    init(number: Int) {
        self.number = number
    }
    deinit {
        print("Apartment #\(number) is being deinitialized")
    }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John")
unit4A = Apartment(number: 4)

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
// 输出 "John is being deinitialized"
// 输出 "Apartment #4 is being deinitialized"

在上面的示例中,我们将 tenant 设置为一个弱引用。当我们将 john 设为 nil 时,由于 tenant 是一个弱引用,与 Apartment 关联的 Person 实例被释放并打印 "John is being deinitialized"。同时,Apartment 实例也被释放并打印 "Apartment #4 is being deinitialized"

下面是一个使用无主引用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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")
    }
}

var john: Customer?
john = Customer(name: "John")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// 输出 "John is being deinitialized"
// 输出 "Card #1234567890123456 is being deinitialized"

在上面的示例中,我们将 customer 设置为一个无主引用。当我们将 john 设为 nil 时,由于 card 是一个无主引用,与 Customer 关联的 CreditCard 实例被释放并打印 "Card #1234567890123456 is being deinitialized"。同时,Customer 实例也被释放并打印 "John is being deinitialized"

需要注意的是,在使用弱引用或无主引用时,必须确保引用的实例始终存在。否则,如果尝试访问已释放的弱引用或无主引用,程序会崩溃。

循环引用捕获

Swift 中的闭包(Closure)可以捕获外部变量,并保留其引用。这种情况下,如果闭包和被捕获的变量相互持有强引用,则会导致循环引用。为了避免这种情况,可以使用捕获列表(Capture List)修饰闭包,明确指定对被捕获的变量进行弱引用或无主引用。

以下是使用捕获列表的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class HTMLElement {
    let name: String
    let text: String?
    lazy var asHTML: () -> String = {
        [weak self] in
        if let text = self?.text {
            return "<\(self!.name)>\(text)</\(self!.name)>"
        } else {
            return "<\(self!.name) />"
        }
    }
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    deinit {
        print("\(name) is deallocated")
    }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello, world")
print(paragraph!.asHTML()) // <p>Hello, world</p>

paragraph = nil

在上述例子中,HTMLElement 类中的 asHTML 属性是一个闭包,它捕获了对 self 的弱引用。这样,即使 HTMLElement 类的实例已经被释放,闭包仍然能够访问 self 所指向的对象,并正确地生成对应的 HTML 代码。

内存泄漏检测和调试

在开发过程中,可能会遇到一些难以察觉的内存泄漏和野指针问题。为了更好地排查这些问题,我们可以使用 Xcode 中自带的工具来进行内存泄漏检测和调试。

Xcode 中的 Instruments 工具提供了多种内存分析功能,其中包括 Memory Debugger、Leaks 和 Zombies 等。Memory Debugger 可以分析进程的内存使用情况,Leaks 可以检测是否存在内存泄漏,Zombies 可以帮助我们找到野指针问题。

除了使用工具外,我们还可以在代码中手动添加调试信息,比如输出日志等。例如,在 Person 类和 Apartment 类中,我们添加了 deinit 方法,在对象被释放时输出一条日志信息。这样,我们就能够更清楚地了解对象的生命周期和内存释放情况。

本文由作者按照 CC BY 4.0 进行授权

Swift中的不透明类型

Swift内存安全