Управление памятью в Swift: как работает ARC и сборщик мусора

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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. Если объект, который должен был удалиться, всё ещё висит — у вас утечка.

Практические советы по оптимизации памяти

  1. Используйте value types (struct) где возможно. Структуры не создают ссылок — они копируются. Это безопаснее и часто быстрее для небольших данных.
  2. Избегайте большого числа сильных ссылок. Чем сложнее граф объектов, тем выше риск цикла.
  3. Не храните замыкания без необходимости. Если

Блоги

Рекомендаций по книгам