Кто убирает за объектами
Каждый раз, когда вы создаёте объект класса, ему выделяется место в памяти. Рано или поздно это место нужно освободить — иначе программа будет постепенно съедать всю доступную память, пока не упадёт. Вопрос в том, кто следит за тем, что объект больше никому не нужен, и в какой момент его можно убрать.
В одних языках за это отвечает отдельный процесс, который время от времени останавливает программу и ищет мусор среди объектов. В Swift выбран другой путь: компилятор сам считает, сколько мест в коде ссылается на объект, и освобождает его ровно тогда, когда счётчик доходит до нуля. Этот механизм называется ARC — Automatic Reference Counting, автоматический подсчёт ссылок.
Сильные ссылки и счётчик
Когда вы присваиваете экземпляр класса переменной, константе или свойству, вы создаёте сильную ссылку (strong reference) на этот объект. У каждого объекта класса есть невидимый счётчик — сколько сильных ссылок на него сейчас существует. Пока счётчик больше нуля, объект живёт в памяти. Как только последняя сильная ссылка исчезает — переменная выходит из области видимости, свойство перезаписывается другим значением, — счётчик становится равен нулю, и объект немедленно освобождается.
Важно: несколько переменных могут указывать на один и тот же объект. Присваивание let second = first не создаёт копию объекта — оно создаёт вторую сильную ссылку на тот же самый объект, и счётчик увеличивается на единицу.
class Session {
let id: Int
init(id: Int) { self.id = id }
}
let first = Session(id: 1)
let second = first
// счётчик ссылок у объекта Session сейчас равен 2
Тут first и second — это две ссылки на один объект, а не два объекта. Изменение через одну ссылку будет видно и через другую, потому что это буквально одна и та же вещь в памяти.
deinit как наблюдаемый момент освобождения
У класса можно объявить deinit — специальный блок без параметров, который Swift вызывает сам, в тот самый момент, когда счётчик ссылок на объект доходит до нуля. Именно через deinit удобно увидеть работу ARC: если внутри него напечатать сообщение, вы буквально услышите момент освобождения объекта.
class Resource {
let name: String
init(name: String) {
self.name = name
print("Открыт ресурс: \(name)")
}
deinit {
print("Ресурс закрыт: \(name)")
}
}
print("Программа началась")
do {
let resource = Resource(name: "файл.txt")
print("Работаем с ресурсом: \(resource.name)")
}
print("Область видимости завершилась")
Блок do { ... } здесь используется не для обработки ошибок, а просто как область видимости: всё, что объявлено внутри фигурных скобок, живёт только до закрывающей скобки. Переменная resource — единственная сильная ссылка на объект Resource, и как только выполнение доходит до конца блока do, эта ссылка исчезает, счётчик обнуляется, и Swift сразу вызывает deinit. Программа напечатает пять строк в таком порядке: сначала «Программа началась», потом «Открыт ресурс», потом «Работаем с ресурсом», потом «Ресурс закрыт» — и только после этого «Область видимости завершилась». Освобождение происходит не когда-нибудь потом, а именно в момент выхода из блока, до следующей строки кода.
Что делает компилятор, а что не приходится делать вам
В языках без ARC и без сборщика мусора программист сам вызывает что-то вроде «выделить память» и «освободить память», и забытый вызов освобождения — частая причина утечек. В Swift вы никогда не пишете подобные команды руками. Компилятор сам расставляет в скомпилированном коде операции увеличения и уменьшения счётчика ссылок — при каждом присваивании сильной ссылки, при каждом выходе переменной из области видимости, при каждом удалении элемента из массива, который хранил ссылку на объект. Вы просто пишете обычный код с переменными и областями видимости, а ARC синхронно, по ходу выполнения, поддерживает счётчик в точности каждого объекта.
Чем это отличается от сборщика мусора
Сборщик мусора (garbage collector), который используется во многих других языках, устроен иначе: он не считает ссылки на лету, а время от времени запускает отдельный проход по памяти, ищет объекты, до которых больше нет пути от «живых» переменных, и удаляет их все разом. Из-за этого момент освобождения непредсказуем — объект может провисеть в памяти ещё какое-то время после того, как стал не нужен, а сама проверка способна на мгновение притормозить программу.
ARC работает по-другому: решение о том, где в коде нужно проверить счётчик, компилятор принимает заранее, во время компиляции, а не гадает во время выполнения. Освобождение происходит детерминированно — сразу, в конкретной строке, где обнуляется последняя сильная ссылка, без отдельной паузы на «уборку». Это удобно предсказуемостью, но накладывает свою цену: если два объекта держат сильные ссылки друг на друга, их счётчики никогда не дойдут до нуля сами по себе — с такой ситуацией вы разберётесь отдельно, когда пойдёт речь про слабые ссылки.
Почему у структур такого вопроса нет
Всё это касается только классов. Структуры и перечисления — типы значения: при присваивании переменной, передаче в функцию или сохранении в массив они не делятся одним объектом с общим счётчиком, а копируются целиком. У копии нет понятия «последняя ссылка на общий объект», поэтому у структур нет ни ARC, ни deinit — они просто перестают существовать вместе с переменной, которая их хранила, без отдельного наблюдаемого события.
Частые ошибки
- Считать, что
let second = firstсоздаёт новый объект класса. На самом деле это вторая сильная ссылка на тот же самый объект — счётчик увеличивается, а не появляется копия. - Думать, что объект освобождается «через какое-то время после того, как стал не нужен», как при сборке мусора. В Swift это происходит сразу и в конкретном месте — как только счётчик сильных ссылок доходит до нуля.
- Пытаться вызвать
deinitвручную или добавить ему параметры — Swift вызывает его сам и только сам, без параметров. - Ждать
deinitу структуры — у типов значения его нет вообще, потому что нет разделяемого объекта со счётчиком ссылок. - Забывать, что объекты внутри коллекции (например, массива) тоже держат сильную ссылку: пока объект лежит в массиве, счётчик не обнулится, даже если переменная, через которую его создали, давно вышла из области видимости.
Резюме
ARC— автоматический подсчёт сильных ссылок на объект класса; компилятор сам вставляет в код операции учёта ссылок, без ручных вызовов.- Каждое присваивание экземпляра класса переменной или свойству создаёт ещё одну сильную ссылку на тот же объект, а не копию.
- Объект освобождается сразу, как только счётчик сильных ссылок на него доходит до нуля — обычно в момент выхода последней ссылающейся переменной из области видимости.
deinit— блок у класса, который Swift вызывает автоматически в момент освобождения объекта; вручную его вызвать нельзя, параметров у него нет.- В отличие от сборщика мусора, ARC освобождает память детерминированно и сразу, а не отдельным проходом во время выполнения.
- У структур и перечислений ARC и
deinitне нужны: как типы значения, они копируются целиком, а не делят общий объект со счётчиком ссылок.
Проверьте себя
4 вопроса
Наблюдаем освобождение объекта
Напишите программу верхнего уровня, которая наглядно показывает работу ARC.
Объявите класс с любым названием и хранимым свойством-именем. В инициализаторе напечатайте сообщение о том, что объект создан (включив в сообщение значение свойства). В deinit напечатайте сообщение о том, что объект освобождён (тоже со значением свойства).
Затем в коде верхнего уровня: напечатайте любое сообщение о начале программы, откройте вложенную область видимости через do { ... }, создайте внутри нее экземпляр вашего класса и присвойте его константе, напечатайте сообщение о том, что объект используется, закройте область видимости закрывающей скобкой и напечатайте сообщение о том, что область видимости завершилась.
Программа должна успешно скомпилироваться и завершиться без ошибок — конкретный текст сообщений не проверяется, важно само поведение: сообщение об освобождении из deinit должно появиться после выхода из do-блока и до финального сообщения о завершении области видимости.