Narzędzia i skuteczność

Praktyczne strategie zmniejszania wykorzystania pamięci bitmapy przez aplikację

Głośniki

Informacje

Informacje o wystąpieniu

Zarządzanie pamięcią bitmapy ma kluczowe znaczenie dla zmniejszenia ryzyka braku pamięci (OOM) w aplikacjach na Androida z bogatą grafiką. Deweloperzy wiedzą, że należy zmniejszać rozmiary obrazów, ale aby zapewnić długoterminową stabilność aplikacji, trzeba opanować 2 inne kluczowe filary: ponowne użycie i recykling. Podczas tej sesji przedstawimy praktyczny przewodnik po optymalizacji wydajności pamięci za pomocą nowoczesnych narzędzi, bibliotek open source, takich jak Glide i Coil, oraz interfejsów API na poziomie systemu.

Podczas tej sesji dowiesz się, jak diagnozować rzeczywiste regresje pamięci za pomocą Android Vitals, wdrażać zaawansowane wzorce buforowania (ponowne użycie) za pomocą Glide i Coil oraz proaktywnie zwalniać pamięć (recykling) za pomocą wywołania zwrotnego onTrimMemory.

Podsumowanie

  • Google Play wprowadza progi wydajności pamięci, które będą egzekwowane od lutego 2027 r. Przekroczenie limitu 90 percentyla ma bezpośredni wpływ na ocenę stanu aplikacji i może zmniejszyć jej widoczność w Sklepie Play.
  • Po zdekodowaniu bitmapy rozszerzają się o wymiary i głębię kolorów, co sprawia, że w większości aplikacji zużywają najwięcej pamięci. Zaledwie kilka obrazów w pełnej rozdzielczości może zajmować setki megabajtów.
  • Zmniejsz próbkowanie obrazów i dekoduj tylko widoczne obszary, aby zmniejszyć zużycie pamięci. Zamiast wczytywać zbyt duże obrazy w pełnej rozdzielczości, poproś o dopasowanie wymiarów obrazu przez CDN lub użyj interfejsów API takich jak BitmapRegionDecoder.
  • Wiele modułów wczytywania obrazów zwiększa domyślne wykorzystanie pamięci podręcznej sterty i duplikuje przechowywane bitmapy. Używaj jednego modułu ładującego lub ustaw wyraźne limity pamięci dla istniejących modułów ładujących.
  • Zwolnij pamięć obrazu, gdy aplikacja przejdzie do działania w tle, obsługując onTrimMemory. Zatrzymaj odpytywanie i animacje w tle w onStop, aby zapobiec zwiększaniu wykorzystania pamięci w tle.

Zasoby

Gemini Notebook

Tematy

  • bitmap-optimization
  • android-memory
  • android-vitals
  • image-caching
  • memory-profiler

Transkrypcja

Pobierz transkrypcję

Wprowadzenie i omówienie pamięci bitmapy

  • Fung Lam

    Tak, do lunchu została nam jeszcze godzina, więc zostańcie ze mną.

  • Nazywam się Fung Lam i jestem inżynierem ds. relacji z programistami Androida w Londynie.

  • Dziękuję.

  • Zanim zacznę, czy mogę zadać pytanie?

  • Ilu z Was kupiło w tym roku nowy telefon?

  • Niesamowite.

  • Niesamowite.

  • Bądź szczery, nie będę Cię winić, jeśli nie kupisz Pixela.

  • Nowy telefon jest droższy.

  • Tak, dlatego niektórzy z Was nie kupili nowego telefonu.

  • Dlaczego jest droższy?

  • Czy wiesz jaka jest…

  • Nie.

  • RAM.

  • RAM, tak, tak.

  • Tak, pamięć RAM jest wyzwaniem dla Androida i dla nas, deweloperów.

  • Dlatego dziś opowiem, jak możemy rozwiązać problem z pamięcią RAM.

  • Wcześniej podczas Google I/O i Google I/O Connect odbyły się sesje dotyczące pamięci, w tym sposobu przydzielania pamięci przez system operacyjny, profilowania aplikacji, optymalizowania kodu za pomocą R8 i oczywiście trochę o bitmapach.

  • Na zrzucie ekranu widać moje zdjęcia.

  • Optymalizacja pamięci to duży obszar, który obejmuje różne aspekty, takie jak środowisko wykonawcze Androida, czyszczenie pamięci, narzędzia do analizy wydajności, alokacja pamięci obiektów, kompilator itp. W tej 30-minutowej sesji przekażę Ci kilka informacji o tym, jak zoptymalizować pamięć bitmapy.

  • Możesz się zastanawiać, dlaczego bitmapa.

  • Dlaczego powinniśmy skupić się na pamięci bitmapy?

  • Ponieważ jeden obraz jest wart więcej niż tysiąc słów.

  • To prawda, wyjaśnię Ci dlaczego.

  • Na ekranie telefonu, na przykład Pixela 7, Pixela 9 lub Pixela 11 Pro, rozdzielczość ekranu wynosi około 2500 x 1200 lub 1400.

  • Dla każdego piksela mamy więc 32 bity głębi kolorów.

  • W przypadku obrazu na pełnym ekranie, nie mam wystarczającej liczby palców, ale zajmie to około 15 MB pamięci, 50 MB.

  • Słowo w systemie operacyjnym ma 64 bity, czyli 8 bajtów.

  • Tysiąc słów to tylko 8 MB.

  • Tak, 8K.

  • Zdecydowanie jeden obraz jest wart więcej niż tysiąc słów.

  • Tak, to ważne.

  • Jeden obraz ma już 15 MB.

  • Co, jeśli aplikacja ma 10 lub 100 obrazów?

  • To duża liczba.

  • Zazwyczaj to właśnie bitmapa zużywa najwięcej pamięci w aplikacji.

  • Gdy obraz jest dekodowany do pamięci, jak już wspomniano, jest on rozszerzany do wymiaru i głębi kolorów.

  • Nie chodzi o rozmiar pliku, ale o to, czy jest on w formacie JPEG czy PNG.

  • Nie, zostanie zdekodowany i będzie używać pamięci.

  • Dlatego kilka zdjęć w pełnej rozdzielczości może łatwo zająć setki megabajtów pamięci na urządzeniu.

  • Po drugie, gdy pamięć jest bardzo ograniczona, system operacyjny musi zużywać więcej energii, aby obsłużyć jej wykorzystanie.

  • Może to sprawiać wrażenie, że interfejs działa nieprawidłowo.

  • System operacyjny może też zamknąć aplikację, aby zwolnić pamięć.

  • W przeciwnym razie w aplikacji wystąpi błąd lub wyjątek związany z brakiem pamięci.

  • Na szczęście ta tarcza zwykle sprawdza się świetnie i nie wymaga dużego wysiłku.

  • Większość skoków wykorzystania pamięci wynika z typowych błędów, których można uniknąć, i możemy je naprawić bez pogarszania jakości obrazu.

  • Teraz już wiesz, że pamięć bitmapy jest ważna dla aplikacji, nie tylko ze względu na płynność działania, ale także stabilność i utrzymanie użytkowników.

  • Dlatego dzisiaj omówimy 5 prostych kroków, które pomogą Ci zapewnić sprawne działanie aplikacji.

  • Najpierw przyjrzymy się jakości technicznej w Google Play, nowym wymaganiom dotyczącym jakości technicznej, w tym nadchodzącemu limitowi pamięci, oraz temu, gdzie można łatwo sprawdzić ten limit.

  • Po drugie, przedstawię studium przypadku dotyczące 500 aplikacji na Androida, sposobu, w jaki korzystają z pamięci, czy robią to we właściwy sposób i czy możemy się czegoś od nich nauczyć.

  • Następnie pokażę Ci, jak zoptymalizować wykorzystanie pamięci bitmapy.

  • Na koniec przedstawimy kilka narzędzi, które mogą pomóc Ci sprawdzić własną aplikację.

Standardy jakości Google Play i typowe problemy

  • Fung Lam

    Po pierwsze, nowe wymagania Google Play dotyczące jakości.

  • Jak często odwiedzasz Konsolę Google Play?

  • Co miesiąc?

  • Co tydzień?

  • Nigdy?

  • Te śmiechy oznaczają „tak”, prawda?

  • Nie?

  • Nie.

  • Czy odwiedziłeś(-aś) sekcję Android Vitals?

  • Tak, tak.

  • Tak, więc na pewno coś wiesz, zanim to zrobię.

  • Dlatego w Google Play obowiązują nowe wymagania dotyczące jakości w zakresie progu wydajności.

  • Zespoły Androida i Google Play chcą, aby użytkownicy korzystali z aplikacji wysokiej jakości.

  • Ten próg to również coś, co staramy się zapewnić użytkownikom końcowym.

  • Mówiąc prościej: „Hej, gdy Twoja aplikacja działa w tle lub na widocznej powierzchni na pierwszym planie, 90 percentyl wykorzystania pamięci nie powinien przekraczać tego progu.

  • W przeciwnym razie aplikacja może być mniej widoczna w Sklepie Play”.

  • Egzekwowanie tych zasad w przypadku aplikacji i gier rozpocznie się w lutym 2027 r.

  • Przekroczenie tego progu ma bezpośredni wpływ na ocenę kondycji aplikacji.

  • Dlatego warto wejść do Konsoli Google Play i sprawdzić, jaki jest dzisiaj 90 percentyl.

  • Wprowadź ulepszenia przed upływem terminu.

  • To jest animacja.

  • „Jeśli wejdziesz do Konsoli Google Play, zobaczysz, że są tam Android Vitals.

  • Jest to część pamięci.

  • Możesz też zobaczyć 90 percentyl.

  • Jeśli klikniesz, zobaczysz średnie wykorzystanie pamięci w czasie.

  • Są też różne poziomy pamięci RAM, formatu, gęstości ekranu i wersji systemu operacyjnego, które pomagają wyizolować wąskie gardło pamięci na konkretnym urządzeniu podstawowym lub w konkretnych wersjach aplikacji.

  • Możesz powiedzieć: „Dziękuję za informację, że moja aplikacja przekracza próg.

  • Ale tak naprawdę nie wiem, co jest nie tak.

  • Czy pomożesz?"

  • Tak, dlatego chętnie Ci pomogę.

  • Postaram się to uprościć. Wyobraź sobie, że oglądasz telewizję i nagle znika obraz lub dźwięk. Co robisz?

  • Wyjmujesz instrukcję obsługi telewizora, przechodzisz na ostatnie strony, gdzie zwykle znajduje się przewodnik po rozwiązywaniu problemów, i czytasz: „Jeśli telewizor nie ma dźwięku, naciśnij ten lub tamten przycisk”.

  • Tak, podobnie dzisiaj zidentyfikowałem kilka typowych problemów.

  • Mamy nadzieję, że ten przewodnik rozwiązywania problemów okaże się przydatny.

  • Zwykle, gdy czytamy instrukcję obsługi telewizora, powinniśmy zacząć od pierwszej strony, na której zwykle znajdują się narzędzia diagnostyczne, które pomagają zrozumieć, jak działa telewizor.

  • Teraz jednak wiem, że są one mniej interesujące, więc dzisiaj najpierw zajmę się rozwiązywaniem problemów, a później wrócimy do tych kwestii.

  • Szczerze mówiąc, właśnie tak czytam instrukcję obsługi telewizora.

  • Aby wykryć najczęstsze problemy z pamięcią bitmapy w ekosystemie Androida, przeanalizowałem około 500 popularnych aplikacji na Androida.

  • Uruchamiam aplikację, loguję się, przechodzę na różne strony, zagłębiam się w nie i wracam.

  • Testuję aplikację na różnych urządzeniach z różną ilością pamięci RAM, np. na urządzeniu z Androidem Go z 2 GB pamięci, na urządzeniu podstawowym z 4 GB, na urządzeniu średniej klasy z 4, 6 lub 8 GB, a także na urządzeniu składanym klasy premium z bardzo dużym ekranem o bardzo dobrej jakości i 16 GB pamięci.

  • Zmierzyliśmy wykorzystanie pamięci na tym urządzeniu w różnych stanach aplikacji, przechwytując zrzut sterty, i użyliśmy różnych narzędzi, w tym umiejętności AI Perfetto, aby przeanalizować alokację i zidentyfikować typowe antywzorce, czyli typowe błędy, które popełniamy na dużą skalę.

  • Tak zmieniała się pamięć w różnych stanach aktywności.

  • Dzięki tej analizie identyfikujemy ponad 15 typowych problemów, od zbyt dużych obrazów po zwiększanie pamięci podręcznej w tle.

  • To jest główne studium przypadku, które dziś omówimy.

Studia przypadków: określanie rozmiaru i dekodowanie bitmap

  • Fung Lam

    Tak, całe studium przypadku, które teraz udostępniam, dotyczy badania 500 aplikacji.

  • Skoro tu jesteście, to chyba wszyscy kochamy Androida, prawda?

  • Wszyscy kochamy Androida.

  • Załóżmy, że ktoś z nas też uwielbia Androida, ale nie jest ekspertem w dziedzinie programowania na tę platformę.

  • Tworzy więc aplikację na Androida do zamawiania figurek Androida.

  • Ale są pewne błędy.

  • Porozmawiajmy. Może to jest pierwszy błąd.

  • „Na stronie produktu mamy układ dwukolumnowy, który pokazuje różne figurki Androida.

  • W każdej kolumnie obraz ma rozmiar około 400 x 400 pikseli.

  • Jednak aplikacja pobiera z serwera tylko 1 obraz, niezależnie od rozmiaru ekranu.

  • Pobierany jest obraz o rozmiarze 3000 x 3000.

  • Tak, bo deweloperzy mówią: „Android jest taki piękny, taki niesamowity.

  • Chcę pokazać użytkownikowi to, co najlepsze”.

  • Ale tak naprawdę nie, ponieważ jest on ograniczony przez wymiar widoku obrazu.

  • Nawet jeśli pobierzesz duży obraz, nie będzie on wyświetlany użytkownikowi w najlepszej jakości.

  • Dodatkowo ten duży obraz spowodował dodatkowe zużycie pamięci.

  • Nawet jeśli zmniejszymy próbkowanie, proces ten również wymaga dodatkowej pamięci.

  • Jak możemy pomóc deweloperowi rozwiązać ten problem?

  • Jeśli znasz sieci CDN, wiesz, że mogą one obsługiwać niektóre operacje na obrazach, takie jak przycinanie, zmiana rozmiaru, obracanie, stosowanie filtrów czy zmiana kształtu z kwadratowego na okrągły.

  • W przypadku żądania HTTP sieć CDN może coś obsłużyć.

  • Dlatego nawet jeśli po stronie serwera hostowany jest tylko 1 obraz, możesz użyć sieci CDN, aby wysłać na urządzenie obraz o zmienionym rozmiarze.

  • Po pierwsze, gdy prosisz o obraz, spróbuj użyć CDN, aby poprosić o obraz w rozmiarze ImageView.

  • Jeśli jednak nie jest to możliwe, np. gdy obraz jest nieco większy niż ImageView, pamiętaj o zmniejszeniu próbkowania, aby zmienić jego rozmiar w kodzie.

  • Dzięki temu podczas wczytywania do pamięci ładowany jest mniejszy obraz, a nie obraz w pełnej rozdzielczości.

  • Oto przykładowy kod dla Coil.

    // 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
    val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
    
    // 2. Enforce exact downsampling + Hardware Bitmap config
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(sizedUrl)
            .size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
            .precision(Precision.EXACT)      // Downsample to exact card size
            .bitmapConfig(Bitmap.Config.HARDWARE)
            .build(),
        contentDescription = product.title
    )
  • Możesz przekazać rozmiar jako tekst zaznaczony na żółto.

  • Tak, możesz przekazać rozmiar jako HTTP w żądaniu HTTP.

  • W podświetlonym kodzie widać, że nie należy używać Size.ORIGINAL.

  • Tak, użyj szerokości i wysokości, aby zostały przekazane jako parametr.

  • Użyj precyzji, aby zmniejszyć obraz do dokładnego rozmiaru.

  • Jeśli to możliwe, używaj Bitmap.Config.HARDWARE, które również zużywają mniej pamięci, ponieważ obraz będzie przechowywany bezpośrednio w pamięci VRAM.

  • Podobnie w tej aplikacji, powiedzmy, że wydają nową wersję figurek kolekcjonerskich Androida, więc jest długi obraz, który użytkownik może przesunąć w lewo i w prawo, aby zobaczyć różne projekty nowej figurki Androida.

  • Ale jest też inny problem.

  • W przypadku długiego obrazu duża jego część znajduje się poza ekranem, co oznacza, że użytkownik nie może go zobaczyć, ale obraz jest wczytywany do pamięci.

  • Czy możemy to zrobić lepiej?

  • Tak.

  • Możemy podzielić go na mniejsze obrazy i wyświetlać tylko ten, który jest widoczny na ekranie.

  • Te, które znajdują się poza ekranem, nie zostaną wczytane lub zostaną usunięte.

  • Dzięki temu będzie zużywać mniej pamięci.

  • Podobnie, w tym przypadku zaoszczędzono około 60% pamięci.

  • Oczywiście, jeśli przytniesz do mniejszego prostokąta, zaoszczędzisz więcej.

  • Jeśli jednak użytkownik chce udostępnić funkcję, która pozwala powiększyć obraz, aby zobaczyć każdy szczegół figurki Androida, częstym błędem jest wczytywanie bardzo dużego obrazu, który można powiększać i powiększać.

  • Nie musisz tego robić.

  • Lepszym rozwiązaniem jest sytuacja, gdy użytkownik powiększa oryginalny, już wczytany obraz o niskiej rozdzielczości, a gdy przestaje go powiększać, obliczasz współrzędne i wczytujesz tylko ten obszar, czyli obliczony obszar z obrazu o wysokiej rozdzielczości.

  • Dlatego do pamięci zostanie wczytany tylko ten widoczny obszar.

  • Jak to zrobić?

  • Istnieje interfejs API o nazwie BitmapRegionDecoder, który pomaga wczytać określony region obrazu.

  • Oczywiście są też narzędzia i biblioteki, które mogą Ci pomóc.

  • Możesz go wyszukać na GitHubie.

  • Niektóre aplikacje z komiksami lub mapami już z tego korzystają.

  • Możesz ich użyć jako punktu odniesienia.

  • Poza tym jesteśmy ludźmi i popełniamy błędy.

  • AI też popełnia błędy.

  • Aby uniknąć błędów, możemy też dodać limit podczas wczytywania obrazu.

  • Na przykład w tym przypadku mogę powiedzieć: „Hej, użyjmy rozmiaru okna lub wymiaru ekranu jako limitu i umieśćmy go w funkcji min, min szerokości żądania lub szerokości granicy”.

    // Query active window bounds instead of hardcoding 1080 / 1920
    val bounds = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(activity).bounds
    
    // Clamp target dimensions so Bitmap NEVER exceeds the active window
    val safeWidth  = minOf(requestedWidthPx,  bounds.width())
    val safeHeight = minOf(requestedHeightPx, bounds.height())
    
    val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)
  • Dzięki tej funkcji za każdym razem, gdy wczytuję obraz, nie przekracza on granicy.

  • Nie będzie więc wczytywać obrazu o rozdzielczości większej niż rozdzielczość ekranu.

  • Pamiętaj, aby dekodować tylko widoczny obszar obrazu, który pozwoli zaoszczędzić pamięć.

Zarządzanie pamięcią, duplikatami i pamięcią podręczną

  • Fung Lam

    I ponownie: powiedzmy, że ta aplikacja nie pochodzi od jednego dewelopera.

  • Właściwie jest to dla wielu współtwórców, którzy mogą używać różnych narzędzi.

  • Na przykład część z nich używa… Przepraszam.

  • Zacznę od nowa.

  • To samo dotyczy tej aplikacji.

  • Na stronie produktu mam listę produktów i widzę ten obraz. Dodaję go do koszyka.

  • Kilka minut później przechodzę do kasy i widzę ten sam obraz.

  • Ale czy to naprawdę ten sam obraz, czy może są to 2 różne obiekty w pamięci?

  • Tak, jednym z częstych błędów, które znaleźliśmy, jest to, że aplikacja wczytuje dużo obrazów.

  • Jednak w pamięci znajduje się też wiele zduplikowanych obrazów.

  • Jeśli używasz modułu do wczytywania obrazów, możesz przekazać klucz do buforowania.

  • Dzięki temu, gdy używany jest ten sam obraz, można użyć już przydzielonego obiektu Bitmap, zamiast tworzyć nowy obiekt.

  • W Android Studio możesz użyć narzędzia Memory Profiler, aby sprawdzić, czy aplikacja ma zduplikowane bitmapy.

  • Omówiłem to podczas sesji na Google I/O, więc możesz wrócić do YouTube, aby obejrzeć film.

  • Jeśli nie używasz Android Studio, możesz też użyć polecenia adb, aby przechwycić zrzut sterty, a następnie użyć języka SQL wykresu sterty Perfetto, aby sprawdzić, czy aplikacja ma zduplikowaną bitmapę.

  • Oto przykład kodu w Coil, który pokazuje, jak dodać klucz do buforowania bitmapy.

    // Strip volatile query tokens (?from=list / ?sig=...) for cache key
    val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
    
    // On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(productUrl)
            .placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
            .memoryCacheKey(canonicalKey)
            .diskCacheKey(productUrl.substringBefore("?"))
            .build(),
        contentDescription = product.title
    )
  • A teraz? Tak.

  • Wróćmy do poprzedniego przykładu.

  • Moja aplikacja ma innego dewelopera.

  • Niektóre z nich używają widoków, więc Glide jest używany jako moduł wczytywania obrazów.

  • Niektóre z nich używają Coil, ponieważ korzystają z Compose.

  • Wspomniałeś, że moduł wczytywania obrazów pomaga w buforowaniu. Czy to źle, jeśli mam więcej niż jeden moduł wczytywania obrazów?

  • Zdecydowanie coś jest nie tak.

  • W przypadku modułu wczytywania obrazów domyślny rozmiar pamięci podręcznej będzie wynosił X% rozmiaru pamięci sterty.

  • Jeśli więc masz jeden moduł wczytywania obrazów, wykorzystuje on X% pamięci.

  • Jeśli masz 2 moduły wczytywania obrazów, pamięć będzie 2 razy większa.

  • Jeśli masz 3 moduły ładowania obrazów, będzie to 3X.

  • Jeśli moduł wczytywania obrazów nie korzysta z tej samej puli, efekt będzie taki sam.

  • Na przykład na stronie produktu wczytuję obraz za pomocą Glide, a następnie na stronie płatności ten sam obraz jest wczytywany za pomocą Coil.

  • We wspomnieniu nadal będą 2 obrazy.

  • To nie jest dobre rozwiązanie.

  • Tak, więc aby to naprawić, najlepiej użyć jednego modułu wczytywania obrazów lub jednej współdzielonej puli do buforowania.

  • Ale co, jeśli wymaga to dużo pracy inżynieryjnej, której nie jestem w stanie wykonać?

  • Łatwiejszym rozwiązaniem będzie zmierzenie, jaki procent obrazu będzie obsługiwany przez ten moduł, a jaki przez inny, i ustawienie wagi dla różnych modułów, tak jak w tym kodzie.

    val totalBudgetBytes = computeAdaptiveCacheBytes(context)
    val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
    
    // Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
    val loaderABudget = (totalBudgetBytes * 0.70).toInt()
    val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
    
    // Approach B (Simple Fix without stats): Divide by number of loaders!
    val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
    
    MemoryCache.Builder()
        .maxSizeBytes(simpleSplitBudget)
        .build()
  • Mogę ustawić loaderABudget na 70%, a loaderBBudget na 30%. To kolejny sposób.

  • Jeśli nie wiesz, ile obrazów będzie obsługiwanych przez poszczególne moduły, najlepiej po prostu podzielić je po równo.

  • Jeśli mam 2 moduły wczytywania obrazów, dzielę przez 2, a jeśli 3, dzielę przez 3, zamiast używać wartości domyślnej, ponieważ spowodowałoby to nadmierne wykorzystanie pamięci.

  • Załóżmy, że na stronie produktu oczekujemy, że użytkownik przewinie stronę, aby zobaczyć więcej informacji o produkcie.

  • Zwykle zapisujemy w pamięci podręcznej około 50 obrazów lub 50 MB obrazów.

  • Co się jednak stanie, gdy aplikacja przejdzie do działania w tle?

  • Jeśli aplikacja działa w tle, użytkownik może nie przeglądać treści.

  • Jeśli użytkownik nie przegląda, czy nadal mam przechowywać tę ilość pamięci podręcznej obrazu w pamięci?

  • Nie, powinniśmy go wypuścić.

  • Tak, powinniśmy uwzględniać wywołanie zwrotne onTrimMemory.

  • Tak, wiele aplikacji obecnie nie implementuje ani nie respektuje wywołania zwrotnego onTrimMemory.

  • W przypadku wywołania zwrotnego onTrimMemory należy zwrócić uwagę na 2 sygnały.

  • Jednym z nich jest UI_HIDDEN, który działa tak, jakby użytkownik nacisnął przycisk ekranu głównego. Nie następuje to natychmiast po naciśnięciu przycisku ekranu głównego, ale po pewnym czasie. Oznacza to, że aplikacja jest w stanie, w którym nie ma widocznego interfejsu użytkownika.

  • Możemy się spodziewać, że użytkownik nie korzysta z Twojej aplikacji.

  • Zwolnij zasoby dla systemu operacyjnego”.

  • Dlatego powinniśmy zmniejszyć rozmiar pamięci podręcznej.

  • Drugi to TRIM_MEMORY_BACKGROUND, co oznacza, że aplikacja działa w tle od dłuższego czasu, a system operacyjny potrzebuje zasobów.

  • Na tym etapie należy zwolnić jak najwięcej pamięci, prawie 100%.

  • W ten sposób zmniejszasz też ryzyko, że system operacyjny zamknie Twoją aplikację.

  • Bo „Hej, chcą pamięci, dajesz im ją, a potem system operacyjny się tym zajmuje”.

  • Jeśli nie, system operacyjny nie ma wyboru i musi coś zrobić.

Zapobieganie wzrostowi wykorzystania pamięci w tle

  • Fung Lam

    Inny poważny błąd, który często widzę, to: „Wspomniałem, że jeśli aplikacja działa w tle, powinna zużywać mniej pamięci”.

  • Zauważyłem jednak, że aplikacje zużywają więcej pamięci w tle.

  • Dlaczego?

  • Dlatego właśnie szukam informacji.

  • Próbowałem dowiedzieć się, dlaczego tak jest.

  • Jednym z powodów jest to, że w aplikacji jest animacja lub karuzela.

  • Na przykład pobierają nowy obraz co 15 sekund i wyświetlają go użytkownikowi”.

  • Deweloper nie zatrzymał jednak animacji, gdy aplikacja działa w tle.

  • Nawet gdy aplikacja działa w tle, kod nadal jest wykonywany, pobiera nowy obraz, dekoduje go i zużywa więcej pamięci.

  • Aplikacja nie powinna tego robić.

  • Zgadza się.

  • Gdy aplikacja przechodzi do działania w tle, powinniśmy zatrzymać wszystkie animacje, buforowanie i niepowiązane zadania.

  • Jeśli znasz cykl życia aktywności, to wiesz, że są zdarzenia onPause i onStop. Pamiętaj, aby zaimplementować onStop, onPause i onDestroyView, aby zatrzymać animację, żądania pobierania obrazów lub inne rzeczy związane z pracą, które nie wymagają uwagi użytkownika.

    override fun onStop() {
        super.onStop()
        // 1. Stop animations so they don't decode next frames in background!
        bannerAnimationView.cancelAnimation()
        animatedImageDrawable?.stop()
    
        // 2. Cancel next-image buffering / prefetch queues!
        nextImagesBufferingJob?.cancel()
        Glide.with(this).pauseRequests()
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        bannerAnimationView.clearAnimation() // Release resources!
    }

Zasada 3R dotycząca zdrowia pamięci

  • Fung Lam

    Wiem, że rozmawialiśmy o wielu sprawach, ale powiedziałem: „Mam ponad 15 spraw”.

  • Jeśli opowiem Ci o wszystkich przypadkach, możesz o wszystkim zapomnieć.

  • Po powrocie do domu po prostu wyszukasz w Google „Hej, jak rozwiązać problemy z pamięcią?”.

  • Dlatego ułatwiam Ci ich zapamiętywanie.

  • Zgadza się.

  • Aby ułatwić Ci ich zapamiętanie, powiążę je z bardzo dobrze znaną zasadą 3R.

  • Myślę, że każdy powinien znać zasadę 3R.

  • Pierwszy to zmniejszenie wykorzystania pamięci podczas wczytywania obrazu.

  • Of course, like I mentioned, we could downsample, get the right dimension of the image when we get from the server.

  • Optymalizujemy głębię kolorów lub, jeśli to możliwe, używamy sprzętowej bitmapy.

  • Jeśli nie używasz sieci CDN, spróbuj jej użyć.

  • Dlatego gdy pobieramy obraz z serwera, pobieramy tylko ten, który najlepiej pasuje do rozmiaru.

  • Ponowne użycie – używamy pamięci podręcznej, aby obraz wczytany do pamięci był używany ponownie i nie było duplikatu obrazu bitmapowego w pamięci.

  • Oczywiście staraj się nie mieć w kodzie wielu modułów wczytywania obrazów, ponieważ jest to mniej wydajne.

  • Ograniczaj, wykorzystuj ponownie i co jeszcze?

  • Czy wiesz jaka jest…

  • Recykling.

  • Spróbować ponownie?

  • Recykling, tak, recykling.

  • Tak, w szkole podstawowej uczymy się o ograniczaniu, ponownym wykorzystywaniu i recyklingu, ale nie o kodowaniu.

  • Tak, recykling, tak.

  • Tak, chodzi o to, jak zwolnić pamięć, aby mogła być używana przez inną aplikację lub system operacyjny.

  • There there are things like about recycle.

  • Po pierwsze, wspomniałem o zdarzeniu onTrimMemory. System operacyjny wysyła 2 sygnały, gdy interfejs jest ukryty lub gdy system naprawdę potrzebuje zasobów.

  • Tak, spróbuj obsłużyć tę prośbę.

  • Jeśli masz animację, moduł wczytywania obrazów lub inne zadanie, które jest wykonywane na pierwszym planie, zatrzymaj je, gdy wystąpi zdarzenie onPause, onStop.

  • Ograniczaj, wykorzystuj ponownie i poddawaj recyklingowi.

  • Łatwo zapamiętać, prawda?

Zestaw narzędzi diagnostycznych dla deweloperów i analiza AI

  • Fung Lam

    Teraz wracamy do mniej interesującej części instrukcji obsługi telewizora.

  • Jak możemy używać różnych zestawów narzędzi do wykrywania i skutecznego pomiaru tego problemu z pamięcią?

  • Pierwszy to Konsola Google Play, Android Vitals i interfejs API do raportowania.

  • Tak, sprawdź 90 percentyl w Konsoli Google Play.

  • Gdzie teraz jesteś?

  • Jeśli nie odwiedzasz strony internetowej, możesz też użyć interfejsu Reporting API.

  • Nadal możesz używać interfejsu API, aby uzyskać wartość.

  • Tak, upewnij się, że progi to 200 MB i 400 MB.

  • Upewnij się, że nie przekraczasz progu.

  • Dostępne są też polecenia ADB, które pomogą Ci np. przechwycić zrzut sterty lub wysłać zdarzenie, aby zasymulować, że aplikacja jest teraz na etapie UI_HIDDEN.

    adb shell dumpsys meminfo <pkg>  # watch Native Heap!
    adb shell am send-trim-memory <pkg> UI_HIDDEN
  • Jeśli używasz Android Studio, to dobrze.

  • Mimo to zawsze jest Twoim dobrym przyjacielem.

  • Możesz użyć profilera Android Studio, aby przechwycić zrzut sterty i wyświetlić w interfejsie użytkownika informacje o wykorzystaniu i alokacji pamięci.

  • Oczywiście rozmawiamy o AI z Gemini w Android Studio lub innych dostępnych umiejętnościach Androida.

  • Pomaga on analizować zrzut stosu i używać logów czasu Perfetto SQL do znajdowania bitmapy, aby zwykle określić, kto jest właścicielem pamięci podręcznej.

  • Czy mój obraz jest wczytywany przez Glide czy Coil?

  • Czy w mojej aplikacji występują inne wycieki pamięci?

  • Niektórzy z Was mogą powiedzieć: „Nie używam już IDE.

  • Jestem inżynierem promptów.

  • Używam, tak, po prostu wysyłam prompt do agenta AI, aby napisał kod”.

  • Tak, wiem.

  • W wolnym czasie zajmuję się też vibe codingiem.

  • Udostępniam Ci ten przykład,

  • Tak, to tylko część prompta, którego używam do wyszukiwania aplikacji 500.

  • Tak, tylko część.

  • Ekran nie jest duży.

  • Jeśli dodam więcej, będzie to ledwo widoczne.

  • ale dają wyobrażenie o tym, jak można wykorzystać AI do automatyzacji, rejestrowania wykorzystania pamięci, w tym zrzutu sterty, oraz do analizowania i przedstawiania wyników.

  • Mogę też dodać jeszcze jedno zdanie: „Podaj mi też poprawkę do mojego kodu”.

  • Mam nadzieję, że to pomoże Ci zmniejszyć obawy. „Pamięć to duża domena.

  • Na studiach informatycznych nie uczyłem się o systemach operacyjnych ani architekturze systemów komputerowych.

  • Ale tak naprawdę nie jest to takie straszne ani trudne”.

  • Tak, dzięki temu możesz łatwo analizować swoją aplikację.

  • Spróbuj.

  • Oczywiście AI będzie popełniać błędy, więc podstawowa wiedza o wykorzystaniu pamięci i sposobie jej przydzielania przez system operacyjny jest nadal ważna.

  • Dlatego zapraszam na moją prezentację i do obejrzenia naszego filmu na YouTube.

Najważniejsze informacje i podsumowanie

  • Fung Lam

    Podsumujmy więc, co udało nam się ustalić.

  • Po pierwsze: ograniczaj.

  • Upewnij się, że rozmiar obrazu jest prawidłowy.

  • Dzielisz obraz na mniejsze obrazy, aby nie ładować wszystkiego do pamięci, tylko obraz, który jest widoczny dla użytkownika.

  • Jeśli to możliwe, używaj sprzętowych kluczy bitmapConfig lub RGB_565.

  • Używaj ponownie, więc upewnij się, że masz strategię buforowania, i używaj modułu do wczytywania obrazów, aby zmniejszyć liczbę zduplikowanych bitmap w pamięci.

  • Oczywiście nie używaj wielu obrazów bitmapowych, nawet jeśli próbujesz zastąpić domyślny rozmiar pamięci podręcznej tych modułów ładowania obrazów.

  • Nie używamy ich ponownie ani nie poddajemy recyklingowi.

  • Zgadza się.

  • Zarządzanie pamięcią aplikacji powinno być dostosowane do jej stanu.

  • Jeśli interfejs jest ukryty, powinno być używane mniej pamięci.

  • Jeśli system operacyjny poprosi o zwolnienie pamięci, zwolnij jej jak najwięcej.

  • I przestać zużywać zasoby, niezależnie od tego, czy chodzi o bitmapę, energię czy zasoby obliczeniowe.

  • Tak, zatrzymaj minutnik banera, zatrzymaj animację, zatrzymaj pobieranie obrazów.

  • Na koniec pamiętaj o monitorowaniu.

  • Regularnie odwiedzaj Konsolę Google Play.

  • Upewnij się, że jest to ćwiczenie.

  • Tak, dzięki temu dowiesz się, jaki jest aktualny stan 90 percentyla wykorzystania pamięci przez aplikację w całej populacji użytkowników.

  • Pomoże Ci to również we wczesnym wykrywaniu problemów.

  • Zgadza się.

  • Do profilowania lokalnego możesz użyć narzędzia Memory Profiler w Android Studio.

  • Możemy też użyć polecenia adb.

  • Mamy też umiejętność Android.

  • Jeśli chcesz zobaczyć statystyki dotyczące prawdziwych użytkowników, Konsola Google Play jest odpowiednim miejscem.

  • Możesz też użyć interfejsu Google Play Reporting API, aby uzyskać te dane bez logowania się w Konsoli Play.

  • Jeśli chcesz dowiedzieć się więcej, odwiedź stronę developer.android.com/memory.

  • Ograniczaj, wykorzystuj ponownie i poddawaj recyklingowi.

  • Możemy stworzyć lepszy… Mam nadzieję, że wspólnie uda nam się stworzyć lepszy ekosystem Androida, aby sprostać wyzwaniom, przed którymi stoimy.

  • Dziękujemy i życzymy udanego kodowania.

  • Wkrótce będę mieć dyżur na zewnątrz, więc jeśli masz pytania, zapraszam.

  • Kolejny prelegent pojawi się za chwilę, więc prosimy o cierpliwość. Dziękujemy.

Kod

Fragmenty kodu

Fragment kodu w języku Kotlin w 11:50 w filmie „Studia przypadków: określanie rozmiaru i dekodowanie bitmap”
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"

// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(sizedUrl)
        .size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
        .precision(Precision.EXACT)      // Downsample to exact card size
        .bitmapConfig(Bitmap.Config.HARDWARE)
        .build(),
    contentDescription = product.title
)
Fragment kodu w Kotlinie w 14:45 w filmie „Studia przypadków: określanie rozmiaru i dekodowanie map bitowych”
// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
    .computeCurrentWindowMetrics(activity).bounds

// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth  = minOf(requestedWidthPx,  bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())

val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)
Fragment kodu w Kotlinie w 16:51 w filmie „Zarządzanie pamięcią, duplikatami i pamięcią podręczną”
// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))

// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
    model = ImageRequest.Builder(context)
        .data(productUrl)
        .placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
        .memoryCacheKey(canonicalKey)
        .diskCacheKey(productUrl.substringBefore("?"))
        .build(),
    contentDescription = product.title
)
Fragment kodu w języku Kotlin w 18:17 w filmie „Zarządzanie pamięcią, duplikatami i pamięcią podręczną”
val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)

// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()

// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders

MemoryCache.Builder()
    .maxSizeBytes(simpleSplitBudget)
    .build()
Fragment kodu w języku Kotlin w 22:05 w filmie „Preventing background memory growth”
override fun onStop() {
    super.onStop()
    // 1. Stop animations so they don't decode next frames in background!
    bannerAnimationView.cancelAnimation()
    animatedImageDrawable?.stop()

    // 2. Cancel next-image buffering / prefetch queues!
    nextImagesBufferingJob?.cancel()
    Glide.with(this).pauseRequests()
}

override fun onDestroyView() {
    super.onDestroyView()
    bannerAnimationView.clearAnimation() // Release resources!
}
Fragment kodu powłoki w 25:17 w filmie „Developer diagnostic toolkit & AI analysis”
adb shell dumpsys meminfo <pkg>  # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN