В кэше объект хранится только через weak ссылку. Почему он может исчезнуть сразу после добавления в кэш?

В кэше объект хранится только через weak-ссылку. Почему он может исчезнуть сразу после добавления в кэш?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

weak-ссылка не владеет объектом, поэтому не увеличивает его счётчик сильных ссылок. Если после добавления в кэш не осталось других сильных ссылок, объект уничтожается, а weak-ссылка автоматически становится nil.

Такой кэш хранит объект только пока его жизнью управляет внешний владелец. Это позволяет кэшу не продлевать время жизни данных, но требует корректно обрабатывать промахи кэша.

Исторический контекст

ARC автоматизировал управление сильными ссылками, заменив ручное управление подсчёком ссылок. При этом сохранилась необходимость описывать отношения, которые не должны владеть объектом: например, делегаты, обратные ссылки и кэши.

Для таких отношений используются weak-ссылки. Их задача — не удерживать объект в памяти и автоматически обнуляться после его уничтожения, предотвращая висячие ссылки.

Постановка проблемы

Предположим, кэш сохранил объект только через weak. Разработчик может ошибочно считать сам факт наличия записи в кэше гарантией того, что объект доступен.

На самом деле объект может быть уничтожен сразу после исчезновения последней сильной ссылки. В результате запись в кэше останется, но её значение будет nil; это нормальный промах кэша, а не ошибка ARC.

Подробное решение

Сильная ссылка увеличивает число владений объектом. weak-ссылка владением не является: она лишь регистрируется runtime как ссылка, которую нужно обнулить при уничтожении объекта.

Последовательность событий такова:

  1. Внешний код создаёт объект и владеет им через сильную ссылку.
  2. Кэш сохраняет на объект weak-ссылку, не добавляя владение.
  3. Внешняя сильная ссылка удаляется или переназначается.
  4. Если других сильных ссылок нет, ARC запускает уничтожение объекта.
  5. Runtime обнуляет зарегистрированные weak-ссылки.
final class Item {} final class WeakBox<T: AnyObject> { weak var value: T? init(_ value: T) { self.value = value } } var item: Item? = Item() var cache: [String: WeakBox<Item>] = [:] cache["item"] = WeakBox(item!) item = nil print(cache["item"]?.value == nil) // true

WeakBox остаётся в словаре, но его value становится nil. Поэтому при чтении кэша нужно проверять значение и удалять устаревшую запись либо создавать объект заново.

Преимущество такого подхода — кэш не вызывает утечек памяти и не удерживает редко используемые объекты. Компромисс — содержимое кэша не гарантировано: объект может исчезнуть между двумя операциями, если код не владеет им сильной ссылкой.

Если кэш должен гарантировать наличие объекта, он должен хранить его сильной ссылкой или использовать отдельную стратегию вытеснения. Сильный кэш надёжнее с точки зрения доступности, но способен существенно увеличить потребление памяти.

Ситуация из практики

В приложении изображения хранятся в кэше экранов. При использовании сильных ссылок изображения остаются в памяти после закрытия экранов, потому что кэш продолжает ими владеть. Это приводит к росту памяти и возможному аварийному завершению при нехватке памяти.

Рассматривались два варианта:

  • Сильный кэш — изображение гарантированно доступно после добавления, но кэш сам удерживает все изображения и должен иметь явную политику очистки.
  • Слабый кэш — память освобождается автоматически, когда изображение больше никому не нужно, но повторное обращение может потребовать загрузки изображения заново.

Для вторичного кэша выбран слабый вариант. Код сначала проверяет weak-значение, а при nil загружает или создаёт объект повторно. Результат — кэш ускоряет повторное использование объектов, но не становится скрытым владельцем всей коллекции.

Что кандидаты часто упускают

  1. Можно ли считать запись в weak-кэше признаком того, что объект существует?

Нет. Запись подтверждает только наличие контейнера или обёртки, но не наличие объекта. В любой момент после освобождения последней сильной ссылки значение weak-ссылки может стать nil, поэтому результат чтения нужно обрабатывать как необязательный.

  1. Почему сильная локальная ссылка всё же нужна во время работы с объектом из weak-кэша?

Чтение weak-ссылки даёт доступ к объекту, но сам кэш не становится его владельцем. Если объект используется в нескольких шагах, следует получить сильную локальную ссылку и работать с ней в рамках операции; это не даёт объекту исчезнуть из-за отсутствия владельца во время этой операции.

Такая локальная ссылка действует только до окончания своего времени жизни. После её освобождения объект снова может быть уничтожен, если других сильных владельцев нет.

  1. Чем weak-кэш отличается от обычного кэша с ограничением размера?

Ограниченный сильный кэш сам владеет объектами и удаляет их по политике, например при превышении размера или по принципу наименее недавно использованных элементов. Weak-кэш не владеет объектами вообще, поэтому они исчезают независимо от размера кэша.

Сильный кэш даёт предсказуемую доступность до момента вытеснения, а weak-кэш экономит память, но допускает неожиданные промахи. Выбор зависит от того, важнее ли гарантированное повторное использование или минимальное удержание объектов в памяти.