Как по графу владения отличить retain cycle от объекта, который всё ещё удерживается ожидаемой сильной ссыл...

Как по графу владения отличить retain cycle от объекта, который всё ещё удерживается ожидаемой сильной ссылкой?

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

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

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

Главный критерий — не сам факт наличия цикла, а достижимость объекта от ожидаемого корня через сильные ссылки. Объект, доступный только внутри замкнутой группы, вероятно утёк; объект с валидным внешним владельцем ещё не должен уничтожаться.

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

До ARC разработчик вручную управлял парами retain/release. Это позволяло явно контролировать владение, но ошибки в балансировке приводили к преждевременному освобождению или утечкам.

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

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

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

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

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

В Memory Graph Debugger сначала выбирают подозрительный экземпляр и прослеживают входящие связи. Сильные ссылки образуют ownership-граф, а weak и unowned-ссылки не увеличивают число владельцев и не поддерживают объект живым.

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

Практический алгоритм проверки:

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

Для подтверждения используют Memory Graph как снимок связей, Allocations для наблюдения за ростом числа экземпляров и Leaks для поиска недостижимой памяти. Снимок графа показывает состояние в конкретный момент, поэтому его нужно сопоставлять с жизненным циклом операции, а не трактовать изолированно.

Исправление выбирают по смыслу владения, а не по желанию «убрать цикл». Если связь не должна продлевать жизнь объекта, её делают weak. Если зависимый объект гарантированно переживает ссылку, возможен unowned, но нарушение этой гарантии приведёт к аварийному завершению при обращении.

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

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

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

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

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

Дополнительный вопрос 1: достаточно ли найти цикл сильных ссылок, чтобы объявить его утечкой?

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

Дополнительный вопрос 2: почему weak-ссылка в графе не доказывает, что объект уже должен быть уничтожен?

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

Дополнительный вопрос 3: почему рост памяти после операции не всегда означает retain cycle?

Память может оставаться занятой аллокатором, кэшем, пулом авторелизаций или внутренними структурами фреймворка даже после уничтожения объектов. Нужно наблюдать не только RSS, но и число живых экземпляров, пути владения и повторяемость роста при многократном выполнении операции. Цикл подтверждается недостижимым сильным графом и отсутствием ожидаемого освобождения, а не одним показателем потребления памяти.