Какие основные проблемы управления памятью в iOS (Swift)?

«Какие основные проблемы управления памятью в iOS (Swift)?» — вопрос из категории Управление памятью, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Основные проблемы связаны с автоматическим подсчетом ссылок (ARC) и циклами сильных ссылок:

  1. Retain Cycles (Циклические ссылки) — возникают, когда два или более объекта удерживают друг друга сильными (strong) ссылками, предотвращая их освобождение ARC.

    • Типичные сценарии:
      • Делегаты без weak.
      • Замыкания (closures), захватывающие self.
    • Решение: Использовать weak или unowned ссылки.
    • Пример:
      class ViewController {
          var dataLoader: DataLoader?
      }
      class DataLoader {
          // weak предотвращает цикл: ViewController -> DataLoader -> weak -> ViewController
          weak var delegate: ViewController?
      }
  2. Сильное захвата self в замыканиях. Замыкания по умолчанию захватывают переменные сильными ссылками.

    • Решение: Использовать список захвата [weak self] или [unowned self].
    • Пример:
      networkService.fetchData { [weak self] result in
          // self стал Optional из-за weak
          guard let self = self else { return }
          self.updateUI(with: result)
      }
  3. Непреднамеренное удержание объектов (Unintentional Retention). Объекты могут жить дольше необходимого из-за сильных ссылок в коллекциях (например, кэш в виде обычного Dictionary), глобальных переменных или синглтонах.

    • Решение: Использовать NSCache (автоматически очищает память под давлением), тщательнее управлять жизненным циклом.
  4. Утечки в UIViewController. Контроллер не освобождается после закрытия, часто из-за:

    • Замыканий (см. п.2).
    • Наблюдателей NotificationCenter, не отписанных в deinit.
    • Таймеров (Timer), не инвалидированных.
    • Решение: Всегда отписываться и останавливать активность в deinit.
      deinit {
          NotificationCenter.default.removeObserver(self)
          myTimer?.invalidate()
      }