Как работает ARC
ARC — это не сборщик мусора, а расстановка retain/release компилятором: объект умирает ровно в ту секунду, когда на него больше никто не смотрит.
ARC (Automatic Reference Counting) — механизм управления памятью, при котором компилятор на этапе сборки вставляет в код вызовы увеличения и уменьшения счётчика ссылок. Когда счётчик доходит до нуля, объект немедленно разрушается.
«Что такое ARC и чем он отличается от garbage collector?»
Что на самом деле проверяют
Интервьюер хочет понять, видите ли вы разницу между compile-time и runtime подходом. Кандидат, который отвечает «ARC — это автоматическая сборка мусора в Swift», расписывается в том, что не понимает, почему в iOS бывают retain cycle и почему нет пауз на сборку. Дальше из этого ответа обычно вырастает разговор про утечки, замыкания и производительность.
Развёрнутый ответ
Garbage collector — это подсистема рантайма. Она периодически просыпается, обходит граф объектов от корней и освобождает недостижимое. Отсюда две особенности: циклы собираются сами собой (они недостижимы от корней), но момент освобождения непредсказуем, а обход стоит процессорного времени и даёт паузы.
ARC работает иначе. Никакого обхода графа нет. Компилятор анализирует код и сам расставляет retain и release там, где ссылка появляется и исчезает. В рантайме остаётся только атомарный инкремент и декремент счётчика.
final class Avatar {
let id: Int
init(id: Int) {
self.id = id
print("init Avatar \(id)")
}
deinit {
print("deinit Avatar \(id)")
}
}
var a: Avatar? = Avatar(id: 1) // счётчик = 1
var b = a // retain -> счётчик = 2
a = nil // release -> счётчик = 1, объект жив
print("между присваиваниями")
b = nil // release -> счётчик = 0, вызывается deinit
print("после")
Вывод:
init Avatar 1
между присваиваниями
deinit Avatar 1
после
Обратите внимание на порядок строк: deinit случился строго между двумя print, а не «когда-нибудь потом». Это и есть детерминированное освобождение — главный практический плюс ARC. Файл закроется, сокет отключится, наблюдатель отпишется ровно в тот момент, когда умерла последняя ссылка. Никакого обхода графа за этим не стоит: компилятор просто вставил в бинарник вызовы swift_retain и swift_release в нужных точках.
Плата за предсказуемость — циклы ARC не собирает. Если два объекта держат друг друга сильными ссылками, счётчик каждого никогда не станет нулём, и никто не придёт это заметить. Именно поэтому в iOS существуют weak, unowned и capture list, а в Java или C# их аналоги не нужны.
Для каких типов ARC вообще работает
Счётчик есть только у ссылочных типов: class, замыкания (это объекты на куче), actor. У struct и enum счётчика нет — они копируются по значению. Но косвенно они в ARC участвуют: если внутри структуры лежит поле-класс, копирование вызывает retain этого поля.
struct Profile {
let name: String
let avatar: Avatar // ссылочное поле внутри значимого типа
}
let p1 = Profile(name: "Ann", avatar: Avatar(id: 2))
let p2 = p1 // структура скопирована, но avatar общий: retain
«Когда именно вызывается deinit?»
Что на самом деле проверяют
Проверяют, понимаете ли вы, что deinit — это не «финализатор, который когда-нибудь позовут», а гарантированная точка. И умеете ли вы ей пользоваться для уборки ресурсов, не наделав при этом типичных бед.
Развёрнутый ответ
deinit вызывается синхронно, на том потоке, где произошёл последний release, сразу после того как счётчик стал нулём и до освобождения памяти. Затем ARC разрушает все хранимые свойства объекта, а после — вызывает deinit суперкласса. Порядок всегда снизу вверх: наследник, потом родитель. Своего deinit нельзя вызвать руками и нельзя объявить в структуре.
final class Session {
private var observer: NSObjectProtocol?
private var socket: Socket?
deinit {
// Хорошо: синхронная детерминированная уборка
socket?.close()
if let observer {
NotificationCenter.default.removeObserver(observer)
}
// Плохо: self уже нельзя «спасти» — сохранять его наружу нельзя
// Плохо: async-работа с self внутри deinit — объект уже разрушается
// Плохо: deinit не может бросать ошибки и не может быть async
}
}
Ещё тонкость: если последняя ссылка исчезла в фоновой очереди, deinit выполнится в фоне. Трогать UIKit оттуда без перехода на главный поток — риск.
Немного истории и краёв
До 2011 года в Objective-C был MRC: retain, release и autorelease писали руками, а autoreleasepool собирал отложенные освобождения на границе цикла событий. ARC — та же модель, но расстановку взял на себя компилятор. Отсюда наследие вроде @objc и toll-free bridging между NSString и CFString.
Отдельный край — Core Foundation: функции с Create или Copy в имени отдают объект во владение вам, и в неаннотированном C-коде его придётся освобождать через CFRelease. Unmanaged тоже выводит из-под ARC — баланс retain/release снова ваша забота.
let raw = Unmanaged.passRetained(session).toOpaque() // +1 вручную
// ... передали указатель в C-API ...
Unmanaged<Session>.fromOpaque(raw).release() // -1 вручную, иначе утечка
Типичные ошибки кандидатов
- Называть ARC «сборщиком мусора Swift» — и не мочь объяснить, откуда тогда берутся retain cycle.
- Считать, что счётчик ссылок считается в рантайме «по графу»: на самом деле граф никто не обходит.
- Думать, что у
structтоже есть счётчик ссылок. - Верить, что
deinitвызовется «когда-нибудь позже», и не использовать его для уборки ресурсов. - Пытаться сделать
deinitасинхронным или продлить жизньselfвнутри него. - Забывать, что
deinitвыполняется на потоке последнегоrelease, и дёргать оттуда UIKit.
Как ответить кратко
ARC — это автоматический подсчёт ссылок. Компилятор на этапе компиляции сам расставляет retain и release, в рантайме остаётся только менять счётчик. Как только счётчик обнуляется, немедленно вызывается deinit — освобождение детерминированное, пауз на сборку нет. В отличие от garbage collector, который обходит граф объектов в рантайме, ARC ничего не обходит, поэтому не умеет разрывать циклы: за них отвечаем мы через weak и unowned. Счётчик есть только у ссылочных типов — классов, замыканий, акторов; структуры копируются, но их поля-классы retain'ятся.