Automatic Reference Counting (ARC) — система керування пам'яттю у Swift та Objective-C, яка автоматично підраховує кількість посилань на кожен об'єкт і звільняє його, коли лічильник досягає нуля. Згідно з Документацією Apple Swift, 2026, ARC вбудовано в компілятор і він працює на етапі компіляції, вставляючи виклики retain/release у потрібні місця. На відміну від Garbage Collection, ARC не потребує окремого потоку збирача та не створює пауз під час виконання застосунку.
Головне
ARC (Automatic Reference Counting) — це компіляторний механізм керування пам'яттю, представлений Apple у Xcode 4.2 (2011) для Objective-C та успадкований Swift. На відміну від ручного керування пам'яттю (Manual Retain-Release, MRR), ARC повністю автоматизує виклики retain, release та autorelease, вставляючи їх на етапі компіляції без участі розробника.
ARC не є збирачем сміття. Це статичний аналіз із динамічним вставленням коду: компілятор аналізує час життя об'єктів і розміщує retain/release у точках, де об'єкти створюються, копіюються або виходять з області видимості. Результат — детерміноване звільнення пам'яті: об'єкт видаляється саме тоді, коли на нього більше немає посилань, без затримок і пауз.
Згідно з WWDC 2011 Session 323, перехід з MRR на ARC скоротив кількість помилок, пов'язаних із пам'яттю, на 70% у застосунках Apple. Розробники перестали вручну балансувати retain/release, що усунуло цілий клас витоків і double-free помилок.
Кожен об'єкт у пам'яті має лічильник посилань (retain count). При створенні об'єкта лічильник встановлюється на 1. Коли нове strong-посилання вказує на об'єкт — лічильник збільшується (retain). Коли strong-посилання зникає — лічильник зменшується (release). При досягненні нуля об'єкт негайно деалокується.
Компілятор Swift вставляє retain/release не на кожне присвоєння — він використовує статичний аналіз для оптимізації. Наприклад, якщо гарантовано, що об'єкт не використовується після передачі, компілятор може пропустити зайвий release/retain. Ця оптимізація називається ARC Optimization.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) initialized (retain count: 1)")
}
deinit {
print("\(name) deallocated")
}
}
func testARC() {
let p = Person(name: "Alice") // retain count = 1
let q = p // retain count = 2
// q виходить з області видимості
// retain count = 1
// p виходить з області видимості
// retain count = 0 → deinit
}
У цьому прикладі видно, як ARC керує лічильником: при присвоєнні q = p лічильник збільшується, при виході q з області видимості — зменшується. Коли останнє strong-посилання зникає, деініціалізатор викликається негайно. Жоден збирач сміття не чекає — пам'ять звільняється одразу.
ARC та Garbage Collection вирішують одне завдання — автоматичне керування пам'яттю — але принципово різними підходами. Вибір між ними визначає архітектуру мови: Swift (ARC) vs Java/Go (GC). Розглянемо основні відмінності.
| Характеристика | ARC (Swift/ObjC) | GC (Java/Go) |
|---|---|---|
| Момент звільнення | Детермінований: одразу при обнулінні лічильника | Недетермінований: при наступному збиранні |
| Паузи виконання | Немає (retain/release на етапі компіляції) | Stop-The-World паузи (2–200 мс) |
| Накладні витрати | Інкремент/декремент лічильника при кожному посиланні | Обхід графа об'єктів, маркування, звільнення |
| Проблеми | Retain Cycle (ручне вирішення) | Фрагментація купи, витоки через забуті посилання |
| Додатковий потік | Не потрібен | Потрібен потік збирача сміття |
Ключовий компроміс: ARC дає передбачуваний час життя об'єктів і нульові паузи, але вимагає від розробника розуміння retain cycle та правильного вибору weak/unowned. GC звільняє від цих турбот, але ціною недетермінованих пауз і додаткового потоку.
ARC визначає три типи кваліфікаторів посилань, кожен з яких по-різному впливає на лічильник і життєвий цикл об'єкта. Правильний вибір кваліфікатора — основа безпечної роботи з пам'яттю у Swift.
Strong — кваліфікатор за замовчуванням. Кожне strong-посилання збільшує retain count об'єкта на 1. Поки існує хоча б одне strong-посилання, об'єкт живий. Усі властивості класів і локальні змінні у Swift за замовчуванням strong. Strong-посилання створюють відношення володіння: об'єкт A володіє об'єктом B.
Weak — посилання, яке не збільшує retain count. Об'єкт може бути деаллокований, навіть якщо на нього вказує weak-посилання. Після деаллокації weak-посилання автоматично встановлюється в nil. Weak-посилання завжди оголошуються як var з опціональним типом (?). Використовуються для розриву retain cycle, особливо в патерні delegate.
Unowned — неволодіюче посилання, яке, як і weak, не збільшує retain count. Однак unowned-посилання не встановлюється в nil після деаллокації — звернення до звільненого об'єкта викликає crash. Unowned застосовується, коли гарантовано, що об'єкт живе щонайменше стільки ж, скільки й об'єкт, що посилається. Типовий сценарій — замикання та батьківсько-дочірні відносини з гарантованим часом життя.
class Customer {
let name: String
var card: CreditCard? // strong
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
class CreditCard {
let number: String
unowned let customer: Customer // unowned — не володіє
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card \(number) deallocated") }
}
var customer: Customer? = Customer(name: "Bob")
customer?.card = CreditCard(number: "1234", customer: customer!)
customer = nil
// Customer та CreditCard обидва звільнені — немає retain cycle
Тут CreditCard використовує unowned-посилання на Customer. Customer володіє карткою (strong), а картка не володіє клієнтом (unowned). Коли Customer обнуляється, обидва об'єкти звільняються — retain cycle не виникає. Якби card.customer був strong, цикл заблокував би звільнення.
Незважаючи на автоматизацію, ARC не є панацеєю. Розробники стикаються з кількома типовими проблемами, які потребують розуміння внутрішнього механізму керування пам'яттю.
Замикання (closures) у Swift захоплюють зовнішні змінні за strong-посиланням. Якщо замикання присвоєно властивості класу і захоплює self — виникає retain cycle: клас тримає замикання, замикання тримає self. Рішення — capture list з weak або unowned.
class NetworkManager {
var completionHandler: ((Data?) -> Void)?
var data: Data?
func fetchData() {
completionHandler = { [weak self] result in
guard let self else { return }
self.data = result
self.processResult()
}
}
func processResult() { }
}
Capture list [weak self] створює weak-посилання на self всередині замикання. Це розриває потенційний retain cycle. Guard let self гарантує, що об'єкт живий перед виконанням коду. weak self — стандартна практика для асинхронних замикань у Swift.
Хоча retain/release — легкі операції, у гарячих циклах часті інкременти/декременти лічильника дають накладні витрати. У Swift 5.9+ компілятор використовує оптимізацію, при якій надлишкові retain/release видаляються, якщо аналізатор доводить безпеку. Однак в Objective-C retain/release все ще можуть бути вузьким місцем у високонавантажених сценаріях із мільйонами викликів на секунду.
Autorelease Pool — механізм відкладеного release, який використовується в Objective-C та деяких сценаріях Swift. Об'єкти поміщаються в пул і отримують release при drain пула. У циклах з великою кількістю тимчасових об'єктів (наприклад, парсинг JSON) створення власного autoreleasepool знижує пікове споживання оперативної пам'яті.
Поширені запитання
При ручному керуванні (MRR) розробник явно викликав retain, release та autorelease. ARC вставляє ці виклики автоматично на етапі компіляції, усуваючи ризик double-free, витоків через забутий release та помилок балансування retain/release.
ARC керує лише об'єктами Objective-C та класами Swift. Для структур і вказівників C/C++ ARC не застосовується — ці об'єкти керуються вручну або через розумні вказівники C++ (shared_ptr, unique_ptr). Об'єкти Core Foundation (CFString, CGColor) також не підпадають під ARC.
weak — коли об'єкт може бути деаллокований раніше за той, що посилається (делегати, асинхронні замикання). unowned — коли гарантовано, що об'єкт живе щонайменше стільки ж, скільки й той, що посилається (батько-дитина, де дитина не може існувати без батька). Якщо не впевнені — обирайте weak.
Екзистенційні типи (protocol as type) у Swift упаковують значення у спеціальний контейнер (existential container). Це збільшує кількість retain/release на межах протоколів. У Swift 5.7+ opaque result types та some-параметри знижують накладні витрати за рахунок усунення контейнера.
Прямого API для читання retain count у Swift немає — це вважається деталлю реалізації. Для діагностики використовуйте Instruments (Allocations, Leaks) або Memory Debugger у Xcode. Ці інструменти показують кількість живих екземплярів класу та ланцюжки утримання.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також