Память и ARC
Как ARC освобождает объекты, как возникают циклы удержания и как их разрывают через `weak`/`unowned`, и как искать утечку инструментами анализа памяти.
12 вопросов
JuniorТеорияОчень частоЧто такое ARC — когда вставляются вызовы retain/release и чем он отличается от трассирующего сборщика мусора?
Что такое ARC — когда вставляются вызовы retain/release и чем он отличается от трассирующего сборщика мусора?
ARC вставляет retain/release при компиляции, считая владельцев объекта, и освобождает его, как только счётчик достигает нуля. В отличие от трассирующего сборщика мусора фонового сканирования нет — освобождение детерминировано.
Типичные ошибки
- ✗Думают, что ARC сканирует кучу во время выполнения, как сборщик мусора
- ✗Считают, что retain/release решаются в рантайме, а не вставляются компилятором
- ✗Полагают, что момент освобождения при ARC недетерминирован
Уточняющие вопросы
- →Что происходит со счётчиком ссылок при добавлении
weak-ссылки? - →Почему ARC всё равно может допустить утечку, несмотря на подсчёт ссылок?
JuniorТеорияОчень частоweak против unowned — что делает каждый при освобождении объекта и когда unowned оправдан?
weak против unowned — что делает каждый при освобождении объекта и когда unowned оправдан?
weak — optional и обнуляется в nil, когда объект освобождён, поэтому читать безопасно. unowned — non-optional, не обнуляется и падает после смерти объекта; оправдан, только когда объект живёт дольше ссылки.
Типичные ошибки
- ✗Меняют семантику местами — думают, что падает
weak, аunownedбезопасен - ✗Считают, что
unownedавтоматически обнуляется, какweak - ✗Полагают, что любое из ключевых слов влияет на счётчик ссылок
Уточняющие вопросы
- →Почему обращение к
unownedпосле освобождения — это UB, а не чистыйnil? - →Чем
weakотличается отunowned(unsafe)во время выполнения?
JuniorТеорияЧастоКогда вызывается deinit, что в нём ещё валидно и почему нельзя воскрешать self?
Когда вызывается deinit, что в нём ещё валидно и почему нельзя воскрешать self?
deinit вызывается, когда счётчик ссылок объекта достигает нуля, прямо перед освобождением памяти. Внутри self и его свойства ещё валидны, можно освободить ресурсы. Сохранение self не отменит освобождение.
Типичные ошибки
- ✗Думают, что
deinitвызывается с задержкой после того, как объект стал недостижим - ✗Считают, что хранимые свойства внутри
deinitуже равныnil - ✗Полагают, что деинициализацию можно отменить, снова сохранив
self
Уточняющие вопросы
- →В каком порядке вызываются
deinitпо цепочке «подкласс–суперкласс»? - →Почему
deinitне должен запускатьasync-работу, захватывающуюself?
JuniorТеорияЧастоЧто такое цикл удержания? Приведите классический пример «родитель–ребёнок» и укажите, какую ссылку нужно разорвать.
Что такое цикл удержания? Приведите классический пример «родитель–ребёнок» и укажите, какую ссылку нужно разорвать.
Цикл удержания — это два объекта с сильными ссылками друг на друга, поэтому ни один счётчик не доходит до нуля и оба утекают. В паре «родитель–ребёнок» обратная ссылка ребёнка на родителя должна быть weak, чтобы цикл разорвать.
Типичные ошибки
- ✗Думают, что ARC автоматически обнаруживает и разрывает циклы ссылок
- ✗Помечают
weakне ту сторону пары (владельца вместо обратной ссылки) - ✗Считают, что цикл утекает, только если охватывает несколько потоков
Уточняющие вопросы
- →Когда для обратной ссылки ребёнка выбрать
unowned, а неweak? - →Как замыкание, хранимое родителем, создаёт такой же цикл?
MiddleДебаггингЧастоНайдите утечку за ростом памяти при каждом push и pop экрана
Найдите утечку за ростом памяти при каждом push и pop экрана
Граф показывает, что закрытый контроллер всё ещё жив: сильные стрелки к замыканию и обратно образуют цикл, поэтому он не освобождается. Захватите [weak self], и контроллер освободится при pop, прекращая рост.
Типичные ошибки
- ✗Винят кэши в ровном росте вместо проверки графа удержаний
- ✗Ждут, что
viewDidDisappearили ручное обнуление освободит зациклённый контроллер - ✗Помечают
unownedвсе рёбра вместо разрыва одного черезweak
Уточняющие вопросы
- →Как отличить сильную стрелку от
weakв графе? - →Почему утёкший контроллер часто тянет за собой всю иерархию view?
MiddleТеорияЧастоЧто живёт на стеке, а что в куче в Swift — всегда ли struct живёт на стеке?
Что живёт на стеке, а что в куче в Swift — всегда ли struct живёт на стеке?
Значимые типы по умолчанию на стеке или inline, а классы — в куче под управлением ARC. Но нет: struct, захваченный замыканием или упакованный в existential, попадает в кучу. Размещение решают escaping и размер.
Типичные ошибки
- ✗Утверждают, что struct всегда размещается на стеке без исключений
- ✗Думают, что
letпротивvarрешает размещение на стеке или в куче - ✗Полагают, что захваченный struct остаётся на стеке
Уточняющие вопросы
- →Как захват struct в escaping-замыкание меняет его размещение?
- →Почему упаковка struct в existential
anyвызывает аллокацию?
MiddleДебаггингЧастоTimer с target: self держит контроллер живым навсегда — исправьте
Timer с target: self держит контроллер живым навсегда — исправьте
Запланированный Timer удерживает свой target, а run loop — таймер, поэтому target: self держит контроллер живым вечно. Исправьте вызовом invalidate() в viewDidDisappear или блочным API с [weak self].
Типичные ошибки
- ✗Помечают свойство
Timerкакweak— это не разрывает удержание run loop - ✗Считают, что
Timerне удерживает свой target - ✗Ждут, что ARC или нехватка памяти всё равно освободят контроллер
Уточняющие вопросы
- →Почему инвалидированный таймер наконец освобождает свой target?
- →Как блочный API
Timerпозволяет избежать сильногоtarget?
MiddleДебаггингЧастоЗамыкание с [unowned self] падает после закрытия экрана — исправьте
Замыкание с [unowned self] падает после закрытия экрана — исправьте
Замыкание переживает self, поэтому его unowned-ссылка смотрит в мёртвую память и вызов падает — он зря полагал, что self его переживёт. Захватывайте [weak self] с guard let self, тогда мёртвый self — безопасный no-op.
Типичные ошибки
- ✗Принимают падение из-за висячего указателя за цикл удержания
- ✗Винят поток, а не ошибочное предположение
unownedо времени жизни - ✗Переходят на сильный захват, что снова создаёт утечку
Уточняющие вопросы
- →Когда
[unowned self]действительно верный выбор для замыкания? - →Почему
guard let selfделает вызов на мёртвомselfбезопасным no-op?
JuniorТеорияИногдаКакой инструмент докажет утечку — Leaks и Allocations из Instruments или Memory Graph Debugger из Xcode — и что упускает каждый?
Какой инструмент докажет утечку — Leaks и Allocations из Instruments или Memory Graph Debugger из Xcode — и что упускает каждый?
Leaks помечает блоки без оставшихся ссылок, но упускает объекты, удерживаемые циклом. Allocations показывает рост памяти, но не называет виновника. Memory Graph Debugger показывает граф удержаний и указывает на циклы.
Типичные ошибки
- ✗Считают Leaks, Allocations и граф взаимозаменяемыми
- ✗Ждут, что Leaks поймает цикл, чьи объекты вы всё ещё удерживаете
- ✗Думают, что Allocations сам по себе указывает утёкший объект
Уточняющие вопросы
- →Почему цикл удержания виден как ровный рост в Allocations, но не в Leaks?
- →Что означает фиолетовый значок
!в Memory Graph Debugger?
MiddleТеорияИногдаСтоит ли чего-нибудь weak var? Объясните side table и что означает обнуляемая weak-ссылка.
Стоит ли чего-нибудь weak var? Объясните side table и что означает обнуляемая weak-ссылка.
Да, weak не бесплатен: у объекта появляется запись в side table со списком его weak-ссылок ценой небольшой аллокации и косвенности. При освобождении рантайм проходит по таблице и атомарно обнуляет каждую weak-ссылку, поэтому висячих указателей нет.
Типичные ошибки
- ✗Считают, что
weakкомпилируется в обычный указатель без накладных расходов - ✗Думают, что учёт weak живёт в переменной, а не в side table у объекта
- ✗Полагают, что
weak-ссылку надо вручную обнулять вdeinit
Уточняющие вопросы
- →Почему обнуление должно быть атомарным относительно освобождения?
- →Как
unownedизбегает цены side table и чем при этом жертвует?
SeniorТеорияИногдаЧто такое autorelease pool и когда современному Swift всё ещё нужен явный autoreleasepool?
Что такое autorelease pool и когда современному Swift всё ещё нужен явный autoreleasepool?
Autorelease pool откладывает release своих объектов до слива пула; run loop сливает один пул за итерацию. Тесный цикл, порождающий много временных Cocoa-объектов, всё ещё оборачивают в явный autoreleasepool, чтобы ограничить пик памяти.
Типичные ошибки
- ✗Считают, что чистому Swift-коду
autoreleasepoolникогда не поможет - ✗Путают autorelease pool со сборщиком мусора
- ✗Думают, что временные объекты всегда освобождаются в конце инструкции без пула
Уточняющие вопросы
- →Почему цикл
for, читающий много изображений, разрастается без явного пула? - →Когда главный run loop сливает свой autorelease pool?
SeniorПроизводительностьРедкоПакетный цикл обработки изображений вырастает до 2 ГБ, и ОС убивает его — как autoreleasepool и downsampling ограничивают пик?
Пакетный цикл обработки изображений вырастает до 2 ГБ, и ОС убивает его — как autoreleasepool и downsampling ограничивают пик?
Каждый проход декодирует полное изображение в большой autorelease-буфер, освобождаемый при сливе run loop, поэтому пик равен сумме проходов. Оберните тело в autoreleasepool, освобождая буфер на каждом проходе, и применяйте downsampling, декодируя пиксели миниатюры.
Типичные ошибки
- ✗Думают, что
autoreleasepoolлишь переупорядочивает освобождения и не снижает пик - ✗Считают, что больше конкурентности снижает здесь пик памяти
- ✗Пропускают downsampling и декодируют пиксели в полном разрешении
Уточняющие вопросы
- →Как
CGImageSourceCreateThumbnailAtIndexизбегает декодирования всего изображения? - →Где в цикле должен стоять блок
autoreleasepool, чтобы помочь?