Подсчёт ссылок
Чему вы научитесь:
- объяснять, когда Swift освобождает объект класса и когда срабатывает
deinit; - находить цикл сильных ссылок, из-за которого объекты не освобождаются;
- разрывать такой цикл через
weakиunownedи выбирать между ними; - писать
[weak self]в замыкании, которое хранится в самом объекте.

Представьте книгу на подставке в читальном зале библиотеки. Каждый, кто с ней работает, получает карточку читателя. Счётчик на подставке показывает, сколько карточек выдано: взяли карточку — плюс один, вернули — минус один.
Пока выдана хотя бы одна карточка, книга кому-то нужна и остаётся на подставке. Когда возвращают последнюю, счётчик показывает ноль, и библиотекарь сразу ставит книгу на её место на полке.
Так Swift обращается с объектами классов. Каждый объект занимает место в памяти. Пока объект нужен, он там лежит, а потом память нужно вернуть, иначе программа будет только расти. Вопрос в том, как понять, что объект больше не нужен. Посмотрим, когда это случается:
class Book {
let title: String
init(title: String) {
self.title = title
}
deinit {
print("\(title) is back")
}
}
var anna: Book? = Book(title: "Dune")
print("Anna is reading") // Anna is reading
anna = nil // Dune is back
print("The end") // The endДавайте разберём программу по строкам:
- Класс
Bookхранит название, а блокdeinitпечатает строку, когда книгу освобождают. var anna: Book? = Book(title: "Dune")создаёт объект и кладёт вannaссылку на него — карточку читателя. Тип опциональный, чтобы потом положить тудаnil.print("Anna is reading")печатает строку: книга уanna.anna = nilвозвращает карточку. Других ссылок на книгу нет, и прямо на этой строке срабатываетdeinit: программа печатаетDune is back.print("The end")печатает последнюю строку, когда книги уже нет.
Блок deinit
Swift вызывает блок deinit прямо перед тем, как освободить объект. Это пара к init: init готовит объект к работе, а deinit выполняется в самом конце его жизни.
Что важно знать о deinit:
- его пишут без скобок и параметров:
deinit { … }; - его вызывает сам Swift, а строка
book.deinit()не скомпилируется; - внутри ещё доступны свойства объекта, поэтому
deinitможет напечататьtitle; deinitбывает у классов, а у структур, которые вы писали, его нет.
В настоящих программах в deinit закрывают то, что объект открыл, например файл. В уроках этой темы deinit печатает строку, чтобы было видно, когда объект освобождён.
Как Swift считает ссылки
Swift ведёт счётчик для каждого объекта класса, как библиотекарь у подставки. Такой способ управлять памятью называют ARC — автоматический подсчёт ссылок.
Ссылку, которая держит объект, называют сильной ссылкой. Все ссылки, которые вы писали до сих пор, сильные. Объект держат:
- переменная или константа;
- свойство другого объекта;
- элемент массива.
Счётчик меняется по трём правилам:
- Новая сильная ссылка прибавляет единицу.
- Исчезнувшая ссылка отнимает единицу.
- Когда счётчик становится нулём, Swift вызывает
deinitи сразу освобождает память объекта.
Нажимайте «Следующий шаг» и следите за счётчиком в рамке main: deinit срабатывает на той строке, где счётчик становится нулём.
1class Book {2 let title: String3 init(title: String) {4 self.title = title5 }6 deinit {7 print("\(title) is back")8 }9}10 11var anna: Book? = Book(title: "Dune")12var boris = anna13anna = nil14print("Anna is done")15boris = nil16print("Boris is done")
Book(title:) создаёт объект, и init заполняет его свойство title.
Строка anna = nil книгу не освободила: у boris осталась своя карточка. Объект живёт, пока на него есть хотя бы одна сильная ссылка, и неважно, какая из них появилась первой.
Когда ссылка исчезает
Переменной присвоили nil
Так было в примерах выше. Чтобы положить в переменную nil, ей нужен опциональный тип, например Book?.
В переменную положили другой объект
Старая ссылка при этом исчезает:
var boris: Book? = Book(title: "Emma")
boris = Book(title: "Ulysses") // Emma is back
print(boris?.title ?? "none") // Ulyssesboris отпускает книгу Emma и берёт Ulysses. Других ссылок на Emma нет, поэтому её deinit срабатывает на строке присваивания.
Функция закончилась
Ссылка из константы внутри функции исчезает не позже, чем закончится вызов:
func visit() {
let book = Book(title: "Odyssey")
print("Reading \(book.title)") // Reading Odyssey
}
visit() // Odyssey is back
print("After visit") // After visitКнига Odyssey освобождается до того, как программа напечатает After visit. Точную строку Swift не обещает: ссылка в константе может исчезнуть и раньше конца функции, сразу после последнего обращения к book. Поэтому в уроках момент освобождения задаём явно, через nil.
Программа завершилась
Если к концу программы на объект ещё есть ссылка, его deinit не сработает вовсе. Программа завершается целиком, и система забирает всю её память разом. Поэтому не ждите строки из deinit у объекта, который до конца лежит в переменной.
ARC и сборщик мусора
Здесь картинка с библиотекой неточна. В библиотеке счётчик ведёт человек, а в Swift счёт встроен в саму программу. Компилятор заранее добавляет команды «прибавить» и «отнять» к строкам, где ссылка появляется или исчезает. Поэтому во время работы никто не ищет ненужные объекты: счётчик меняется на каждой такой строке.
На собеседовании часто спрашивают, чем ARC отличается от сборщика мусора. В Java, C# и Go память освобождает сборщик мусора — часть программы, которая время от времени просматривает память и ищет объекты, до которых больше не добраться. Ответ можно строить так:
- ARC освобождает объект сразу, на той строке, где исчезла последняя сильная ссылка;
- сборщик мусора освобождает ненужные объекты позже, когда до них дойдёт очередной просмотр;
- поэтому при ARC момент вызова
deinitпредсказуем, а просмотров памяти в фоне нет.
Частая ошибка
Книгу положили в массив, потом присвоили переменной nil и ждут строку из deinit:
var hamlet: Book? = Book(title: "Hamlet")
var library = [Book]()
library.append(hamlet!)
hamlet = nil // ✗
print(library.count) // 1Строка hamlet = nil ничего не печатает. Элемент массива — тоже сильная ссылка, и у книги осталась одна. deinit сработает, когда книгу уберут из массива: library.remove(at: 0) напечатает Hamlet is back.
Главное
- ARC считает сильные ссылки на каждый объект класса и освобождает объект, как только счётчик становится нулём.
- Объект держит любая сильная ссылка: переменная, свойство другого объекта, элемент массива.
deinitсрабатывает на строке, где исчезла последняя ссылка; если ссылка жива до конца программы, он не сработает.- В отличие от сборщика мусора ARC ничего не ищет в памяти: компилятор встраивает счёт в программу.