Kod, który piszesz, sam w sobie jest formą wykorzystania pamięci. Każda klasa, metoda i stała tekstowa w aplikacji musi zostać załadowana do pamięci RAM podczas wykonywania. Im większa jest baza kodu aplikacji, tym więcej pamięci zużywa ona tylko po to, aby istniała.
Pamięć oparta na plikach i stronicowanie na żądanie
Android wczytuje kod wykonywalny z .apk (np. pliki .oat lub .so) za pomocą mmap. Oznacza to, że kod jest oparty na pliku.
Co ważne, Android używa stronicowania na żądanie. Gdy aplikacja się uruchamia, jądro nie wczytuje od razu całego pliku APK do pamięci RAM. Zamiast tego mapuje tylko plik do wirtualnej przestrzeni adresowej procesu. Gdy aplikacja jest wykonywana, a procesor przechodzi do nowej funkcji, wywołuje to „błąd strony”. Jądro wstrzymuje wątek, odczytuje z pamięci masowej do fizycznej pamięci RAM konkretną stronę kodu o rozmiarze 4 KB i wznawia wykonywanie.

Oznacza to, że kod, który pakujesz, ale nigdy nie wykonujesz, nie używa pamięci fizycznej na potrzeby samych stron kodu. Nieużywane biblioteki nadal zwiększają jednak ogólny rozmiar pliku APK i mogą znacznie zwiększyć ilość pamięci używanej przez wewnętrzne metadane systemu (takie jak indeksy DEX i deskryptory klas), które muszą zostać odczytane, aby wiedzieć, że kod istnieje. Poza tym wiele bibliotek zawiera statyczne inicjatory lub jest używanych przez platformy wstrzykiwania zależności podczas uruchamiania aplikacji, co powoduje, że i tak są one wczytywane do pamięci RAM.
Usuwanie stron i spowolnienia
Pamięć opartą na plikach można zawsze odczytać z pamięci masowej, dlatego jądro uznaje te strony za „czyste”. Gdy system odczuwa presję na pamięć, jądro usuwa (zwalnia) te czyste strony kodu z pamięci RAM, aby zrobić miejsce na inne elementy.
Jeśli aplikacja będzie później musiała ponownie wykonać ten kod, procesor zgłosi błąd i jądro systemu będzie musiało ponownie odczytać stronę z pamięci. Im więcej kodu zawiera aplikacja, tym większe jest ryzyko usunięcia jej kodu. Gdy użytkownik wróci do rozbudowanej aplikacji po użyciu innych aplikacji, będzie doświadczać losowych zacięć i spowolnień, ponieważ procesor będzie się ciągle zatrzymywać w oczekiwaniu na przywrócenie kodu z pamięci.
Koszt błędu strony: chociaż zależy on w dużej mierze od szybkości pamięci urządzenia (UFS vs. eMMC) i stanu jądra, poważny błąd strony (odczytanie 4 KB z pamięci) może kosztować od 0,5 ms do 5 ms. Jeśli ścieżka uruchamiania aplikacji obejmuje 500 różnych stron nieoptymalnego kodu, możesz łatwo wprowadzić kilkaset milisekund czystego opóźnienia wejścia/wyjścia do czasu uruchamiania aplikacji.
Sprawdzanie rozmiaru kodu za pomocą narzędzia Compiler Explorer
Aby zrozumieć, jak kod Java lub Kotlin jest tłumaczony na natywny kod maszynowy (a tym samym na bajty pamięci), możesz użyć Compiler Explorer.
Obsługa Androida jest wbudowana bezpośrednio w Godbolt. Pozwala zobaczyć, jak różne części łańcucha narzędzi Androida (D8, R8 i dex2oat) przekształcają kod źródłowy.
Jak korzystać z Compiler Explorer na Androidzie
- Otwórz stronę godbolt.org.
- W menu języka (w lewym górnym rogu) wybierz Android Java lub Android Kotlin.
- W menu kompilatora (w prawym górnym rogu panelu kodu) możesz wybrać jedno z tych narzędzi:
d8: wyświetla kod bajtowy Dalvik (.dex). Jest to najbardziej zbliżona do oryginalnego kodu i łatwiejsza do odczytania reprezentacja.r8: pokazuje, jak optymalizator R8 zmniejsza i optymalizuje kod bajtowy.dex2oat: wyświetla końcowy kod maszynowy ARM64, który jest wykonywany na urządzeniu. W tym miejscu możesz zobaczyć rzeczywisty wpływ na pamięć (4 bajty na instrukcję).dex2oatmoże być kierowany na różne zestawy instrukcji, ale ARM64 jest najczęściej używany w przypadku telefonów komórkowych.
- Podświetlanie źródła i danych wyjściowych: najechanie kursorem na wiersz kodu spowoduje podświetlenie odpowiednich instrukcji kodu bajtowego lub kodu maszynowego, co ułatwi śledzenie wpływu poszczególnych instrukcji.
- Potok optymalizacji: w widoku dezasemblacji możesz kliknąć Dodaj nowy... -> Optymalizacja potoku. Dzięki temu możesz zobaczyć wewnętrzne kroki wykonywane przez kompilator. Możesz sprawdzić, jak reprezentacja wewnętrzna jest przekształcana na każdym etapie (np. między krokami „Inliner (before)” i „Inliner (after)”) przed przekształceniem w końcowy kod maszynowy ARM64.

Dlaczego jest to ważne dla pamięci
Każda instrukcja w dex2oat, która jest kierowana na zestaw instrukcji ARM64, zajmuje 4 bajty w pliku wykonywalnym aplikacji (.odex lub .oat).
Spróbuj wpisać kod, który wykorzystuje różne funkcje języka, i przeanalizuj dane wyjściowe kompilatora:
- Dostęp do tablicy a iteratory list:
- Prosta pętla tablicy w
int[]może zostać skompilowana do około 10 instrukcji (około 40 bajtów). - Pętla foreach w przypadku
Listniejawnie używaIterator. Może to spowodować wykonanie 30–40 instrukcji (ok. 160 bajtów) ze względu na dodatkowe wywołania metod (hasNext(),next()) i przydzielenie samego obiektu iteratora. - Optymalizacja R8: w odpowiednich warunkach (np. gdy
Listokazuje sięArrayList) optymalizator R8 może przekształcić pętlę foreach z powrotem w prostą pętlę indeksowaną, eliminując narzut iteratora i zmniejszając zarówno rozmiar kodu, jak i fragmentację pamięci w czasie działania.
- Prosta pętla tablicy w
- Wirtualne wywołania metod: obejmują wczytanie klasy obiektu, znalezienie metody w
vtablei rozgałęzienie. Zwykle zajmuje to 4–5 instrukcji (ok. 20 bajtów). - Wywołania bezpośrednie/statyczne: często przekładane na pojedynczą instrukcję
bl(Branch with Link) (4 bajty). - Wyrażenia lambda w Kotlinie: mogą generować całe klasy anonimowe i dodatkowe metody pomostowe, co w przypadku prostego bloku funkcyjnego powoduje dodanie setek bajtów kodu i metadanych.
Korzystając z Compiler Explorer, możesz sprawdzić, jak zaawansowane funkcje języka (takie jak lambdy w Kotlinie, interfejsy API strumieni lub intensywne używanie typów generycznych) wpływają na ostateczny rozmiar skompilowanej aplikacji oraz jak optymalizatory, takie jak R8, mogą w niektórych przypadkach przeciwdziałać kosztom abstrakcji językowych. To narzędzie pomoże Ci podejmować świadome decyzje dotyczące kompromisów podczas projektowania i wdrażania aplikacji.
Ogólnie rzecz biorąc, im bardziej złożony jest kod aplikacji, tym większe jest wykorzystanie pamięci. Z kolei prostszy kod – lub kod uproszczony przez R8 – ma mniejszą reprezentację w postaci instrukcji procesora i bajtów w pamięci masowej i RAM.
Mierzenie wpływu kodu za pomocą zmiennych meminfo i showmap
Aby sprawdzić, ile pamięci zużywa kod aplikacji, możesz użyć standardowych narzędzi do zarządzania pamięcią na Androidzie.
dumpsys meminfo
Gdy uruchomisz adb shell dumpsys meminfo <package>, w sekcji Podsumowanie aplikacji w kategorii Kod zobaczysz ogólne informacje o pamięci związanej z kodem:
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
Aby uzyskać bardziej szczegółowy widok, użyj elementu showmap. Ujawnia regiony poza określonymi plikami mapowanymi do pamięci.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
Zobaczysz wpisy dotyczące skompilowanego kodu aplikacji:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
Martwy kod i R8
Każda wykonana metoda zajmuje pamięć, więc „rozbudowana” aplikacja z niepotrzebnymi inicjalizacjami lub nieużywanymi bibliotekami może poważnie wpłynąć na wydajność uruchamiania i podstawowe wykorzystanie pamięci.
Dlatego tak ważne są narzędzia takie jak R8 (ProGuard). R8 analizuje kod bajtowy aplikacji i usuwa wszystkie klasy lub metody, które nigdy nie są wywoływane („usuwanie martwego kodu”).
Praktyczne ćwiczenie: koszt nadmiaru
Aby zademonstrować wpływ rozmiaru kodu, rozważ eksperyment porównujący 2 wersje aplikacji zawierające 300 wygenerowanych klas (każda z 500 metodami):
- CodeBloat (niezoptymalizowana): standardowa, niezoptymalizowana kompilacja zawierająca wszystkie wygenerowane klasy i unikalne ciągi znaków.
- CodeBloatOptimized: ten sam kod źródłowy, ale skompilowany z włączonym zmniejszaniem rozmiaru za pomocą R8.
1. Kompilacja z wyprzedzeniem (AOT)
Aby zmaksymalizować wpływ pamięci opartej na plikach, użyjemy narzędzia cmd package compile
do kompilacji aplikacji z wyprzedzeniem (AOT) do plików .oat.
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
Pamiętaj, że to tylko przykład. Zwykle aplikacje korzystają z speed-profiletrybu kompilacji (więcej informacji znajdziesz poniżej).
2. Uruchom i porównaj
Aby zobaczyć prawdziwe uruchomienie „na zimno”, w którym system musi odczytać kod z pamięci, przed uruchomieniem każdej aplikacji usuniemy pamięć podręczną jądra. Wymaga to dostępu do roota.
Uruchom nieoptymalizowaną aplikację:
adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat
Teraz zrób to samo w przypadku zoptymalizowanej aplikacji:
adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
Wyniki
Jeśli w sekcji App Summary przyjrzysz się wierszowi Kod, zobaczysz ogromną różnicę:
- Nieoptymalizowany
Code: ok. 30 000 KB (30 MB) - Zoptymalizowana
Code: ok. 2000 KB (2 MB)
R8 stwierdził, że 500 metod w tych klasach nigdy nie wykonywało żadnych przydatnych działań (metoda doSomething() wywołuje tylko method0(), a wyniki są ignorowane), więc usunął z końcowego pliku APK prawie cały wygenerowany sztucznie kod.
3. Sprawdzanie wpływu w Perfetto
Wpływ nadmiaru kodu jest wyraźnie widoczny podczas początkowej fazy wczytywania aplikacji. W szczególności poszukaj wycinka bindApplication w wątku głównym i zagnieżdżonych wycinków zaczynających się od madvising, które wskazują, że system przygotowuje się do wczytania plików z pliku APK i jego skompilowanego kodu (.odex).
Podczas interaktywnego uruchamiania „na zimno” system mmap() i madvise() kod oraz inne dane z tych plików, które są niezbędne do wczytania i uruchomienia aplikacji. Wartość po „size=” w wycinkach madvising wskazuje, ile danych należy wczytać. Wstępne pobieranie kodu aplikacji ma na celu przyspieszenie jej uruchamiania.
Z porównania wynika, że w przypadku aplikacji z nadmiarem kodu ilość kodu aplikacji, który trzeba było wczytać z pamięci masowej do pamięci RAM, była znacznie większa, co spowodowało dłuższe czasy wczytywania i wolniejsze uruchamianie aplikacji. Dodatkowo ślad uruchomienia rozbudowanej aplikacji pokazuje fragmenty ładowania dodatkowych plików DEX (classes2.dex, classes3.dex), które rozbudowana aplikacja musiała „rozlać”, ponieważ nie mieściły się w jednym pliku DEX.
Porównanie (uruchomienie „na zimno” na Pixelu 10a)
| Wskaźnik | Niezoptymalizowane (nadmiar kodu) | Zoptymalizowana (CodeBloatOptimized) |
|---|---|---|
base.odex madvise size |
~7,9 MB (2,0 ms) | ~16 KB (0,003 ms) |
base.apk madvise size |
~2,4 MB (2,4 ms) | ~4 KB (0,001 ms) |
classes2.dex madvise size |
~7,3 MB (8,6 ms) | Nie dotyczy |
classes3.dex madvise size |
~7,3 MB (8,0 ms) | Nie dotyczy |
Łączny czas trwania madvising |
~21 ms | ~0,004 ms |
Nieoptymalna wydajność ładowania aplikacji

Zoptymalizowana wydajność ładowania aplikacji

Wpływ nadmiaru kodu zależy od rozmiaru aplikacji, charakterystyki urządzenia użytkownika i obciążenia systemu.
PerfettoSQL do analizy wczytywania
Aby wyodrębnić te dane z śladów, możesz użyć tych zapytań.
1. Czas uruchamiania aplikacji
Pokazuje czas od uruchomienia działania aplikacji do momentu, gdy działanie narysuje pierwszą klatkę.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
Zobacz:Informacje o różnych stanach uruchamiania aplikacji
Czas uruchamiania aplikacji zależy od wielu czynników innych niż te, które opisujemy w tym przewodniku.
2. Wyodrębnianie rozmiarów madvising i czasów trwania
To zapytanie powiększa część madvising, którą widzieliśmy powyżej.
INCLUDE PERFETTO MODULE slices.with_context;
SELECT
name,
dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
AND name LIKE 'madvising %';
3. Zestawienie stanu wątku głównego (łączny czas trwania w poszczególnych stanach)
To zapytanie pokazuje, ile czasu główny wątek aplikacji spędził w różnych stanach.
SELECT
p.name AS process_name,
state,
sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
GROUP BY p.name, state;
Możesz zawęzić zapytanie, aby sprawdzać stany głównego wątku tylko podczas uruchamiania aplikacji.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT
p.name AS process_name,
ts.state,
-- Calculate only the duration that falls within the startup window
SUM(
MAX(0,
MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
)
) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
-- Only select thread states that overlap with the startup interval
AND ts.ts + ts.dur > s.ts
AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;
Może to ujawnić ciekawe problemy, na przykład:
- Długi czas oczekiwania na uruchomienie (R), ale nieuruchomiona: oznacza to, że uruchomienie aplikacji zostało opóźnione przez rywalizację o procesor, tzn. główny wątek aplikacji nie mógł się uruchomić, ponieważ inne wątki (prawdopodobnie z innych aplikacji) zajmowały procesory.
- Długi czas w stanie przerywanego uśpienia (D): zwykle wskazuje to na powolne operacje wejścia/wyjścia lub obciążenie pamięci, które opóźnia uruchomienie aplikacji.
- Długi czas spędzony na spaniu (S): oznacza to, że główny wątek czekał, aż inne wątki wykonają swoją pracę. Czasami wskazuje to na konflikt blokad na ścieżce uruchamiania aplikacji (tzn. wątek główny został zablokowany na zasobie wyłącznym, który był zajęty przez inny wątek w aplikacji).
4. Maksymalna pamięć obsługiwana przez plik (plik RSS)
Ten wskaźnik jest ściśle powiązany z ilością kodu i danych, które aplikacja wczytuje podczas uruchamiania. Bardziej „rozbudowana” aplikacja osiągnie tu wyższą wartość, co spowoduje obciążenie pamięci systemu. Taka presja może opóźnić uruchomienie aplikacji, ponieważ system będzie miał trudności z realizacją żądań przydzielenia pamięci lub będzie odwracać czas procesora od uruchamiania aplikacji i przekierowywać go na odzyskiwanie pamięci z innych procesów, aby zaspokoić natychmiastowe potrzeby uruchamianej aplikacji.
SELECT
p.name AS process_name,
max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.name = 'mem.rss.file'
GROUP BY p.name;
Tryby kompilacji ART i pamięć
Środowisko wykonawcze Androida (ART) może kompilować kod aplikacji w jednym z kilku różnych trybów, zwanych też filtrami kompilatora. Wybrany filtr kompilatora ma bezpośredni wpływ na wykorzystanie pamięci przez aplikację.
verify: ART przeprowadza tylko weryfikację kodu bajtowego. Nie jest przeprowadzana kompilacja AOT. Kod jest wykonywany przez interpreter lub kompilowany w czasie działania programu przez kompilator JIT.- Wpływ na pamięć: najmniejszy rozmiar na dysku. Wykorzystanie pamięci przez kod natywny jest przekazywane do
JIT Cache(anonimowa pamięć zmodyfikowana).
- Wpływ na pamięć: najmniejszy rozmiar na dysku. Wykorzystanie pamięci przez kod natywny jest przekazywane do
speed: ART wykonuje pełną kompilację AOT wszystkich metod.- Wpływ na pamięć: największy rozmiar
.odex. Maksymalizuje wykorzystanie pamięci zapisywanej w plikach (czystej).
- Wpływ na pamięć: największy rozmiar
speed-profile: ART kompiluje tylko metody oznaczone jako „gorące” w profilu JIT.- Wpływ na pamięć: zrównoważone podejście. Kompilacja AOT obejmuje tylko najważniejszy kod.
Najczęściej używanym filtrem jest speed-profile, który jest stosowany podczas instalowania aplikacji użytkownika. Jest to konfigurowane we właściwościach systemu pm.dexopt.install i pm.dexopt.bg-dexopt, a zwykle ustawiane w build/make/target/product/runtime_libart.mk.
Niektóre aplikacje systemowe będą korzystać z kompilacji speed, a także będą kompilowane w czasie kompilacji obrazu systemu. verify jest zwykle używane tylko w przypadkach związanych z programowaniem.
| Przypadek użycia | Typowy filtr kompilatora |
|---|---|
| Programowanie | verify |
| Obraz systemu | speed |
| Aplikacje użytkownika | speed-profile |
Praktyczne ćwiczenie: tryby kompilacji i pamięć
Możemy użyć aplikacji CodeBloat, aby sprawdzić, jak te filtry wpływają na pamięć. Aby odtworzyć te pomiary:
- Wymuś ponowną kompilację aplikacji w trybie docelowym.
- Wymuś zatrzymanie aplikacji i uruchom ją „na zimno”.
- Poczekaj, aż wątek w tle zakończy dotykanie klas (obserwuj logcat lub poczekaj 5 sekund).
- Uruchom
adb shell dumpsys meminfo com.android.codebloat.
Tryb: verify (bez AOT)
adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
W verify trybie podsumowanie aplikacji pokazuje: * Kod PSS: ok. 8000 KB * Dalvik
Inne (JIT): ok. 25 000 KB
Ponieważ żaden kod nie jest kompilowany AOT, środowisko wykonawcze musi kompilować JIT często używane metody do pamięci podręcznej JIT, która jest wyświetlana jako zmodyfikowana pamięć anonimowa (Dalvik
Other).
Tryb: speed (pełna kompilacja AOT)
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
W speed trybie wyniki zmieniają się drastycznie: * Code PSS: ok. 24 000 KB * Dalvik Other (JIT): ok. 5000 KB
Kod aplikacji jest teraz mapowany z pliku .odex jako czysta pamięć oparta na pliku. Zmniejsza to obciążenie pamięci podręcznej JIT i sprawia, że pamięć może zostać usunięta w przypadku obciążenia, zamiast „utknąć” jako zmodyfikowana pamięć RAM.
Tryb: speed-profile (wybiórcze AOT)
Nowoczesne aplikacje mogą zawierać profil podstawowy baseline.prof. ART używa tej funkcji do selektywnej kompilacji tylko tego kodu, który jest potrzebny do szybkiego i wydajnego uruchomienia.
W tym ćwiczeniu utworzymy profil podstawowy, aby wyświetlić listę klas uruchamiania aplikacji. W rzeczywistości kompilator może też otrzymywać profile ze źródeł zewnętrznych, np. ze sklepu z aplikacjami („profile w chmurze”), które mogą dostarczać profile JIT pochodzące z crowdsourcingu dla aplikacji niezależnie od tego, czy deweloper dołączył też wygenerowany przez siebie profil podstawowy.
Generowanie i używanie profili na urządzeniu
Aby zobaczyć wpływ speed-profile, możesz wygenerować własny profil na urządzeniu:
Resetuj i rozpocznij:
adb shell am force-stop com.android.codebloatInterakcja: uruchom aplikację i pozwól jej przejść sekwencję uruchamiania.
Zrzut profilu:
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)(Wymusza to zapisanie bieżącego profilu aplikacji na dysku).
Zainstaluj profil:
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profKompilacja:
adb shell cmd package compile -m speed-profile -f com.android.codebloat
Po ponownym uruchomieniu zobaczysz saldo: Code PSS będzie niższe niż
speed (np.ok. 16 000 KB), ponieważ skompilowane zostały tylko „gorące” metody uruchamiania, a pozostałe będą obsługiwane przez interpreter lub kompilator JIT tylko wtedy, gdy będą faktycznie używane.
Zobacz:
Szczegółowe omówienie skompilowanego kodu
Jeśli chcesz zobaczyć, jakie dokładnie instrukcje generuje ART, zapoznaj się z art/DISASSEMBLY_GUIDE.md.
Zawiera szczegółowe instrukcje dotyczące korzystania z:
oatdump: aby wyświetlić instrukcje ARM64 w istniejącym pliku.odex.dex2oat: symulacja kompilacji z użyciem flag debugowania w trybie szczegółowym.
Ćwiczenie: wstawianie kodu
Jedną z przyczyn nieoczekiwanego wzrostu rozmiaru skompilowanego kodu jest wstawianie metod. Kompilator może zdecydować się na skopiowanie treści małej, często wywoływanej metody bezpośrednio do jej wywołujących.
W naszej aplikacji CodeBloat metoda doSomething() w każdej wygenerowanej klasie po prostu wywołuje method0(). Gdy kod zostanie skompilowany w trybie speed, kompilator optymalizujący ART prawdopodobnie włączy funkcję method0() do funkcji doSomething().
Ćwiczenie: sprawdź to za pomocą oatdump na urządzeniu:
# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk
adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]
# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
--class-filter=com.android.codebloat.GeneratedClass0
W danych wyjściowych poszukaj metody doSomething. Jeśli została wstawiona, zobaczysz instrukcje wczytywania długiego ciągu znaków bezpośrednio w doSomething, a nie instrukcję bl kierowaną na method0.
Wizualizacja optymalizacji (CFG)
Aby dokładnie sprawdzić, kiedy kompilator zdecydował się na wstawienie funkcji w kodzie, możesz wygenerować graf przepływu sterowania (CFG). Pokazuje to stan kodu na każdym etapie potoku optymalizacji, z każdą transformacją w reprezentacji pośredniej kompilatora, aż do momentu, gdy kod zostanie obniżony do docelowego zestawu instrukcji (np. ARM64).
Uruchom polecenie
dex2oatz flagami zrzutu: użyj flagi--verbose-methods, aby ograniczyć dane wyjściowe do określonych metod. W przeciwnym razie plik.cfgw przypadku dużej aplikacji może osiągnąć rozmiar kilku gigabajtów.# Substitution of actual paths required: adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \ --oat-file=/data/local/tmp/dump.odex \ --compiler-filter=speed \ --dump-cfg=/data/local/tmp/codebloat.cfg \ --verbose-methods=doSomethingPrzeciągnij i wyświetl: przeciągnij plik
.cfgna stację roboczą i otwórz go za pomocą IR Hydra.Znajdź funkcję Inliner: w IR Hydra wczytaj artefakty kompilacji i wyszukaj
doSomething. Porównaj reprezentację przed i po przejściu Inliner. Wykres będzie się rozszerzać w miarę scalania instrukcji zmethod0z wywołującym.
Możesz też użyć narzędzia Opt Pipeline w Compiler Explorer (jak opisano w sekcji powyżej) i wpisać podobny kod, aby zobaczyć podobną transformację przeprowadzoną w ramach etapu Inliner.
Ćwiczenie: pola ulotne i bariery pamięci
W aplikacji MemoryLab pole mGarbageSink jest oznaczone jako volatile. Dzięki temu kompilator nie zoptymalizuje naszych alokacji pamięci na śmieci.
public volatile byte[] mGarbageSink;
W dezasemblacji ARM64 zobaczysz, że każdemu zapisowi w tym polu towarzyszy bariera pamięci (dmb ish) lub instrukcje Load-Acquire/Store-Release (ldar/stlr). Zapewnia to widoczność wątków, ale dodaje kilka dodatkowych instrukcji do każdego dostępu, co nieznacznie zwiększa rozmiar kodu w porównaniu ze zwykłym polem.
Ćwiczenie: znajdź w dezasemblacji dostępy do pól i powiązane z nimi bariery pamięci.
Ćwiczenie: sprawdzanie niejawnego zawieszenia
Jeśli rozłożysz pętlę, taką jak w generateAllocationChurn, na końcu jej treści zauważysz ciekawą instrukcję:
ldr x21, [x21]
Jest to niejawne sprawdzenie zawieszenia. ART używa tego, aby umożliwić bezpieczne wstrzymywanie wątków przez moduł odśmiecania pamięci. Rejestr x21 zwykle wskazuje na siebie.
Gdy GC musi zawiesić wątek, „zatruwa” to miejsce w pamięci. Gdy wątek następnym razem wykona ten kod ldr, spowoduje to błąd, który środowisko wykonawcze przechwyci i wykorzysta do przejścia wątku w stan zawieszenia.
Ten wzorzec powtarza się w każdej pętli i na początku każdej metody, co zwiększa całkowity rozmiar kodu aplikacji.
Ćwiczenie: znajdź wszystkie niejawne sprawdzenia zawieszenia w dezasemblacji metody i spróbuj powiązać je z oryginalnym kodem źródłowym.
← WebView | ↑ W górę | Threads →