Wycieki pamięci i ich usuwanie
Znajduj i zatrzymuj wycieki
Wycieki pamięci i ich usuwanie to bezpłatna lekcja Android Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Android Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Android Academy zawiera 4 lekcji w sumie.
Czym jest wyciek pamięci
Wyciek pamięci występuje, gdy obiektów, które nie są już potrzebne, nie można usunąć podczas zbierania śmieci, ponieważ coś nadal przechowuje do nich referencję. W Androidzie typową ofiarą jest Activity lub Context, który pozostaje w pamięci po zniszczeniu ekranu.
Wycieki z czasem powiększają stertę, powodują częstsze zbieranie śmieci (co wstrzymuje interfejs i wywołuje zacinanie), a ostatecznie mogą doprowadzić do awarii aplikacji z błędem OutOfMemoryError. W tej lekcji pokażemy, jak je znajdować i naprawiać.
Jak GC decyduje, co zachować
Moduł odśmiecania Androida zachowuje każdy obiekt, który jest osiągalny z korzenia GC (aktywnych wątków, pól statycznych itd.). Wszystko, co jest nieosiągalne, zostaje zwolnione.
Wyciek to po prostu niepożądana ścieżka osiągalności: obiekt o długim czasie życia przechowuje referencję do obiektu o krótkim czasie życia. Poniższy przykład w czystym Kotlinie pokazuje, jak osiągalność utrzymuje obiekt przy życiu.
object GlobalCache {
val items = mutableListOf<ByteArray>()
}
fun cacheSomething() {
// This 1MB array stays alive forever because GlobalCache
// (a static singleton, a GC root) keeps referencing it.
GlobalCache.items.add(ByteArray(1_000_000))
}
fun main() {
repeat(3) { cacheSomething() }
println("Held arrays: ${GlobalCache.items.size}") // never freed
}Klasyczny przypadek: wyciek Activity
Najczęstszy wyciek pamięci w Androidzie występuje, gdy statyczny lub długo żyjący obiekt przechowuje Context. Gdy Activity zostaje zniszczone (po obrocie ekranu lub przejściu do innego widoku), system chce zwolnić zajmowaną przez nie pamięć, ale statyczne odwołanie utrzymuje je przy życiu bez końca.
Poniższy kod powoduje wyciek: singleton przechowuje kontekst Activity.
// LEAK: static field holds an Activity Context
object Analytics {
var context: Context? = null // <- keeps Activity alive
fun init(ctx: Context) { context = ctx }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.init(this) // passing the Activity -> leak on rotation
}
}Naprawa: użyj kontekstu aplikacji
Jeśli trzeba przechowywać Context w obiekcie długo żyjącym, należy przechowywać kontekst aplikacji, który istnieje przez cały czas działania procesu i można go bezpiecznie przechowywać.
Kontekst Activity/View należy przechowywać tylko tak długo, jak długo istnieje dany ekran. Ta jedna zasada zapobiega większości wycieków pamięci w Androidzie.
object Analytics {
private var appContext: Context? = null
fun init(ctx: Context) {
// applicationContext is process-scoped and safe to keep
appContext = ctx.applicationContext
}
}
// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the ActivityKlasy wewnętrzne i Handlers
Niestatyczna klasa wewnętrzna (w tym większość listenerów, obiektów Runnable i wywołań zwrotnych Handler) przechowuje niejawne odwołanie do klasy zewnętrznej. Jeśli taka praca trwa dłużej niż istnienie ekranu, powoduje wyciek tego ekranu.
Opóźnione wywołanie Handler.postDelayed jest częstą przyczyną: oczekująca wiadomość utrzymuje Activity przy życiu aż do momentu jej wykonania.
// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)
// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}Korutyny: określ zakres pracy
Korutyna, która działa dłużej niż ekran, z którego została uruchomiona, powoduje wyciek wszystkiego, co przechwytuje. Rozwiązaniem jest ustrukturyzowana współbieżność: uruchamianie pracy w zakresie powiązanym z cyklem życia, aby była automatycznie anulowana.
W ViewModel użyj viewModelScope, a w kontrolerze interfejsu użyj lifecycleScope. Gdy właściciel zostanie zniszczony, zakres anuluje pracę, a odwołania zostaną zwolnione.
class FeedViewModel : ViewModel() {
fun load() {
// Cancelled automatically when the ViewModel is cleared
viewModelScope.launch {
val feed = repository.fetchFeed()
_state.value = feed
}
}
}
// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()DisposableEffect w Compose
W Compose wszystko, co rejestrujesz, musisz wyrejestrować, gdy composable zostanie usunięty. DisposableEffect udostępnia do tego blok onDispose: służy on do obsługi listenerów, obserwatorów, sensorów i odbiorników transmisji.
Brak sprzątania w tym miejscu powoduje wyciek callbacku i wszystkiego, co on przechwytuje.
@Composable
fun LocationDisplay(manager: LocationManager) {
DisposableEffect(manager) {
val listener = LocationListener { /* update */ }
manager.register(listener)
onDispose { manager.unregister(listener) } // prevents the leak
}
}Wykrywanie wycieków za pomocą LeakCanary
LeakCanary to standardowe narzędzie do automatycznego wykrywania wycieków w kompilacjach debugowych. Dodaj zależność, a narzędzie będzie obserwować zniszczone obiekty. Jeśli któryś z nich nie zostanie usunięty przez garbage collector, narzędzie zrzuci stertę i pokaże dokładny łańcuch odwołań, który utrzymuje obiekt przy życiu.
Ten łańcuch wskazuje rozwiązanie: prowadzi bezpośrednio do problematycznego pola lub listenera.
// build.gradle.kts (app module)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
// Analytics.context -> MainActivity (leaked)Zrzuty sterty w Profilerze
W przypadku wycieków, których LeakCanary nie wykrywa (powolny wzrost, wycieki natywne, duże pamięci podręczne), użyj Memory Profiler. Wykonaj zrzut sterty, wymuś GC i sprawdź, które klasy mają zaskakująco wiele instancji.
Rosnąca liczba instancji Activity, Fragment lub ViewModel po opuszczeniu ekranu to wyraźny sygnał problemu. Profiler pokazuje również ścieżkę do katalogów głównych GC, dzięki czemu można prześledzić odwołanie.
Bitmapy i duże alokacje
Nie każdy problem z pamięcią jest wyciekiem odwołania. Duże bitmapy i nieograniczone pamięci podręczne mogą samodzielnie przepełnić stertę. Zdjęcie w pełnej rozdzielczości może zajmować w pamięci dziesiątki megabajtów.
Pozwól bibliotece obrazów (Coil/Glide) zmniejszyć obraz do rozmiaru, w którym jest wyświetlany, a każdą tworzoną pamięć podręczną ogranicz za pomocą jawnie określonego maksimum. Fragment kodu pokazuje pamięć podręczną LRU ograniczoną rozmiarem.
// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.Lista kontrolna zapobiegania wyciekom
Wyrób te nawyki, a większość wycieków nigdy się nie pojawi:
- W długo żyjących obiektach przechowuj applicationContext, nigdy Activity ani View.
- Uruchamiaj pracę asynchroniczną w korutynie o zakresie cyklu życia (
viewModelScope,lifecycleScope). - Zawsze wyrejestrowuj listenery i odbiorniki (
onDispose,onDestroy). - Anuluj opóźnione Handlers i timery.
- Ograniczaj rozmiar pamięci podręcznych i zmniejszaj bitmapy.
- Zachowaj LeakCanary w kompilacjach debugowych i reaguj na wskazywane przez niego ślady.
Szybkie sprawdzenie
Potrzebujesz obiektu singleton, który będzie przechowywać Context przez cały czas swojego istnienia. Który kontekst powinien przechowywać, aby uniknąć wycieku ekranu?
Podsumowanie: utrzymuj porządek w pamięci
Dowiedziałeś się, czym są wycieki pamięci i jak im zapobiegać:
- Wycieki to niepożądane ścieżki osiągalności, które utrzymują martwe obiekty przy życiu.
- Klasyczny wyciek to statyczne lub długo żyjące odwołanie do Activity/Context — zamiast niego przechowuj applicationContext.
- Listenery w klasach wewnętrznych, opóźnione Handlery i korutyny bez zakresu powodują wyciek właściciela — określaj ich zakres i anuluj je.
- W Compose używaj
DisposableEffect, aby wyrejestrowywać callbacki. - LeakCanary i Memory Profiler ujawniają łańcuch odwołań.
- Ograniczaj rozmiar pamięci podręcznych i zmniejszaj bitmapy.
Dalej: przyspieszanie uruchamiania aplikacji za pomocą optymalizacji startu i profili bazowych.
Często zadawane pytania
Czy lekcja „Wycieki pamięci i ich usuwanie” jest bezpłatna?
Tak — pełny tekst „Wycieki pamięci i ich usuwanie” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Android Academy, przejdź na CoddyKit PRO. Kurs Android Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Wycieki pamięci i ich usuwanie”?
Znajduj i zatrzymuj wycieki Ćwiczysz Android Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Android Academy?
Nie wymagamy żadnego doświadczenia. Android Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Wycieki pamięci i ich usuwanie”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Android Academy?
Tak. Każda lekcja Android Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Pomiar wydajności
- Poskramianie rekompozycji
- Wycieki pamięci i ich usuwanie
- Profile uruchamiania i Baseline Profiles