Zarządzanie pamięcią aplikacji

Na tej stronie wyjaśniamy, jak aktywnie zmniejszać wykorzystanie pamięci przez aplikację. Więcej informacji o tym, jak system operacyjny Android zarządza pamięcią, znajdziesz w artykule Omówienie zarządzania pamięcią.

Pamięć RAM jest cennym zasobem w każdym środowisku programistycznym. Jest ona jeszcze bardziej przydatna w mobilnych systemach operacyjnych, w których pamięć fizyczna jest często ograniczona. Mimo że zarówno środowisko wykonawcze Androida (ART), jak i maszyna wirtualna Dalvik wykonują rutynowe czyszczenie pamięci, nie oznacza to, że możesz zignorować to, kiedy i gdzie aplikacja przydziela i zwalnia pamięć. Nadal musisz unikać wycieków pamięci, które zwykle są powodowane przez wstrzymanie odwołań do obiektów w statycznych zmiennych składowych, i udostępniać obiekty Reference w odpowiednim momencie, zgodnie z wywołaniami zwrotnymi cyklu życia.

Monitorowanie dostępnej pamięci i wykorzystania pamięci

Aby móc rozwiązać problemy z wykorzystaniem pamięci przez aplikację, musisz najpierw je znaleźć. Program profilujący pamięci w Android Studio ułatwia znajdowanie i diagnozowanie problemów z pamięcią na te sposoby:

  • Zobacz, jak aplikacja przydziela pamięć w czasie. Narzędzie do profilowania pamięci wyświetla w czasie rzeczywistym wykres wykorzystania pamięci przez aplikację, liczbę przydzielonych obiektów Java oraz czas odśmiecania.
  • Inicjuj zdarzenia czyszczenia pamięci i zrób zrzut stosu Javy podczas działania aplikacji.
  • Rejestruj przydziały pamięci w aplikacji, sprawdzaj wszystkie przydzielone obiekty, wyświetl zrzut stosu dla każdego alokacji i przejdź do odpowiedniego kodu w edytorze Android Studio.

Zwolnij pamięć w odpowiedzi na zdarzenia

Android może odzyskać pamięć z aplikacji lub w razie potrzeby całkowicie ją zatrzymać, aby zwolnić pamięć na potrzeby krytycznych zadań. Więcej informacji znajdziesz w artykule Omówienie zarządzania pamięcią. Aby jeszcze bardziej zrównoważyć pamięć systemową i uniknąć zatrzymywania procesu aplikacji przez system, możesz wdrożyć w klasach Activity interfejs ComponentCallbacks2. Podana metoda wywołania zwrotnego onTrimMemory() umożliwia aplikacji nasłuchiwanie zdarzeń związanych z pamięcią, gdy działa ona na pierwszym planie lub w tle. Dzięki temu aplikacja może zwalniać obiekty w odpowiedzi na cykl życia aplikacji lub zdarzenia systemowe wskazujące, że system musi odzyskać pamięć.

Możesz zaimplementować wywołanie zwrotne onTrimMemory(), aby odpowiadać na różne zdarzenia związane z pamięcią, jak pokazano w tym przykładzie:

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

Sprawdzanie ilości pamięci potrzebnej

Aby umożliwić kilka uruchomionych procesów, Android ustala stały limit rozmiaru stosu dla każdej aplikacji. Dokładny limit rozmiaru stosu różni się w zależności od urządzenia i ogólnej ilości pamięci RAM. Jeśli aplikacja osiągnie limit sterty i spróbuje przydzielić więcej pamięci, system zwróci błąd OutOfMemoryError.

Aby uniknąć wyczerpania pamięci, możesz wysłać zapytanie do systemu, aby określić, ile miejsca na stercie jest dostępne na bieżącym urządzeniu. Możesz wysłać zapytanie do systemu o tę liczbę, wywołując funkcję getMemoryInfo(). Zwraca obiekt ActivityManager.MemoryInfo z informacjami o bieżącym stanie pamięci urządzenia, w tym o dostępnej pamięci, łącznej pamięci i progu pamięci, czyli poziomie pamięci, przy którym system zaczyna zatrzymywać procesy. Obiekt ActivityManager.MemoryInfo ujawnia też lowMemory, czyli prostą wartość logiczną, która informuje, czy na urządzeniu brakuje pamięci.

Ten przykładowy fragment kodu pokazuje, jak używać metody getMemoryInfo() w aplikacji.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

Używaj konstrukcji kodu o mniejszej ilości pamięci

Niektóre funkcje Androida, klasy Java i konstrukcje kodu zużywają więcej pamięci niż inne. Możesz zminimalizować ilość pamięci wykorzystywanej przez aplikację, wybierając w kodzie wydajniejsze alternatywy.

Oszczędnie korzystaj z usług

Zdecydowanie odradzamy pozostawienie usług uruchomionych, gdy są niepotrzebne. Pozostawienie uruchomionych niepotrzebnych usług to jeden z najgorszych błędów w zarządzaniu pamięcią, jakie mogą wystąpić w przypadku aplikacji na Androida. Jeśli Twoja aplikacja wymaga usługi, aby mogła działać w tle, nie pozostawiaj jej uruchomionej, chyba że będzie konieczne uruchomienie zadania. Zatrzymaj usługę po zakończeniu jej zadania. W przeciwnym razie może dojść do wycieku pamięci.

Gdy uruchamiasz usługę, system preferuje jej dalsze działanie. To sprawia, że procesy usługowe są bardzo kosztowne, ponieważ pamięć RAM używana przez usługę jest niedostępna dla innych procesów. Zmniejsza to liczbę procesów w pamięci podręcznej, które system może przechowywać w pamięci podręcznej LRU, co zmniejsza wydajność przełączania się między aplikacjami. Może to nawet doprowadzić do gwałtownego uderzenia w systemie, gdy pamięć jest nieduża, a system nie jest w stanie utrzymać wystarczającej liczby procesów do obsługi wszystkich uruchomionych aktualnie usług.

Zasadniczo unikaj korzystania z usług trwałych ze względu na ciągłe zapotrzebowanie na dostępną pamięć. Zalecamy korzystanie z alternatywnej implementacji, np. WorkManager. Więcej informacji o korzystaniu z WorkManager do planowania procesów w tle znajdziesz w artykule o trwałej pracy.

Użyj zoptymalizowanych kontenerów danych

Niektóre zajęcia w danym języku programowania nie są zoptymalizowane pod kątem urządzeń mobilnych. Na przykład ogólna implementacja HashMap może zajmować dużo pamięci, ponieważ do każdego mapowania wymaga osobnego obiektu wpisu.

Platforma Androida zawiera kilka zoptymalizowanych kontenerów danych, m.in. SparseArray, SparseBooleanArray i LongSparseArray. Na przykład klasy SparseArray są bardziej wydajne, ponieważ unikają konieczności automatycznego dodawania klucza przez system, a czasem wartości, co powoduje utworzenie kolejnego obiektu lub dwóch na wpis.

W razie potrzeby możesz zawsze przełączyć się na tablice nieprzetworzone, aby uzyskać uproszczoną strukturę danych.

Zachowaj ostrożność w przypadku abstrakcji kodu

Deweloperzy często używają abstrakcji jako dobrych praktyk w zakresie programowania, ponieważ mogą one zwiększyć elastyczność kodu i jego obsługę. Abstrakcje są jednak znacznie droższe, ponieważ zazwyczaj wymagają więcej kodu do wykonania, co wymaga więcej czasu i pamięci RAM, aby zmapować kod w pamięci. Jeśli abstrakcje nie przynoszą korzyści, unikaj ich.

Używaj protokołów Lite w przypadku danych zserializowanych

Bufory protokołów (protobufy) to neutralny pod względem języka, neutralny dla platformy i rozszerzany mechanizm zaprojektowany przez Google do serializacji uporządkowanych danych. Jest podobny do XML, ale jest mniejszy, szybszy i prostszy. Jeśli do obsługi danych używasz protokołów, zawsze używaj ich w kodzie po stronie klienta. Zwykłe protobufy generują bardzo szczegółowy kod, co może powodować wiele problemów w aplikacji, takich jak większe wykorzystanie pamięci RAM, znaczne zwiększenie rozmiaru pliku APK i wolniejsze wykonywanie.

Więcej informacji znajdziesz w opisie protokołu protobufreadme.

Unikanie utraty pamięci

Zdarzenia wyrzucania śmieci nie wpływają na wydajność aplikacji. Jednak występujące w krótkim czasie zdarzenia czyszczenia pamięci mogą szybko wyczerpać baterię, a także nieznacznie wydłużyć czas konfigurowania ramek z powodu niezbędnych interakcji między modułem do oczyszczania a wątkami aplikacji. Im więcej czasu system poświęca na usuwanie odpadów, tym szybciej się rozładowuje.

Często rezygnacje pamięci mogą powodować dużą liczbę zdarzeń czyszczenia pamięci. W praktyce rezygnacja z pamięci opisuje liczbę przydzielonych obiektów tymczasowych, które wystąpiły w określonym czasie.

Możesz na przykład przydzielić wiele obiektów tymczasowych w pętli for. Możesz też utworzyć nowe obiekty Paint lub Bitmap w ramach funkcji onDraw() widoku. W obu przypadkach aplikacja tworzy wiele obiektów szybko przy dużych ilościach. Mogą one szybko wykorzystywać całą dostępną pamięć w młodym pokoleniu, wymuszając wystąpienie zdarzenia odśmiecania pamięci.

Za pomocą narzędzia do profilowania pamięci znajdź w kodzie miejsca, w których występuje duża ilość pamięci, zanim da się je naprawić.

Po zidentyfikowaniu problematycznych obszarów w kodzie spróbuj zmniejszyć liczbę przydziałów w obszarach o znaczeniu krytycznym pod kątem wydajności. Rozważ przeniesienie elementów z wewnętrznych pętli do struktury fabrycznej.

Możesz też ocenić, czy pule obiektów odnoszą korzyści w danym przypadku użycia. Gdy pula obiektów nie jest już potrzebna, zamiast upuszczać instancję obiektu na piętro, można ją wpuścić do puli. Następnym razem, gdy będzie potrzebna instancja obiektu tego typu, możesz pozyskać ją z puli zamiast przydzielać.

Dokładnie oceń wydajność, aby określić, czy pula obiektów jest odpowiednia w danej sytuacji. W niektórych przypadkach pule obiektów mogą pogorszyć wydajność. Mimo że pule unikają przydziałów, wiążą się z tym inne narzuty. Na przykład utrzymanie puli obejmuje zwykle synchronizację, która ma minimalny nakład pracy. Ponadto usunięcie puli obiektu z instancją obiektu w celu uniknięcia wycieków pamięci podczas udostępniania, a następnie jej inicjowania podczas pozyskiwania może mieć niezerowy zasięg.

Ograniczenie liczby instancji obiektów w puli, niż jest to konieczne, również utrudnia proces odśmiecania. Pule obiektów zmniejszają liczbę wywołań czyszczenia pamięci, ale w efekcie zwiększa się ilość pracy potrzebnej do każdego wywołania, ponieważ jest proporcjonalna do liczby aktywnych (osiągalnych) bajtów.

Usuń zasoby i biblioteki wymagające dużej ilości pamięci

Niektóre zasoby i biblioteki w Twoim kodzie mogą zajmować pamięć, nawet o tym nie wiesz. Całkowity rozmiar aplikacji, w tym biblioteki zewnętrzne i umieszczone zasoby, może wpływać na ilość pamięci zużywanej przez aplikację. Zużycie pamięci przez aplikację możesz zwiększyć, usuwając z kodu zbędne, zbędne lub nadmiarowe komponenty, zasoby i biblioteki.

Zmniejsz ogólny rozmiar pliku APK

Możesz znacznie zmniejszyć użycie pamięci przez aplikację, zmniejszając jej ogólny rozmiar. Rozmiar mapy bitowej, zasoby, ramki animacji i biblioteki zewnętrzne mogą wpływać na rozmiar aplikacji. Android Studio i pakiet Android SDK udostępniają wiele narzędzi, które pomagają zredukować rozmiar zasobów i zależności zewnętrzne. Narzędzia te obsługują nowoczesne metody skracania kodu, takie jak kompilacja R8.

Więcej informacji o zmniejszaniu ogólnego rozmiaru aplikacji znajdziesz w artykule Zmniejszanie rozmiaru aplikacji.

Użyj Hilt lub Dagger 2 do wstrzykiwania zależności

Platformy wstrzykiwania zależności mogą uprościć pisany kod i zapewnić adaptacyjne środowisko przydatne przy testowaniu i innych zmianach w konfiguracji.

Jeśli zamierzasz używać w aplikacji platformy wstrzykiwania zależności, rozważ użycie Hilt lub Dagger. Hilt to biblioteka wstrzykiwania zależności dla Androida, która działa oprócz Daggera. Dagger nie używa odbicia do skanowania kodu aplikacji. Możesz używać statycznej implementacji czasu kompilacji Daggera w aplikacjach na Androida bez konieczności ponoszenia zbędnych kosztów środowiska wykonawczego i wykorzystania pamięci.

Inne platformy wstrzykiwania zależności, które wykorzystują procesy inicjowania odczuć, skanują Twój kod pod kątem adnotacji. Ten proces może wymagać znacznie więcej cykli procesora i pamięci RAM oraz powodować zauważalne opóźnienie podczas uruchamiania aplikacji.

Zachowaj ostrożność podczas korzystania z bibliotek zewnętrznych

Kod z biblioteki zewnętrznej często nie jest napisany z myślą o środowiskach mobilnych i może być niewydajny podczas pracy z klientem mobilnym. Jeśli korzystasz z biblioteki zewnętrznej, może być konieczne zoptymalizowanie jej pod kątem urządzeń mobilnych. Zaplanuj tę pracę z wyprzedzeniem i przed wykorzystaniem biblioteki przeanalizuj ją pod kątem rozmiaru kodu i ilości pamięci RAM.

Nawet niektóre biblioteki zoptymalizowane pod kątem urządzeń mobilnych mogą powodować problemy ze względu na różną implementację. Na przykład jedna biblioteka może korzystać z protobufów Lite, a inna z mikroprotobufów, co skutkuje 2 różnymi implementacjami protokołów w aplikacji. Może się to zdarzyć w przypadku różnych implementacji logowania, analityki, platform wczytywania obrazów, buforowania i wielu innych rzeczy.

ProGuard może pomóc w usuwaniu interfejsów API i zasobów za pomocą odpowiednich flag, nie może jednak usunąć dużych zależności wewnętrznych biblioteki. Funkcje, których chcesz używać w tych bibliotekach, mogą wymagać zależności niższego poziomu. Staje się to szczególnie problematyczne, gdy używasz podklasy Activity z biblioteki, która może mieć szeroki zakres zależności, gdy biblioteki używają odbicia, co jest powszechne i wymaga ręcznego dostosowania ProGuard, aby działała.

Unikaj korzystania z biblioteki udostępnionej w przypadku tylko jednej lub dwóch funkcji z dziesiątek. Nie pobieraj dużej ilości kodu i narzutów, których nie używasz. Zastanawiając się nad skorzystaniem z biblioteki, poszukaj implementacji, która ściśle pasuje do Twoich potrzeb. W przeciwnym razie możesz utworzyć własną implementację.