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.
BitmapRegionDecoder.onTrimMemory. Zatrzymaj odpytywanie i animacje w tle w onStop, aby zapobiec zwiększaniu wykorzystania pamięci w tle.Transkrypcja
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ę.
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.
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ęć.
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ć.
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!
}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?
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_HIDDENJeś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.
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.
// 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
)// 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)// 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
)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()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!
}adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN