Wyciek pamięci — jeden z najbardziej podstępnych problemów w programowaniu mobilnym. Pamięć aplikacji nieustannie rośnie, aż osiąga limit ustawiony przez system operacyjny, po czym następuje OutOfMemoryError lub wymuszone zakończenie. Według Square Engineering około 40% aplikacji Android ma co najmniej jeden wyciek pamięci, który można wykryć tylko podczas profilowania. Omówimy przyczyny i metody zapobiegania wzrostowi pamięci.
Najważniejsze
Wyciek pamięci (memory leak) — sytuacja, w której obiekt, który nie jest już potrzebny aplikacji, nadal pozostaje w stercie, ponieważ istnieje aktywna referencja z zestawu korzeniowego (GC Root). Garbage collector uznaje taki obiekt za żywy i nie usuwa go.
Rozdymanie pamięci (memory bloat) — szerszy problem, gdy aplikacja zużywa więcej pamięci niż potrzeba do wykonania bieżących zadań. Przyczyny: nadmierne buforowanie, duplikowanie obiektów, nieoptymalne struktury danych i fragmentacja sterty.
W Androidzie każda aplikacja ma ograniczoną stertę (zwykle 64-512 MB w zależności od urządzenia i wersji systemu). W iOS ograniczenie jest mniej restrykcyjne, ale system wysyła ostrzeżenie o pamięci przy zbliżaniu się do limitu.
| Cecha | Android | iOS |
|---|---|---|
| Limit sterty | 64-512 MB (zależy od urządzenia) | Niejawny (systemowy) |
| Garbage collector | ART (Concurrent, Compact) | ARC (Automatic Reference Counting) |
| Mechanizm wycieku | Referencje GC Root | Cykle retain (silne cykle referencji) |
| Skutek | OutOfMemoryError | Ostrzeżenie o pamięci → zakończenie |
Według Facebook Engineering Blog, wycieki pamięci są przyczyną ~15% raportów crash w aplikacjach mobilnych. W Androidzie dochodzą do tego ANR spowodowane częstymi pauzami GC przy braku pamięci.
Statyczna referencja do Activity — klasyka wycieków w Androidzie. Jeśli statyczne pole lub singleton przechowuje referencję do Activity, nie zostanie ona zebrana przez GC nawet po finish(), dopóki singleton żyje. Activity to ciężki obiekt zawierający hierarchię widoków, zasoby i Context.
object LeakHolder {
var activityRef: Activity ?= null // przeciek: statyczna referencja do Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity nigdy nie zostanie zebrane przez GC
}
}
Klasy anonimowe i lambdy — niejawnie przechowują referencję do klasy zewnętrznej. Jeśli Runnable lub Callback jest przekazany do zewnętrznego serwisu, a Activity jest niszczone, obiekt klasy anonimowej nadal wisi w kolejce i nie pozwala Activity zostać zebranym przez GC.
W iOS głównym problemem są retain cycles: dwa obiekty przechowują silne referencje wzajemnie, a ARC nie może wyzerować licznika referencji dla żadnego z nich. Typowy przypadek: closure przechwytujący self silnie, a self przechowujący referencję do closure.
LeakCanary — biblioteka od Square do automatycznego wykrywania wycieków w Androidzie. Po zniszczeniu Activity lub Fragment sprawdza, czy obiekt został zebrany przez GC. Jeśli nie — wykonuje heap dump i pokazuje trace wycieku.
// LeakCanary 2.x — auto-integracja przez Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary instaluje się automatycznie w debug build
// przez ContentProvider — konfiguracja zerowym kodem
}
}
// Wymuszenie sprawdzenia
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — wbudowane narzędzie do monitorowania pamięci w czasie rzeczywistym. Pozwala zapisać heap dump, znaleźć podejrzane obiekty (Retained Size > 1 MB) i prześledzić ścieżkę GC root do każdego obiektu.
Dla iOS używaj Xcode Memory Graph Debugger. Wizualizuje on graf obiektów w pamięci, pokazuje retain cycles i pozwala błyskawicznie wykryć cykliczne referencje. Dostępny jest również Instruments > Allocations do długoterminowego monitorowania.
WeakReference — podstawowy mechanizm dla referencji, które nie powinny blokować garbage collectora. Jeśli GC zdecyduje się usunąć obiekt, WeakReference zwróci null. Używane do callbacków, słuchaczy i referencji do komponentów UI z wątków tła.
Komponenty Lifecycle-aware — podejście architektoniczne zaimplementowane w Android Jetpack (Lifecycle, LiveData, Flow, coroutines). Subskrypcje są automatycznie anulowane przy onDestroy, co eliminuje główną klasę wycieków.
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// korutyna auto-anuluje się przy onCleared()
}
}
}
viewModelScope i lifecycleScope — wbudowane CoroutineScope w Androidzie, które są anulowane przy odpowiednim zdarzeniu cyklu życia. Eliminuje to wycieki przez korutyny — najczęstszy scenariusz we współczesnym programowaniu Androida.
Memory Profiler in Android Studio — główne narzędzie do monitorowania sterty. Pokazuje live allocations, zrzuty sterty, liczbę obiektów według typów. Pozwala zapisać dump i przeanalizować go w MAT (Memory Analyzer Tool) w celu znalezienia podejrzanych obiektów.
Eclipse MAT — desktopowy analizator zrzutów sterty. Po załadowaniu pliku HPROF z Android Studio, MAT buduje drzewo dominatora, pokazuje retain size każdego obiektu i oferuje automatyczną analizę podejrzanych wycieków przez Leak Suspects Report.
Xcode Memory Graph — wizualny debugger retain cycles. Po kliknięciu przycisku Memory Graph Debugger Xcode zatrzymuje aplikację, buduje pełny graf obiektów w pamięci i podświetla retain cycles na czerwono.
| Narzędzie | Platforma | Cecha |
|---|---|---|
| LeakCanary | Android | Automatyczne wykrywanie wycieków po destroy |
| Memory Profiler | Android Studio | Heap dump + live allocations |
| Eclipse MAT | Android | Drzewo dominatora, Leak Suspects Report |
| Memory Graph | iOS (Xcode) | Wizualizator retain cycles |
Według Google I/O 2023, aplikacje używające LeakCanary w kompilacjach debug redukują liczbę crashy związanych z pamięcią o 30-50% w ciągu pierwszych 2 miesięcy po wdrożeniu. Zaleca się dodawanie LeakCanary na etapie onboardingu projektu.
Często zadawane pytania
Wyciek — obiekty niedostępne dla kodu, ale nieusuwane przez GC z powodu aktywnych referencji. Rozdymanie — aplikacja przechowuje w pamięci obiekty, które logicznie są potrzebne, ale w nadmiarowej ilości (np. cache 50 MB przy działającej aplikacji ważącej 80 MB). Rozdymanie leczy się architektonicznie, wyciek — poprzez poprawne zarządzanie referencjami.
LeakCanary używa ObjectWatcher — po onDestroy() Activity tworzy WeakReference na Activity i uruchamia GC. Jeśli po 5 sekundach WeakReference nie jest wyczyszczony, LeakCanary wykonuje heap dump, analizuje najkrótszy łańcuch referencji od GC Root do obiektu i pokazuje dokładny stos wycieku z nazwą pliku i linią kodu.
Bitmap zajmuje pamięć poza stertą Java w pamięci natywnej (native heap). Rozmiar jednego Bitmap = szerokość × wysokość × 4 bajty (ARGB_8888). Zdjęcie 12 MP (4000×3000) zajmuje 48 MB. Android nie zawsze może terminowo zwolnić pamięć natywną, co przy nagromadzeniu kilku Bitmap prowadzi do OOM nawet przy wystarczającej stercie Java.
Retain cycle — sytuacja w ARC, gdy dwa obiekty przechowują silne referencje wzajemnie, a licznik referencji nigdy nie osiąga zera. Typowy przykład: ViewController z silną referencją do closure, a closure przechwytuje self silnie. Rozwiązanie: użyj [weak self] lub [unowned self] w zamknięciach.
Rozmiar sterty zależy od urządzenia i wersji Androida. Dla starych urządzeń (API 15-24) — 64-128 MB. Dla nowoczesnych (API 25+) — 256-512 MB. Dokładną wartość można uzyskać przez ActivityManager.getMemoryClass(). Dla dużych aplikacji (gry, edytory) istnieje largeHeap=true w manifeście, dające do 1 GB.
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ż