Перейти к содержимому
Теория · Память и ARC · урок 1 из 7

Подсчёт ссылок

Чему вы научитесь

Чему вы научитесь:

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

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

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

Так Swift обращается с объектами классов. Каждый объект занимает место в памяти. Пока объект нужен, он там лежит, а потом память нужно вернуть, иначе программа будет только расти. Вопрос в том, как понять, что объект больше не нужен. Посмотрим, когда это случается:

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

Давайте разберём программу по строкам:

  1. Класс Book хранит название, а блок deinit печатает строку, когда книгу освобождают.
  2. var anna: Book? = Book(title: "Dune") создаёт объект и кладёт в anna ссылку на него — карточку читателя. Тип опциональный, чтобы потом положить туда nil.
  3. print("Anna is reading") печатает строку: книга у anna.
  4. anna = nil возвращает карточку. Других ссылок на книгу нет, и прямо на этой строке срабатывает deinit: программа печатает Dune is back.
  5. print("The end") печатает последнюю строку, когда книги уже нет.

Блок deinit

Swift вызывает блок deinit прямо перед тем, как освободить объект. Это пара к init: init готовит объект к работе, а deinit выполняется в самом конце его жизни.

Что важно знать о deinit:

  • его пишут без скобок и параметров: deinit { … };
  • его вызывает сам Swift, а строка book.deinit() не скомпилируется;
  • внутри ещё доступны свойства объекта, поэтому deinit может напечатать title;
  • deinit бывает у классов, а у структур, которые вы писали, его нет.

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

Как Swift считает ссылки

Swift ведёт счётчик для каждого объекта класса, как библиотекарь у подставки. Такой способ управлять памятью называют ARC — автоматический подсчёт ссылок.

Ссылку, которая держит объект, называют сильной ссылкой. Все ссылки, которые вы писали до сих пор, сильные. Объект держат:

  • переменная или константа;
  • свойство другого объекта;
  • элемент массива.

Счётчик меняется по трём правилам:

  1. Новая сильная ссылка прибавляет единицу.
  2. Исчезнувшая ссылка отнимает единицу.
  3. Когда счётчик становится нулём, Swift вызывает deinit и сразу освобождает память объекта.

Нажимайте «Следующий шаг» и следите за счётчиком в рамке main: deinit срабатывает на той строке, где счётчик становится нулём.

ВживуюДве карточки на одну книгушаг 1 из 8 · рамок: 2

Book(title:) создаёт объект, и init заполняет его свойство title.

Счётчик ссылок записан в рамке main

Строка anna = nil книгу не освободила: у boris осталась своя карточка. Объект живёт, пока на него есть хотя бы одна сильная ссылка, и неважно, какая из них появилась первой.

Когда ссылка исчезает

Переменной присвоили nil

Так было в примерах выше. Чтобы положить в переменную nil, ей нужен опциональный тип, например Book?.

В переменную положили другой объект

Старая ссылка при этом исчезает:

SWIFT
var boris: Book? = Book(title: "Emma")
boris = Book(title: "Ulysses") // Emma is back
print(boris?.title ?? "none") // Ulysses

boris отпускает книгу Emma и берёт Ulysses. Других ссылок на Emma нет, поэтому её deinit срабатывает на строке присваивания.

Функция закончилась

Ссылка из константы внутри функции исчезает не позже, чем закончится вызов:

SWIFT
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:

SWIFT
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 ничего не ищет в памяти: компилятор встраивает счёт в программу.