Управление памятью это обязательная часть любого iOS-собеса, от junior до senior. Junior должен объяснить цикл ссылок, senior рассказать, как устроены weak-ссылки и что происходит при освобождении объекта.
Как работает ARC
ARC (Automatic Reference Counting) считает сильные ссылки на объекты классов. Компилятор вставляет retain при появлении новой ссылки и release при её исчезновении. Когда счётчик сильных ссылок падает до нуля, вызывается deinit и объект уничтожается.
final class Resource {
deinit { print("освобождён") }
}
var a: Resource? = Resource()
var b = a // 2 сильные ссылки
a = nil // 1
b = nil // 0: "освобождён"
Отличия от сборщика мусора (Java, Kotlin):
- освобождение детерминированное, сразу после исчезновения последней ссылки;
- нет пауз и фонового потока сборщика, пиковое потребление памяти ниже;
- циклы ссылок ARC не собирает, разрывать их обязан разработчик;
retain/releaseатомарные и стоят времени, оптимизатор убирает лишние пары.
Структуры и enum не считаются, но если в них лежат ссылки (классы, буферы массивов и строк, замыкания), копия структуры делает retain каждой из них.
Цикл сильных ссылок
final class Owner { var pet: Pet? }
final class Pet { var owner: Owner? } // сильная обратная ссылка
let o = Owner(), p = Pet()
o.pet = p
p.owner = o
// после выхода из области оба объекта останутся в памяти навсегда
Решение: одна из ссылок не должна удерживать объект.
weak и unowned
weak | unowned | |
|---|---|---|
| Удерживает объект | нет | нет |
| Тип | всегда опционал, var | не опционал |
| После освобождения | становится nil | обращение вызывает крэш |
| Когда | объект может исчезнуть раньше | объект гарантированно живёт дольше ссылки |
Классика weak: делегаты, обратные ссылки на родителя, [weak self] в сетевых колбэках. Классика unowned: дочерний объект ссылается на владельца, который его создал и хранит.
Где возникают циклы на практике
- Делегат объявлен сильным. Контроллер держит view, view держит контроллер как делегата.
- Замыкание в свойстве захватывает
self.onTap = { self.reload() }. - Таймер.
Timer.scheduledTimer(target: self, ...)удерживает target, а run loop удерживает таймер. Инвалидация вdeinitне сработает:deinitне наступит. Используйте блочный API с[weak self]или инвалидируйте при уходе с экрана. - NotificationCenter с блоком. Центр держит замыкание, пока не вызван
removeObserver(token). Автоматическая отписка с iOS 9 касается только селекторного API. - Combine.
sink { self.value = $0 }.store(in: &cancellables), гдеcancellablesсвойство того же объекта. - Двусвязный список или дерево с ссылкой на родителя.
Что происходит при освобождении
Когда счётчик сильных ссылок становится нулём:
- Объект переходит в состояние деинициализации: weak-ссылки уже возвращают
nil, обращение через unowned вызывает trap. - Выполняется
deinitподкласса, затем суперкласса. - Освобождаются хранимые свойства (отсюда каскад: освободился владелец, освободились его поля).
- Память объекта возвращается, когда не осталось unowned-ссылок.
Side table. С Swift 4 при первой weak-ссылке объекту создаётся побочная таблица счётчиков, и weak-ссылки указывают на неё, а не на объект. Поэтому объект освобождается сразу, а маленькая таблица живёт, пока есть weak-ссылки. В Objective-C runtime weak-ссылки хранились в глобальной хеш-таблице.
autoreleasepool
Swift-объекты освобождаются сразу. Но Objective-C API могут возвращать autoreleased-объекты, которые живут до осушения пула. В длинном цикле обработки картинок это даёт пики памяти:
for url in urls {
autoreleasepool {
let image = UIImage(contentsOfFile: url.path)
process(image)
}
}
Как искать утечки
- Memory Graph Debugger в Xcode: граф объектов, кто кого держит, отметки на утечках.
- Instruments: Leaks для недостижимой памяти, Allocations с поколениями для роста.
- Лог в
deinitи сценарий «открыть и закрыть экран 5 раз». - Тест: держать слабую ссылку на объект и проверить в teardown, что она стала
nil.
Частые ошибки
- Ставить
unownedв асинхронных колбэках. - Инвалидировать таймер в
deinit. - Думать, что
DispatchQueue.main.async { self... }создаёт цикл. - Забывать отписываться от блочных наблюдателей.
Что спросят на собесе
- Как работает ARC и чем он отличается от сборщика мусора?
- Чем
weakотличается отunowned, когда что использовать? - Где возникают циклы ссылок и как их найти?
- Что выведет код с
deinitи слабой обратной ссылкой? - Как устроены weak-ссылки (side table) и когда освобождается память объекта?