Почему Swift не использует традиционный сборщик мусора?
Если вы пришли в мир Swift из Python или Java, вас наверняка удивило, что в этом языке нет классического сборщика мусора (Garbage Collector). Вместо этого Apple внедрила механизм Automatic Reference Counting (ARC) — автоматический подсчёт ссылок. И это не просто прихоть: на мобильных устройствах каждый такт процессора и каждый байт памяти на счету. Традиционный GC «замораживает» приложение во время сборки, что для iOS с её требованием к плавности анимаций (60 FPS) было бы катастрофой.
В этой статье мы разберём, как на самом деле работает управление памятью в Swift, где таятся грабли с циклическими ссылками, и как писать код, который не «утекает» в никуда.
ARC: как это работает под капотом?
ARC — это не сборщик мусора в привычном понимании. Он работает на этапе компиляции: компилятор вставляет инструкции retain и release в нужные места. Но есть нюанс: ARC не умеет обрабатывать циклические ссылки (retain cycles). Если два объекта держат друг друга за сильные ссылки, их счётчик ссылок никогда не упадёт до нуля — память утечёт.
Пример: простой класс и подсчёт ссылок
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) появился в памяти")
}
deinit {
print("\(name) исчез из памяти")
}
}
var person1: Person? = Person(name: "Алиса") // retain count = 1
var person2 = person1 // retain count = 2
person1 = nil // retain count = 1
person2 = nil // retain count = 0 → deinit
В консоли вы увидите: «Алиса появился в памяти» и через некоторое время — «Алиса исчез из памяти». Всё честно.
Циклические ссылки: главный враг памяти
Представьте: у вас есть User и Account. Пользователь имеет счёт, а счёт знает своего владельца. Если оба свойства — сильные ссылки, получается замкнутый круг:
class User {
let name: String
var account: Account?
init(name: String) { self.name = name }
deinit { print("User \(name) удалён") }
}
class Account {
let number: String
var owner: User?
init(number: String) { self.number = number }
deinit { print("Account \(number) удалён") }
}
var user: User? = User(name: "Борис")
var account: Account? = Account(number: "12345")
user?.account = account
account?.owner = user
user = nil // User не удалён! account?.owner всё ещё держит ссылку
account = nil // Account не удалён! user?.account держит ссылку
// Утечка памяти!
В консоли не появится ни одного сообщения deinit. Объекты зависли в памяти навсегда. Это retain cycle — классическая проблема управления памятью в Swift.
Как лечить? Weak и unowned
Решение — сделать одну из ссылок слабой (weak) или бесхозной (unowned). Разница:
- weak — всегда optional, автоматически становится
nil, когда объект удалён. Безопасно. - unowned — не optional, предполагает, что объект живёт дольше. Если обратиться к удалённому объекту — краш.
class Account {
let number: String
weak var owner: User? // ← слабая ссылка
init(number: String) { self.number = number }
deinit { print("Account \(number) удалён") }
}
// Теперь user = nil освободит User, а account?.owner станет nil
Сборщик мусора vs ARC: что быстрее?
Давайте честно: ARC — не «сборщик мусора» в классическом смысле. Настоящий GC (как в Java, Go или Python) работает в рантайме: он периодически сканирует кучу, находит недостижимые объекты и освобождает их. Это требует пауз (stop-the-world).
ARC же — это статический анализ + вставка retain/release. Плюсы:
- Предсказуемое освобождение памяти — объект удаляется сразу, как только на него перестают ссылаться.
- Нет «заморозок» — приложение не тормозит.
- Меньше накладных расходов на мобильных устройствах.
Минусы:
- Циклические ссылки — ваша ответственность.
- Ручное управление weak/unowned — можно ошибиться.
- Немного больше накладных расходов на каждый retain/release (но они оптимизированы).
Замыкания и утечки: скрытая угроза
Замыкания (closures) в Swift — это ссылочные типы. Если замыкание захватывает self, а self хранит это замыкание, получается циклическая ссылка. Классический пример с обработчиком нажатия:
class ViewController {
var onTap: (() -> Void)?
func setup() {
onTap = {
print("Нажата кнопка: \(self.title)") // сильный захват self
}
}
deinit { print("VC удалён") }
}
// VC не удалится, пока onTap жив
Решение — использовать [weak self] в списке захвата:
onTap = { [weak self] in
guard let self = self else { return }
print("Нажата кнопка: \(self.title)")
}
Или [unowned self], если вы уверены, что self переживёт замыкание (например, при анимациях).
Инструменты для поиска утечек памяти
Вы не обязаны искать утечки вслепую. Xcode предоставляет отличные инструменты:
- Debug Memory Graph — визуально показывает все объекты в памяти и их связи. Красные восклицательные знаки указывают на утечки.
- Instruments (Leaks) — автоматически находит циклические ссылки в рантайме.
- Malloc Debug — отслеживает выделения и освобождения памяти.
Совет: запустите приложение, выполните какой-то сценарий (например, открытие/закрытие экрана) и проверьте Memory Graph. Если объект, который должен был удалиться, всё ещё висит — у вас утечка.
Практические советы по оптимизации памяти
- Используйте value types (struct) где возможно. Структуры не создают ссылок — они копируются. Это безопаснее и часто быстрее для небольших данных.
- Избегайте большого числа сильных ссылок. Чем сложнее граф объектов, тем выше риск цикла.
- Не храните замыкания без необходимости. Если