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

Цикл сильных ссылок

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

С объектами бывает то же самое. Объект освобождается, когда на него не осталось сильных ссылок. Но что будет, если два объекта держат сильные ссылки друг на друга? Возьмём человека и квартиру: человек знает, где живёт, а квартира знает своего жильца.

SWIFT
class Person {
    var home: Apartment?
    deinit { print("Person freed") }
}
class Apartment {
    var tenant: Person?
    deinit { print("Apartment freed") }
}

var anna: Person? = Person()
var flat: Apartment? = Apartment()
anna?.home = flat
flat?.tenant = anna
anna = nil
flat = nil

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

  1. У Person есть свойство home, ссылка на квартиру, а у Apartment — свойство tenant, ссылка на жильца. Оба свойства опциональные: человек может жить без квартиры, а квартира — стоять без жильца. Опциональное свойство var без значения сначала равно nil: это и есть его значение по умолчанию, поэтому Person() и Apartment() создаются пустым init().
  2. Строки с anna и flat создают по объекту, и у каждого объекта пока одна сильная ссылка.
  3. anna?.home = flat даёт квартире вторую ссылку, а flat?.tenant = anna — вторую ссылку человеку.
  4. anna = nil и flat = nil убирают внешние ссылки. Программа не печатает ничего: ни один deinit не сработал.

Посчитаем ссылки после каждой из последних строк:

СтрокаСсылок на PersonСсылок на Apartment
flat?.tenant = anna22
anna = nil12
flat = nil11

После последней строки у каждого объекта осталось по одной ссылке — от другого объекта. Ноль не наступит никогда.

Каждый ждёт другого

Объекты, которые держат друг друга сильными ссылками по кругу, образуют цикл сильных ссылок. Круг бывает и длиннее: объект A держит B, B держит C, а C держит A.

Почему цикл не разрывается сам:

  1. Человек освободится, только когда исчезнет ссылка tenant у квартиры.
  2. Эта ссылка исчезнет, только когда освободится сама квартира.
  3. Квартира освободится, только когда исчезнет ссылка home у человека.
  4. А ссылка home исчезнет, только когда освободится человек.

Каждый объект ждёт другого, как Анна и Борис.

Утечка памяти

Объекты из цикла остаются в памяти до конца программы, хотя до них уже не добраться: в anna и flat теперь nil. Память, которую программа заняла и уже не может ни использовать, ни вернуть, называют утечкой памяти.

В маленькой программе этого не видно. А приложение работает часами. Если каждое открытие экрана оставляет в памяти пару таких объектов, память постепенно кончится, и система закроет приложение.

Здесь картинка с читателями неточна. Люди в конце концов заметят, что ждут друг друга, и договорятся. Swift циклы не ищет и не разрывает: объекты будут ждать до конца программы, а компилятор не выдаст об этом ни ошибки, ни предупреждения.

На собеседовании часто спрашивают, почему при ARC возможна утечка памяти. Ответ можно строить так:

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

Разорвать цикл вручную

Цикл можно разорвать самому: убрать одну из ссылок между объектами, пока до них ещё можно добраться.

SWIFT
var boris: Person? = Person()
var room: Apartment? = Apartment()
boris?.home = room
room?.tenant = boris
room?.tenant = nil
boris = nil // Person freed
room = nil // Apartment freed

Разберём, что изменилось:

  1. room?.tenant = nil убирает ссылку квартиры на человека. У Person остаётся одна ссылка — boris.
  2. boris = nil освобождает человека. Вместе с ним исчезает его свойство home, и у квартиры остаётся одна ссылка — room.
  3. room = nil освобождает квартиру.

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

Частая ошибка

Цикл разрывают слишком поздно — после того как отпустили внешнюю ссылку:

SWIFT
var carl: Person? = Person()
var studio: Apartment? = Apartment()
carl?.home = studio
studio?.tenant = carl
carl = nil
carl?.home = nil // ✗
studio = nil

Программа ничего не печатает. К строке carl?.home = nil переменная carl уже равна nil, и ?. пропускает присваивание. До объекта Person больше не добраться, и разорвать цикл нечем.

Перенесите carl?.home = nil выше строки carl = nil. Тогда оба deinit сработают на строке studio = nil.

Главное

  • Цикл сильных ссылок — объекты, которые держат друг друга по кругу; их счётчики не доходят до нуля.
  • nil во внешних переменных цикл не разрывает: ни один deinit не сработает, и память утечёт.
  • ARC циклы не ищет и не разрывает, а сборщик мусора такой цикл освободил бы.
  • Вручную цикл разрывают, убрав ссылку между объектами, пока до них ещё можно добраться.