Strong Reference (сильне посилання) — це стандартний механізм керування пам’яттю, при якому об’єкт залишається в пам’яті, поки на нього вказує хоча б одне активне посилання. На відміну від слабких посилань, сильне посилання збільшує лічильник посилань об’єкта та запобігає його автоматичному звільненню. Згідно з Apple Developer Documentation, ARC автоматично керує часом життя об’єктів в Swift та Objective-C. Розуміння роботи сильних посилань критично для запобігання витоків пам’яті та циклічних залежностей в мобільних додатках.
Головне
Strong Reference — це тип посилання на об’єкт, який запобігає його знищенню збірувачем сміття або системою керування пам’яттю. Поки існує хоча б одне сильне посилання на об’єкт, його пам’ять не звільняється. Це базовий механізм, на якому побудовані ARC в Swift та Objective-C і збірання сміття в Java та Kotlin.
Концепція сильного посилання є фундаментальною для всіх мов з автоматичним керуванням пам’яттю. В системах з ARC кожне сильне посилання збільшує лічильник посилань об’єкта. Коли лічильник падає до нуля, об’єкт негайно деалокується. В Java та Kotlin з збіранням сміття сильне посилання гарантує, що об’єкт досяжний і не буде зібраний GC.
За даними WWDC 2021, близько 35% витоків пам’яті в iOS-додатках пов’язані з неправильним використанням сильних посилань та циклами утримання. В Android-розробці витоки через неявні strong reference в замиканнях та зворотних викликах є другою за частотою причиною проблем з пам’яттю після Context Leak.
Для ефективної роботи з пам’яттю необхідно розуміти різницю між strong, weak та unowned посиланнями і правильно обирати тип посилання залежно від володіння та часу життя об’єктів.
До впровадження ARC розробники вручну викликали retain та release для кожного об’єкта, що призводило до багатьох помилок. ARC, представлений Apple у 2011 році з випуском LLVM 3.0, автоматизував цей процес, аналізуючи граф володіння на етапі компіляції. Компілятор сам вставляє виклики retain, release та autorelease в потрібні місця.
За даними Clang Static Analyzer, впровадження ARC знизило кількість помилок, пов’язаних з пам’яттю, в iOS-додатках на 70%. Для розробника це означає, що керування пам’яттю стало безпечнішим, але водночас з’явилася необхідність розуміти, як працюють сильні посилання під капотом — щоб уникати retain cycles.
В Kotlin та Java роль ARC відіграє збірувач сміття, але принцип сильного посилання залишається тим самим: GC Roots — це точки входу, через які об’єкти утримуються сильними посиланнями. Поки об’єкт досяжний по ланцюжку сильних посилань від GC Root, він не буде зібраний.
ARC (Automatic Reference Counting) працює за принципом підрахунку посилань для кожного об’єкта в купі. Коли створюється нове сильне посилання на об’єкт, лічильник збільшується (retain). Коли посилання знищується або перезаписується, лічильник зменшується (release). Коли лічильник досягає нуля, об’єкт негайно видаляється з пам’яті.
Розглянемо приклад на Swift. При створенні екземпляра класу ARC виділяє пам’ять та встановлює retain count на 1. Кожне нове присвоєння іншій змінній збільшує лічильник. Коли змінна виходить з області видимості, лічильник зменшується:
class ProfileViewController {
var nameLabel: String?
var avatarImage: UIImage?
func loadProfile() {
// retain count = 1 для нового екземпляра
let user = User(name: "Ivan")
// retain count = 2 після присвоєння nameLabel
nameLabel = user.name
// вихід з методу — user виходить з області видимості, retain count = 1
}
}
В цьому коді ARC гарантує, що об’єкт User залишається в пам’яті, поки на нього є хоча б одне сильне посилання. Коли функція loadProfile завершується, локальна змінна user знищується, але nameLabel все ще утримує об’єкт. Пам’ять звільниться тільки тоді, коли nameLabel перестане існувати або буде перезаписана.
В Kotlin аналогічна поведінка забезпечується через GC Roots. Поки існує простежуваний ланцюжок сильних посилань від кореня збірувача сміття (наприклад, статичне поле або активний потік), об’єкт залишається в пам’яті. Різниця в тому, що GC не звільняє пам’ять миттєво — це відбувається асинхронно після аналізу досяжності.
В ARC звільнення відбувається синхронно в момент обнулення лічильника. В Swift та Objective-C ви точно знаєте, коли об’єкт буде видалений. В Kotlin та Java момент звільнення непередбачуваний, але це компенсується більш гнучкою схемою виявлення циклічних залежностей на рівні збірувача сміття.
Retain cycle (цикл утримання) — ситуація, при якій два або більше об’єктів мають взаємні сильні посилання один на одного. У результаті їхній retain count ніколи не падає до нуля, і пам’ять не звільняється навіть після того, як об’єкти перестають бути потрібними додатку.
Класичний приклад: батьківський view controller утримує дочірній об’єкт сильним посиланням, а той, у свою чергу, сильним посиланням утримує батька. Це типово для ситуацій з делегатами, замиканнями та вкладеними lambda-виразами. За даними Instruments Leaks, retain cycles становлять до 60% усіх витоків пам’яті в додатках, що використовують ARC.
class ParentViewController: UIViewController {
var child: ChildViewController?
func setupChild() {
child = ChildViewController()
// retain cycle: батько утримує child, child утримує батька через closure
child?.onEvent = {
self.handleEvent()
}
}
func handleEvent() {}
}
Проблема тут в тому, що замикання onEvent захоплює self (ParentViewController) сильним посиланням, а сам ParentViewController утримує child сильним посиланням. Обидва об’єкти ніколи не будуть звільнені. Рішення — використовувати weak self в замиканні, щоб розірвати цикл.
В Kotlin аналогічні цикли виникають при використанні lambda, що захоплюють зовнішні об’єкти. JVM збірувач сміття може з часом виявити такі цикли, але тільки якщо об’єкти недосяжні з GC Roots. Якщо цикл пов’язаний з активним потоком або UI-контекстом, витік залишається на весь час життя додатка.
Розуміння різниці між типами посилань — ключ до безпечного керування пам’яттю. Strong Reference збільшує retain count. Weak Reference не збільшує retain count і автоматично стає nil при звільненні об’єкта. Unowned Reference також не збільшує retain count, але не обнуляється — звернення до нього після звільнення викликає краш.
| Тип посилання | Retain count | Безпека | Коли використовувати |
|---|---|---|---|
| Strong | +1 | Безпечно (типово) | Володіння об’єктом, відносини батько → дитина |
| Weak | Не змінює | Автообнулення (безпечно) | Делегати, callback, зворотні посилання |
| Unowned | Не змінює | Ризик крашу при пізньому зверненні | Коли об’єкт живе гарантовано довше за власника |
Вибір типу посилання диктується відносинами володіння. Якщо об’єкт B є частиною A і не може існувати без нього — використовуйте Strong. Якщо B може існувати незалежно і посилається на A для сповіщень — використовуйте Weak. Unowned використовується рідко — тільки коли час життя дочірнього об’єкта суворо не перевищує час життя батька.
Apple Developer Documentation рекомендує: типово використовуйте strong для всіх відносин володіння. Якщо необхідно уникнути retain cycle — визначте, яке посилання має бути слабким. Зазвичай це зворотне посилання в ієрархії (дитина → батько). В Kotlin аналогічну роль відіграє WeakReference з java.lang.ref, який використовується для кешів та патернів observer.
Виявлення retain cycles — перший крок. Другий — правильне їх усунення. Основний інструмент боротьби з циклами сильних посилань — заміна одного з посилань на weak або unowned. В мовах зі збіранням сміття додатково застосовується WeakReference з ручною перевіркою на null перед кожним доступом.
В Swift та Objective-C найчастішим виправленням є додавання [weak self] в замикання. Це гарантує, що замикання не утримує об’єкт після його звільнення. В Kotlin для аналогічних цілей використовують обгортки WeakReference або явне очищення посилань в onDestroy.
class NetworkService {
func fetchData(completion: @escaping (Data?) -> Void) {
// захоплення через weak self — retain cycle виключено
URLSession.shared.dataTask(
with: URL(string: "https://api.example.com")!
) { [weak self] data, response, error in
guard let self else { return }
completion(data)
}.resume()
}
}
В цьому прикладі [weak self] гарантує, що NetworkService не буде утримуватися замиканням після того, як він більше не потрібний. Якщо self звільнився до завершення запиту — guard let self else { return } виходить з замикання без виклику completion.
Для діагностики retain cycles використовуйте Instruments Leaks для iOS або Android Profiler + LeakCanary для Android. Ці інструменти показують точний граф утримання та вказують, яке сильне посилання перешкоджає звільненню об’єкта. Регулярне профілювання пам’яті має бути частиною CI/CD пайплайну будь-якого мобільного проекту.
Swift та Kotlin використовують принципово різні механізми керування пам’яттю, але концепція сильного посилання присутня в обох. Swift використовує ARC з синхронним звільненням при retain count = 0. Kotlin використовує трасуючий GC, який асинхронно вичищає недосяжні об’єкти.
| Параметр | Swift (ARC) | Kotlin (JVM GC) |
|---|---|---|
| Механізм | Підрахунок посилань (retain count) | Трасування досяжності (GC Roots) |
| Звільнення | Синхронне (при обнуленні лічильника) | Асинхронне (по циклу GC) |
| Retain cycle | Не виявляється автоматично | GC може виявити, але не відразу |
| Слабке посилання | weak (автообнулення) | WeakReference (ручна перевірка) |
Основна практична відмінність: в Swift retain cycle — це гарантований витік. В Kotlin GC може розірвати цикл, якщо об’єкти недосяжні з кореня, але час життя витеклих об’єктів залишається непередбачуваним. Тому в обох мовах найкраща стратегія — уникати циклів сильних посилань на етапі проектування.
Для Swift використовуйте weak в делегатських патернах та замиканнях. Для Kotlin — WeakReference або Lifecycle-aware компоненти, які автоматично очищають посилання при знищенні власника. В обох підходах мета одна — виключити сильні посилання там, де вони створюють нерозривний ланцюг утримання.
Часті запитання
Strong Reference збільшує retain count об’єкта та запобігає його звільненню, поки існує посилання. Weak Reference не змінює retain count та автоматично стає nil, коли об’єкт видаляється з пам’яті. Сильні посилання використовуються для володіння, слабкі — для зворотних зв’язків та делегатів.
Retain cycle — взаємне блокування, при якому два об’єкти утримують один одного сильними посиланнями. Їхній retain count ніколи не падає до нуля, пам’ять не звільняється. Це призводить до витоку пам’яті: об’єкти назавжди залишаються в купі, додаток споживає все більше ресурсів і в результаті падає з OutOfMemory.
Використовуйте Instruments Leaks з Xcode — запустіть профілювання з шаблоном Leaks, виконайте сценарій в додатку та перевірте індикатори витоків. Для точної діагностики перейдіть на вкладку Cycles & Roots — вона покаже граф взаємних strong reference, які утворюють нерозривний цикл.
Unowned застосовуйте, коли час життя дочірнього об’єкта завідомо не перевищує час життя батька — наприклад, при прив’язці об’єкта до суворо визначеного області. Якщо є сумніви — використовуйте Weak, оскільки звернення до звільненого unowned викликає краш додатку.
Опосередковано — так. Кожен retain та release в ARC — це атомна операція з накладними витратами. При великій кількості об’єктів у циклах це може впливати на продуктивність. Однак основна проблема — не швидкість ARC, а витоки пам’яті через неправильно вибраний тип посилання.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також