Strong Reference (silne odwołanie) — to standardowy mechanizm zarządzania pamięcią, w którym obiekt pozostaje w pamięci, dopóki wskazuje na niego przynajmniej jedno aktywne odwołanie. W przeciwieństwie do słabych odwołań, silne odwołanie zwiększa licznik odwołań obiektu i zapobiega jego automatycznemu zwolnieniu. Według Apple Developer Documentation, ARC automatycznie zarządza czasem życia obiektów w Swift i Objective-C. Zrozumienie działania silnych odwołań jest kluczowe do zapobiegania wyciekom pamięci i cyklicznym zależnościom w aplikacjach mobilnych.
Najważniejsze
Strong Reference — to typ odwołania do obiektu, który zapobiega jego zniszczeniu przez garbage collector lub system zarządzania pamięcią. Dopóki istnieje przynajmniej jedno silne odwołanie do obiektu, pamięć pod niego nie jest zwalniana. Jest to podstawowy mechanizm, na którym zbudowane są ARC w Swift i Objective-C oraz garbage collection w Java i Kotlin.
Koncepcja silnego odwołania jest fundamentalna dla wszystkich języków z automatycznym zarządzaniem pamięcią. W systemach z ARC każde silne odwołanie zwiększa licznik odwołań obiektu. Gdy licznik spada do zera, obiekt jest natychmiast dealokowany. W Java i Kotlin z garbage collectorem silne odwołanie gwarantuje, że obiekt jest osiągalny i nie zostanie zebrany przez GC.
Według danych WWDC 2021, około 35% wycieków pamięci w aplikacjach iOS jest związanych z nieprawidłowym użyciem silnych odwołań i cyklami zatrzymania. W programowaniu na Androida wycieki przez niejawne strong reference w domknięciach i callbackach są drugą najczęstszą przyczyną problemów z pamięcią po Context Leak.
Do efektywnej pracy z pamięcią konieczne jest zrozumienie różnicy między strong, weak i unowned oraz prawidłowy wybór typu odwołania w zależności od własności i czasu życia obiektów.
Przed wprowadzeniem ARC programiści ręcznie wywoływali retain i release dla każdego obiektu, co prowadziło do wielu błędów. ARC, wprowadzony przez Apple w 2011 roku wraz z wydaniem LLVM 3.0, zautomatyzował ten proces, analizując graf własności na etapie kompilacji. Kompilator sam wstawia wywołania retain, release i autorelease w odpowiednie miejsca.
Według Clang Static Analyzer, wprowadzenie ARC zmniejszyło liczbę błędów związanych z pamięcią w aplikacjach iOS o 70%. Dla programisty oznacza to, że zarządzanie pamięcią stało się bezpieczniejsze, ale jednocześnie pojawiła się potrzeba zrozumienia, jak działają silne odwołania pod maską — aby unikać retain cycles.
W Kotlin i Java rolę ARC pełni garbage collector, ale zasada silnego odwołania pozostaje ta sama: GC Roots — to punkty wejścia, przez które obiekty są utrzymywane silnymi odwołaniami. Dopóki obiekt jest osiągalny przez łańcuch silnych odwołań od GC Root, nie zostanie zebrany.
ARC (Automatic Reference Counting) działa na zasadzie zliczania odwołań dla każdego obiektu na stercie. Gdy tworzone jest nowe silne odwołanie do obiektu, licznik zwiększa się (retain). Gdy odwołanie jest niszczone lub nadpisywane, licznik zmniejsza się (release). Gdy licznik osiąga zero, obiekt jest natychmiast usuwany z pamięci.
Rozważmy przykład w Swift. Podczas tworzenia instancji klasy ARC przydziela pamięć i ustawia retain count na 1. Każde nowe przypisanie do innej zmiennej zwiększa licznik. Gdy zmienna wychodzi z zakresu widoczności, licznik zmniejsza się:
class ProfileViewController {
var nameLabel: String?
var avatarImage: UIImage?
func loadProfile() {
// retain count = 1 dla nowej instancji
let user = User(name: "Ivan")
// retain count = 2 po przypisaniu nameLabel
nameLabel = user.name
// wyjście z metody — user wychodzi z zakresu, retain count = 1
}
}
W tym kodzie ARC gwarantuje, że obiekt User pozostaje w pamięci, dopóki istnieje na niego przynajmniej jedno silne odwołanie. Gdy funkcja loadProfile kończy działanie, zmienna lokalna user jest niszczona, ale nameLabel nadal trzyma obiekt. Pamięć zostanie zwolniona dopiero gdy nameLabel przestanie istnieć lub zostanie nadpisana.
W Kotlin podobne zachowanie jest zapewniane przez GC Roots. Dopóki istnieje możliwy do prześledzenia łańcuch strong references od korzenia garbage collectora (np. pole statyczne lub aktywny wątek), obiekt pozostaje w pamięci. Różnica polega na tym, że GC nie zwalnia pamięci natychmiast — odbywa się to asynchronicznie po analizie osiągalności.
W ARC zwolnienie następuje synchronicznie w momencie wyzerowania licznika. W Swift i Objective-C dokładnie wiesz, kiedy obiekt zostanie usunięty. W Kotlin i Java moment zwolnienia jest nieprzewidywalny, ale jest to rekompensowane bardziej elastycznym schematem wykrywania zależności cyklicznych na poziomie garbage collectora.
Retain cycle (cykl zatrzymania) — sytuacja, w której dwa lub więcej obiektów ma wzajemne silne odwołania do siebie. W rezultacie ich retain count nigdy nie spada do zera, a pamięć nie jest zwalniana nawet po tym, jak obiekty przestają być potrzebne aplikacji.
Klasyczny przykład: nadrzędny view controller trzyma podrzędny obiekt silnym odwołaniem, a ten z kolei silnym odwołaniem trzyma nadrzędny. Jest to typowe dla sytuacji z delegatami, domknięciami i zagnieżdżonymi wyrażeniami lambda. Według Instruments Leaks, retain cycles stanowią do 60% wszystkich wycieków pamięci w aplikacjach korzystających z ARC.
class ParentViewController: UIViewController {
var child: ChildViewController?
func setupChild() {
child = ChildViewController()
// retain cycle: parent trzyma child, child trzyma parent przez closure
child?.onEvent = {
self.handleEvent()
}
}
func handleEvent() {}
}
Problem polega na tym, że domknięcie onEvent przechwytuje self (ParentViewController) silnym odwołaniem, a sam ParentViewController trzyma child silnym odwołaniem. Oba obiekty nigdy nie zostaną zwolnione. Rozwiązaniem jest użycie weak self w domknięciu, aby przerwać cykl.
W Kotlin podobne cykle powstają przy użyciu lambd przechwytujących obiekty zewnętrzne. Garbage collector JVM może z czasem wykryć takie cykle, ale tylko jeśli obiekty są nieosiągalne z GC Roots. Jeśli cykl jest związany z aktywnym wątkiem lub kontekstem UI, wyciek pozostaje na cały czas życia aplikacji.
Zrozumienie różnicy między typami odwołań — klucz do bezpiecznego zarządzania pamięcią. Strong Reference zwiększa retain count. Weak Reference nie zwiększa retain count i automatycznie staje się nil przy zwolnieniu obiektu. Unowned Reference również nie zwiększa retain count, ale nie zeruje się — odwołanie do niego po zwolnieniu powoduje crash.
| Typ odwołania | Retain count | Bezpieczeństwo | Kiedy używać |
|---|---|---|---|
| Strong | +1 | Bezpieczne (domyślnie) | Posiadanie obiektu, relacja parent → child |
| Weak | Nie zmienia | Autozerowanie (safe) | Delegaci, callback, odwołania zwrotne |
| Unowned | Nie zmienia | Ryzyko crash przy późnym odwołaniu | Gdy obiekt żyje gwarantowanie dłużej niż właściciel |
Wybór typu odwołania jest podyktowany relacją własności. Jeśli obiekt B jest częścią A i nie może istnieć bez niego — używaj Strong. Jeśli B może istnieć niezależnie i odwołuje się do A dla powiadomień — używaj Weak. Unowned jest stosowany rzadko — tylko gdy czas życia podrzędnego obiektu ściśle nie przekracza czasu życia rodzica.
Apple Developer Documentation zaleca: domyślnie używaj strong dla wszystkich relacji własności. Jeśli konieczne jest uniknięcie retain cycle — określ, które odwołanie powinno być słabe. Zazwyczaj jest to odwołanie zwrotne w hierarchii (child → parent). W Kotlin podobną rolę pełni WeakReference z java.lang.ref, który jest stosowany dla cache i wzorca observer.
Wykrycie retain cycles — pierwszy krok. Drugi — prawidłowe ich usunięcie. Głównym narzędziem walki z cyklami silnych odwołań jest zastąpienie jednego z odwołań na weak lub unowned. W językach z garbage collection dodatkowo stosuje się WeakReference z ręcznym sprawdzaniem na null przed każdym dostępem.
W Swift i Objective-C najczęstszą poprawką jest dodanie [weak self] w domknięciach. Gwarantuje to, że domknięcie nie utrzymuje obiektu po jego zwolnieniu. W Kotlin dla podobnych celów używa się opakowania WeakReference lub jawnego czyszczenia odwołania w onDestroy.
class NetworkService {
func fetchData(completion: @escaping (Data?) -> Void) {
// przechwycenie przez weak self — retain cycle wykluczony
URLSession.shared.dataTask(
with: URL(string: "https://api.example.com")!
) { [weak self] data, response, error in
guard let self else { return }
completion(data)
}.resume()
}
}
W tym przykładzie [weak self] gwarantuje, że NetworkService nie będzie utrzymywany przez domknięcie po tym, jak nie będzie już potrzebny. Jeśli self został zwolniony przed zakończeniem żądania — guard let self else { return } wychodzi z domknięcia bez wywołania completion.
Do diagnostyki retain cycles używaj Instruments Leaks dla iOS lub Android Profiler + LeakCanary dla Androida. Te narzędzia pokazują dokładny graf zatrzymania i wskazują, które silne odwołanie uniemożliwia zwolnienie obiektu. Regularne profilowanie pamięci powinno być częścią pipeline'u CI/CD każdego projektu mobilnego.
Swift i Kotlin używają zasadniczo różnych mechanizmów zarządzania pamięcią, ale koncepcja silnego odwołania występuje w obu. W Swift stosowany jest ARC z synchronicznym zwalnianiem przy retain count = 0. W Kotlin używany jest tracers GC, który asynchronicznie czyści nieosiągalne obiekty.
| Parametr | Swift (ARC) | Kotlin (JVM GC) |
|---|---|---|
| Mechanizm | Zliczanie odwołań (retain count) | Śledzenie osiągalności (GC Roots) |
| Zwolnienie | Synchroniczne (przy wyzerowaniu licznika) | Asynchroniczne (cykl GC) |
| Retain cycle | Nie wykrywany automatycznie | GC może wykryć, ale nie od razu |
| Weak ref | weak (autozerowanie) | WeakReference (ręczne sprawdzanie) |
Główna praktyczna różnica: w Swift retain cycle — to gwarantowany wyciek. W Kotlin GC może przerwać cykl, jeśli obiekty są nieosiągalne z korzenia, ale czas życia wyciekłych obiektów pozostaje nieprzewidywalny. Dlatego w obu językach najlepszą strategią jest unikanie cykli silnych odwołań na etapie projektowania.
Dla Swift używaj weak we wzorcach delegate i domknięciach. Dla Kotlin — WeakReference lub komponenty Lifecycle-aware, które automatycznie czyszczą odwołania przy zniszczeniu właściciela. W obu podejściach cel jest jeden — wykluczenie silnych odwołań tam, gdzie tworzą one nierozerwalny łańcuch zatrzymania.
Często zadawane pytania
Strong Reference zwiększa retain count obiektu i zapobiega jego zwolnieniu, dopóki odwołanie istnieje. Weak Reference nie zmienia retain count i automatycznie zeruje się, gdy obiekt jest usuwany z pamięci. Silne odwołania są używane do posiadania, słabe — do sprzężeń zwrotnych i delegatów.
Retain cycle — wzajemna blokada, w której dwa obiekty trzymają się nawzajem silnymi odwołaniami. Ich retain count nigdy nie spada do zera, pamięć nie jest zwalniana. Prowadzi to do wycieku pamięci: obiekty pozostają na stercie na zawsze, aplikacja zużywa coraz więcej zasobów i ostatecznie pada z OutOfMemory.
Użyj Instruments Leaks z Xcode — uruchom profilowanie z szablonem Leaks, wykonaj scenariusz w aplikacji i sprawdź wskaźniki wycieków. Do dokładnej diagnostyki przełącz się na zakładkę Cycles & Roots — pokaże ona graf wzajemnych strong reference, które tworzą nierozerwalny cykl.
Unowned stosuj, gdy czas życia podrzędnego obiektu z pewnością nie przekracza czasu życia rodzica — na przykład przy powiązaniu obiektu ze ściśle określonym zakresem. W razie wątpliwości używaj Weak, ponieważ odwołanie do zwolnionego unowned powoduje crash aplikacji.
Pośrednio — tak. Każdy retain i release w ARC — to operacja atomowa z narzutem. Przy dużej liczbie obiektów w cyklach może to wpływać na wydajność. Jednak głównym problemem nie jest szybkość działania ARC, ale wycieki pamięci z powodu nieprawidłowo wybranego typu odwołania.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również