Что происходит с объектом при достижении нуля счетчика сильных ссылок и как учитываются слабые ссылки в iOS
Что проверяют
Знаете ли вы, что происходит внутри ARC, а не только правило «у делегата ставим weak». Интервьюер хочет услышать порядок событий: deinit, освобождение свойств, обнуление слабых ссылок, и почему память может жить дольше объекта. Следом спрашивают про weak против unowned и поиск retain cycle.
Как отвечать
ARC работает на этапе компиляции: компилятор вставляет вызовы retain и release, сборщика мусора нет. У объекта Swift фактически три счётчика: сильных, unowned и слабых ссылок.
Когда уходит последняя сильная ссылка:
- вызывается
deinit: синхронно, на том потоке, где случился последний release. Сначала deinit подкласса, затем суперкласса; - освобождаются хранимые свойства: каждое отпускает своё значение, и освобождение идёт каскадом по объектам, которых больше никто не держит;
- слабые ссылки начинают возвращать
nil: объект в процессе деинициализации черезweakуже не получить; - память освобождается, когда обнулились и сильный, и unowned счётчики. Пока жива хоть одна
unownedссылка, блок памяти остаётся, чтобы обращение к ней надёжно падало, а не читало мусор.
Слабые ссылки в Swift устроены через side table: при первой weak ссылке на объект выделяется отдельная структура со счётчиками, и weak указывает на неё, а не на сам объект. Поэтому объект освобождается целиком, а side table доживает, пока на неё смотрят weak-переменные. В Objective-C слабые ссылки живут в глобальной таблице, её чистит dealloc.
Практический вывод: weak для делегатов и [weak self] в замыканиях, которые хранит сам объект, unowned только когда ссылка гарантированно не переживёт владельца. Циклы ищут в Memory Graph Debugger и Instruments (Leaks).
Где спотыкаются
Говорят, что при нуле сильных ссылок память сразу возвращается системе. С учётом unowned и side table это не так.
Считают unowned просто «weak без опционала». Обращение к unowned после deinit это гарантированный краш, а unowned(unsafe) это неопределённое поведение.
Думают, что deinit всегда выполняется на главном потоке. Если последнюю ссылку отпустили в фоне, там же пройдёт и deinit, и трогать в нём UIKit нельзя.