Урок по треку Swift и iOS 7 мин

ARC, weak, unowned и циклы ссылок

Подсчёт ссылок, strong, weak и unowned, циклы в делегатах и замыканиях, deinit и утечки.

Управление памятью это обязательная часть любого 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

weakunowned
Удерживает объектнетнет
Типвсегда опционал, varне опционал
После освобождениястановится nilобращение вызывает крэш
Когдаобъект может исчезнуть раньшеобъект гарантированно живёт дольше ссылки

Классика weak: делегаты, обратные ссылки на родителя, [weak self] в сетевых колбэках. Классика unowned: дочерний объект ссылается на владельца, который его создал и хранит.

Где возникают циклы на практике

  1. Делегат объявлен сильным. Контроллер держит view, view держит контроллер как делегата.
  2. Замыкание в свойстве захватывает self. onTap = { self.reload() }.
  3. Таймер. Timer.scheduledTimer(target: self, ...) удерживает target, а run loop удерживает таймер. Инвалидация в deinit не сработает: deinit не наступит. Используйте блочный API с [weak self] или инвалидируйте при уходе с экрана.
  4. NotificationCenter с блоком. Центр держит замыкание, пока не вызван removeObserver(token). Автоматическая отписка с iOS 9 касается только селекторного API.
  5. Combine. sink { self.value = $0 }.store(in: &cancellables), где cancellables свойство того же объекта.
  6. Двусвязный список или дерево с ссылкой на родителя.

Что происходит при освобождении

Когда счётчик сильных ссылок становится нулём:

  1. Объект переходит в состояние деинициализации: weak-ссылки уже возвращают nil, обращение через unowned вызывает trap.
  2. Выполняется deinit подкласса, затем суперкласса.
  3. Освобождаются хранимые свойства (отсюда каскад: освободился владелец, освободились его поля).
  4. Память объекта возвращается, когда не осталось 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) и когда освобождается память объекта?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.