Цикл сильных ссылок
Вернёмся в библиотеку. Анна взяла книгу у Бориса, а Борис — у Анны, и они договорились: каждый вернёт чужую книгу, только когда получит назад свою. Анна ждёт Бориса, Борис ждёт Анну, и ни одна книга так и не вернётся на полку.
С объектами бывает то же самое. Объект освобождается, когда на него не осталось сильных ссылок. Но что будет, если два объекта держат сильные ссылки друг на друга? Возьмём человека и квартиру: человек знает, где живёт, а квартира знает своего жильца.
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Давайте разберём программу:
- У
Personесть свойствоhome, ссылка на квартиру, а уApartment— свойствоtenant, ссылка на жильца. Оба свойства опциональные: человек может жить без квартиры, а квартира — стоять без жильца. Опциональное свойствоvarбез значения сначала равноnil: это и есть его значение по умолчанию, поэтомуPerson()иApartment()создаются пустымinit(). - Строки с
annaиflatсоздают по объекту, и у каждого объекта пока одна сильная ссылка. anna?.home = flatдаёт квартире вторую ссылку, аflat?.tenant = anna— вторую ссылку человеку.anna = nilиflat = nilубирают внешние ссылки. Программа не печатает ничего: ни одинdeinitне сработал.
Посчитаем ссылки после каждой из последних строк:
| Строка | Ссылок на Person | Ссылок на Apartment |
|---|---|---|
flat?.tenant = anna | 2 | 2 |
anna = nil | 1 | 2 |
flat = nil | 1 | 1 |
После последней строки у каждого объекта осталось по одной ссылке — от другого объекта. Ноль не наступит никогда.
Каждый ждёт другого
Объекты, которые держат друг друга сильными ссылками по кругу, образуют цикл сильных ссылок. Круг бывает и длиннее: объект A держит B, B держит C, а C держит A.
Почему цикл не разрывается сам:
- Человек освободится, только когда исчезнет ссылка
tenantу квартиры. - Эта ссылка исчезнет, только когда освободится сама квартира.
- Квартира освободится, только когда исчезнет ссылка
homeу человека. - А ссылка
homeисчезнет, только когда освободится человек.
Каждый объект ждёт другого, как Анна и Борис.
Утечка памяти
Объекты из цикла остаются в памяти до конца программы, хотя до них уже не добраться: в anna и flat теперь nil. Память, которую программа заняла и уже не может ни использовать, ни вернуть, называют утечкой памяти.
В маленькой программе этого не видно. А приложение работает часами. Если каждое открытие экрана оставляет в памяти пару таких объектов, память постепенно кончится, и система закроет приложение.
Здесь картинка с читателями неточна. Люди в конце концов заметят, что ждут друг друга, и договорятся. Swift циклы не ищет и не разрывает: объекты будут ждать до конца программы, а компилятор не выдаст об этом ни ошибки, ни предупреждения.
На собеседовании часто спрашивают, почему при ARC возможна утечка памяти. Ответ можно строить так:
- ARC освобождает объект, только когда счётчик ссылок на него становится нулём;
- в цикле сильных ссылок счётчики не опускаются ниже единицы, даже когда снаружи до объектов не добраться;
- сборщик мусора ищет объекты, до которых не добраться из переменных, и такой цикл освободил бы, а ARC ничего не ищет.
Разорвать цикл вручную
Цикл можно разорвать самому: убрать одну из ссылок между объектами, пока до них ещё можно добраться.
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Разберём, что изменилось:
room?.tenant = nilубирает ссылку квартиры на человека. УPersonостаётся одна ссылка —boris.boris = nilосвобождает человека. Вместе с ним исчезает его свойствоhome, и у квартиры остаётся одна ссылка —room.room = nilосвобождает квартиру.
Способ рабочий, но ненадёжный: о строке с разрывом нужно помнить каждый раз, а если её забыть, утечка вернётся молча.
Частая ошибка
Цикл разрывают слишком поздно — после того как отпустили внешнюю ссылку:
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 циклы не ищет и не разрывает, а сборщик мусора такой цикл освободил бы.
- Вручную цикл разрывают, убрав ссылку между объектами, пока до них ещё можно добраться.