Когда память не освобождается сама
Вы уже знаете, что ARC считает ссылки на объект класса и освобождает его, как только счётчик доходит до нуля. Но что, если два объекта держат друг друга за руку одновременно: первый хранит сильную ссылку на второй, а второй — сильную ссылку на первый? Каждый из них не даёт счётчику второго опуститься до нуля. Оба объекта становятся недостижимы для остального кода, но ARC всё равно считает их нужными и не вызывает deinit ни у одного. Это называется циклом сильных ссылок (strong reference cycle) — классическая утечка памяти в языке со счётчиком ссылок.
Представьте два человека, которые держат друг друга за руки и не отпускают: со стороны кажется, что оба застряли и никуда не денутся, хотя всем остальным они уже не нужны. Именно так объекты «зависают» в памяти — программа продолжает работать, но занятая ими память никогда не возвращается системе.
Как выглядит цикл на практике
Частый пример — владелец и то, чем он владеет, где обе стороны знают друг о друге:
final class Person {
let name: String
var apartment: Apartment?
init(name: String) { self.name = name }
deinit { print("\(name) освобождён") }
}
final class Apartment {
let address: String
var tenant: Person?
init(address: String) { self.address = address }
deinit { print("Квартира \(address) освобождена") }
}
Если создать оба объекта и связать их сильными ссылками (person.apartment = apartment; apartment.tenant = person), а затем убрать переменные person и apartment из области видимости — ни одна строка из deinit не напечатается. Оба объекта живы для ARC, потому что держат друг друга, хотя достать их из кода больше нельзя.
weak: слабая ссылка, которая умеет обнуляться
Ключевое слово weak помечает ссылку, которая НЕ увеличивает счётчик ссылок. Объект может быть освобождён, даже если на него ещё смотрит слабая ссылка — просто эта ссылка автоматически станет nil. Именно поэтому weak-свойство всегда объявляется как Optional, причём обязательно через var (константу нельзя обнулить):
final class Apartment {
let address: String
weak var tenant: Person?
init(address: String) { self.address = address }
deinit { print("Квартира \(address) освобождена") }
}
Теперь квартира знает о жильце, но не удерживает его. Если person выходит из области видимости, счётчик ссылок на него падает до нуля (квартира не в счёт), объект освобождается, а apartment.tenant сам становится nil. Цикл разорван.
unowned: слабая ссылка без Optional
unowned — тоже не увеличивает счётчик ссылок, но, в отличие от weak, не оборачивает тип в Optional и не обнуляется. Используйте его, когда уверены, что второй объект живёт не дольше, а обычно ровно столько же, сколько первый, и обращение к нему после освобождения — программная ошибка, а не нормальная ситуация. Классический пример — клиент и его кредитная карта: карта не может существовать без владельца, и вы вправе ожидать, что владелец жив всё время жизни карты.
final class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
deinit { print("\(name) освобождён") }
}
final class CreditCard {
let number: String
unowned let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Карта \(number) освобождена") }
}
Если попытаться обратиться к card.customer после того, как владелец уже освобождён, программа аварийно завершится — это плата за отсутствие проверки на nil. Если такой уверенности в порядке жизни объектов нет, безопаснее взять weak.
Цикл в замыканиях: capture list
Замыкания захватывают переменные из окружения, и если внутри замыкания использовать self, а само замыкание хранится как свойство объекта (например, обработчик, сохранённый на будущее), получается тот же цикл: объект держит замыкание, а замыкание сильно держит объект через self.
final class Downloader {
var onFinish: (() -> Void)?
var name: String
init(name: String) { self.name = name }
deinit { print("\(name) освобождён") }
func setup() {
onFinish = { [weak self] in
guard let self else { return }
print("Готово: \(self.name)")
}
}
}
Список захвата [weak self] в квадратных скобках перед списком параметров замыкания говорит: «захвати self слабо». Внутри замыкания self становится опциональным, поэтому его разворачивают через guard let self else { return } — если объект уже освобождён, замыкание просто ничего не делает вместо обращения к мусору. Без [weak self] замыкание держало бы Downloader живым до тех пор, пока живо само замыкание, а замыкание, в свою очередь, хранится в самом объекте — классический цикл.
Как заметить утечку
Циклы не приводят к падению программы, поэтому их легко пропустить. Верный признак — строка из deinit, которую вы ожидали увидеть в консоли, но не увидели после того, как объект должен был выйти из области видимости. Полезная привычка при отладке — добавить в deinit временный print или счётчик и убедиться, что он срабатывает столько раз, сколько создано объектов.
Частые ошибки
- Ставить
weakна обе стороны связи «на всякий случай» — обычно достаточно разорвать цикл в одном месте, там, где связь логически «снизу вверх» (ребёнок знает про родителя слабо, родитель про ребёнка — сильно). - Использовать
unownedтам, где второй объект может быть освобождён раньше — вместо аккуратной обработкиnilполучаете крах приложения. - Забывать список захвата в замыкании, которое сохраняется как свойство того же объекта, чей
selfоно использует. - Объявлять
weak-свойство черезlet— компилятор не разрешит, потому что слабая ссылка обязана уметь обнуляться. - Путать сам факт «цикл существует» с «программа упадёт»: цикл не роняет программу, он просто держит память занятой, поэтому его сложно заметить без специальной проверки.
Резюме
- Цикл сильных ссылок возникает, когда два объекта держат друг друга сильными ссылками — ARC никогда не опускает их счётчики до нуля.
weak— не увеличивает счётчик, тип всегдаOptional, автоматически становитсяnilпри освобождении объекта.unowned— не увеличивает счётчик, тип неOptional, обращение после освобождения объекта приводит к краху.- В замыканиях, которые сохраняются на будущее и используют
self, применяйте список захвата[weak self]и разворачивайте его черезguard let self else { return }. - Утечку из-за цикла можно заметить по тому, что
deinitне вызывается там, где вы этого ожидали.
Проверьте себя
4 вопроса
Разрыв цикла сильных ссылок
Внутри функции leakCheckCounts() опишите два локальных класса — Owner и Pet. У Owner есть сильное свойство var pet: Pet?, у Pet — ссылка на владельца var owner: Owner?, которую нужно объявить со словом weak, чтобы разорвать цикл. У обоих классов есть статическая переменная-счётчик deinitCount (тип Int), которая увеличивается на 1 в deinit соответствующего класса.
В начале функции обнулите оба счётчика (на случай повторного вызова). Затем в отдельном вложенном блоке do { ... } создайте по одному экземпляру Owner и Pet, свяжите их друг с другом (owner.pet = pet и pet.owner = owner) и дайте блоку завершиться — переменные owner и pet должны выйти из области видимости. После блока do верните массив из двух чисел: [Owner.deinitCount, Pet.deinitCount]. Если ссылка на владельца в Pet объявлена правильно, оба значения после выхода из блока должны быть равны 1 — объекты освободились, несмотря на взаимные ссылки.