Swift Автоматический подсчет ссылок (ARC)

Swift использует автоматический подсчёт ссылок (ARC) для отслеживания и управления использованием памяти вашего приложения.

В большинстве случаев нам не нужно вручную освобождать память, поскольку ARC автоматически освобождает память, занятую экземпляром класса, когда этот экземпляр больше не используется.

Но в некоторых случаях нам всё же нужно реализовать управление памятью в коде.

Как работает ARC

  • Каждый раз, когда вы используете init() для создания нового экземпляра класса, ARC выделяет блок памяти для хранения информации об экземпляре.

  • Эта память содержит информацию о типе экземпляра, а также значения всех связанных с ним свойств.

  • Когда экземпляр больше не используется, ARC освобождает память, занятую экземпляром, и делает освобождённую память доступной для других целей.

  • Чтобы гарантировать, что используемые экземпляры не будут уничтожены, ARC отслеживает и подсчитывает, сколько свойств, констант и переменных в данный момент ссылаются на каждый экземпляр.

  • Когда экземпляр присваивается свойству, константе или переменной, они создают сильную ссылку на экземпляр. Пока сильная ссылка всё ещё существует, экземпляр не может быть уничтожен.

Пример с ARC

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) 开始初始化")
    }
    deinit {
        print("\(name) 被析构")
    }
}

// 值会被自动初始化为nil,目前还不会引用到Person类的实例
var reference1: Person?
var reference2: Person?
var reference3: Person?

// 创建Person类的新实例
reference1 = Person(name: "Example")


//赋值给其他两个变量,该实例又会多出两个强引用
reference2 = reference1
reference3 = reference1

//断开第一个强引用
reference1 = nil
//断开第二个强引用
reference2 = nil
//断开第三个强引用,并调用析构函数
reference3 = nil

Вывод вышеприведённой программы:

Example 开始初始化
Example 被析构

Циклы сильных ссылок между экземплярами классов

В приведённом выше примере ARC отслеживает счётчик ссылок созданного вами экземпляра Person и уничтожает его, когда экземпляр Person больше не нужен.

Однако мы можем написать код, в котором у класса никогда не бывает нуля сильных ссылок. Это происходит, когда два экземпляра класса содержат сильные ссылки друг на друга, не позволяя друг другу быть уничтоженными. Это называется циклом сильных ссылок.

Пример

Ниже показан пример случайного создания цикла сильных ссылок. В примере определяются два класса: Person и Apartment, которые моделируют квартиру и её жильцов:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) 被析构") }
}

class Apartment {
    let number: Int
    init(number: Int) { self.number = number }
    var tenant: Person?
    deinit { print("Apartment #\(number) 被析构") }
}

// 两个变量都被初始化为nil
var example: Person?
var number73: Apartment?

// 赋值
example = Person(name: "Example")
number73 = Apartment(number: 73)

// 意感叹号是用来展开和访问可选变量 example 和 number73 中的实例
// 循环强引用被创建
example!.apartment = number73
number73!.tenant = example

// 断开 example 和 number73 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁
// 注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。
// 强引用循环阻止了Person和Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏
example = nil
number73 = nil

Разрешение циклов сильных ссылок между экземплярами классов

Swift предоставляет два способа решения проблемы цикла сильных ссылок, с которой вы сталкиваетесь при использовании свойств класса:

  • Слабые ссылки
  • Бесхозные ссылки

Слабые и бесхозные ссылки позволяют одному экземпляру в цикле ссылок ссылаться на другой экземпляр, не сохраняя сильную ссылку. Это позволяет экземплярам ссылаться друг на друга, не создавая цикл сильных ссылок.

Используйте слабую ссылку для экземпляров, которые могут стать nil в течение своего времени жизни. И наоборот, используйте бесхозную ссылку для экземпляров, которые после присваивания во время инициализации никогда больше не будут установлены в nil.

Пример слабой ссылки

class Module {
    let name: String
    init(name: String) { self.name = name }
    var sub: SubModule?
    deinit { print("\(name) 主模块") }
}

class SubModule {
    let number: Int
    
    init(number: Int) { self.number = number }
    
    weak var topic: Module?
    
    deinit { print("子模块 topic 数为 \(number)") }
}

var toc: Module?
var list: SubModule?
toc = Module(name: "ARC")
list = SubModule(number: 4)
toc!.sub = list
list!.topic = toc

toc = nil
list = nil

Вывод вышеприведённой программы:

ARC 主模块
子模块 topic 数为 4

Пример бесхозной ссылки

class Student {
    let name: String
    var section: Marks?
    
    init(name: String) {
        self.name = name
    }
    
    deinit { print("\(name)") }
}
class Marks {
    let marks: Int
    unowned let stname: Student
    
    init(marks: Int, stname: Student) {
        self.marks = marks
        self.stname = stname
    }
    
    deinit { print("学生的分数为 \(marks)") }
}

var module: Student?
module = Student(name: "ARC")
module!.section = Marks(marks: 98, stname: module!)
module = nil

Вывод вышеприведённой программы:

ARC
学生的分数为 98

Циклы сильных ссылок, вызванные замыканиями

Цикл сильных ссылок также может возникнуть, когда вы присваиваете замыкание свойству экземпляра класса, и тело замыкания использует этот экземпляр. Тело замыкания может обращаться к свойству экземпляра, например self.someProperty, или вызывать метод экземпляра, например self.someMethod. В обоих случаях замыкание «захватывает» self, что приводит к циклу сильных ссылок.

Пример

Следующий пример показывает, как создаётся цикл сильных ссылок, когда замыкание ссылается на self. В примере определяется класс HTMLElement, который моделирует отдельный HTML-элемент с помощью простой модели:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        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 being deinitialized")
    }
    
}

// 创建实例并打印信息
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

Класс HTMLElement создаёт цикл сильных ссылок между экземпляром класса и замыканием, используемым как значение по умолчанию для asHTML.

Свойство asHTML экземпляра хранит сильную ссылку на замыкание. Однако замыкание использует self в своём теле (ссылаясь на self.name и self.text), поэтому замыкание захватывает self, а это означает, что замыкание, в свою очередь, хранит сильную ссылку на экземпляр HTMLElement. Это создаёт цикл сильных ссылок между двумя объектами.

Разрешение циклов сильных ссылок, вызванных замыканиями: определите список захвата как часть замыкания при определении замыкания. Таким образом можно разорвать цикл сильных ссылок между замыканием и экземпляром класса.


Слабые и бесхозные ссылки

Когда замыкание и захваченный экземпляр всегда ссылаются друг на друга и всегда уничтожаются в одно и то же время, определите захват в замыкании как бесхозную ссылку.

И наоборот, когда захваченная ссылка иногда может быть nil, определите захват в замыкании как слабую ссылку.

Если захваченная ссылка никогда не будет установлена в nil, следует использовать бесхозную ссылку, а не слабую ссылку.

Пример

В предыдущем примере с HTMLElement бесхозная ссылка — правильный способ разорвать цикл сильных ссылок. Напишите класс HTMLElement следующим образом, чтобы избежать цикла сильных ссылок:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        [unowned 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) 被析构")
    }
    
}

//创建并打印HTMLElement实例
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

// HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息
paragraph = nil

Вывод вышеприведённой программы:

<p>hello, world</p>
p 被析构
Другие расширения