Der von Ihnen geschriebene Code selbst ist eine Form der Speichernutzung. Jede Klasse, Methode und Stringkonstante in Ihrer Anwendung muss beim Ausführen in den RAM geladen werden. Je größer die Codebasis Ihrer Anwendung ist, desto mehr Arbeitsspeicher wird allein für die Ausführung benötigt.
Dateibasierter Arbeitsspeicher und Demand Paging
Android lädt ausführbaren Code aus Ihrem .apk (z. B. .oat- oder .so-Dateien) mit mmap. Das bedeutet, dass der Code dateibasiert ist.
Android verwendet Demand Paging. Wenn Ihre App gestartet wird, lädt der Kernel nicht sofort das gesamte APK in den RAM. Stattdessen wird die Datei nur in den virtuellen Adressraum des Prozesses eingebunden. Wenn Ihre App ausgeführt wird und die CPU zu einer neuen Funktion wechselt, wird ein „Seitenfehler“ ausgelöst. Der Kernel pausiert den Thread, liest diese bestimmte 4‑KB-Seite mit Code aus dem Speicher in den physischen RAM und setzt die Ausführung fort.

Das bedeutet, dass Code, den Sie verpacken, aber nie ausführen, keinen physischen Speicher für die Code-Seiten selbst verwendet. Unverwendete Bibliotheken erhöhen jedoch die Gesamt-APK-Größe und können den vom System für die internen Metadaten (z. B. DEX-Indizes und Klassendeskriptoren) verwendeten Speicher erheblich erhöhen. Diese Metadaten müssen gelesen werden, um überhaupt zu wissen, dass der Code vorhanden ist. Außerdem enthalten viele Bibliotheken statische Initialisierer oder werden von Frameworks für die Abhängigkeitsinjektion beim Start der App verwendet, sodass sie ohnehin in den RAM ausgelagert werden.
Entfernen von Seiten und Verlangsamungen
Da dateibasierter Arbeitsspeicher immer wieder aus dem Speicher gelesen werden kann, betrachtet der Kernel diese Seiten als „clean“. Wenn das System unter Arbeitsspeicherbelastung steht, entfernt (verwirft) der Kernel diese sauberen Code-Seiten aus dem RAM, um Platz für andere Dinge zu schaffen.
Wenn Ihre App diesen Code später noch einmal ausführen muss, wird ein CPU-Fehler ausgelöst und der Kernel muss die Seite noch einmal aus dem Speicher lesen. Je mehr Code Ihre App enthält, desto anfälliger ist sie für das Entfernen von Code. Wenn ein Nutzer nach der Verwendung anderer Apps zu Ihrer aufgeblähten App zurückkehrt, kommt es zu zufälligen Verzögerungen und Verlangsamungen, da die CPU ständig darauf wartet, dass Code aus dem Speicher zurückgeladen wird.
Kosten eines Seitenfehlers:Die Kosten variieren stark in Abhängigkeit von der Speichergeschwindigkeit des Geräts (UFS vs. eMMC) und dem Kernelstatus.Ein schwerwiegender Seitenfehler (Lesen von 4 KB aus dem Speicher) kann zwischen 0, 5 ms und 5 ms kosten. Wenn Ihr Startpfad 500 verschiedene Seiten mit nicht optimiertem Code umfasst, können Sie die Startzeit Ihrer App leicht um mehrere Hundert Millisekunden reine I/O-Latenz verlängern.
Code-Größe mit Compiler Explorer untersuchen
Mit dem Compiler Explorer können Sie sich ein Bild davon machen, wie Ihr Java- oder Kotlin-Code in nativen Maschinencode (und damit in Speicherbytes) übersetzt wird.
Android-Unterstützung ist direkt in Godbolt integriert. So können Sie sehen, wie verschiedene Teile der Android-Toolchain (D8, R8 und dex2oat) Ihren Quellcode transformieren.
Compiler Explorer mit Android verwenden
- Rufen Sie godbolt.org auf.
- Wählen Sie im Drop-down-Menü für die Sprache (links oben) Android Java oder Android Kotlin aus.
- Im Drop-down-Menü für den Compiler (rechts oben im Codebereich) können Sie zwischen verschiedenen Tools wählen:
d8: Zeigt den Dalvik-Bytecode (.dex) an. Dies ist die Darstellung, die Ihrem ursprünglichen Code am nächsten kommt und leichter zu lesen ist.r8: Hier sehen Sie, wie der R8-Optimierer Ihren Bytecode verkleinert und optimiert.dex2oat: Zeigt den finalen ARM64-Maschinencode an, der tatsächlich auf dem Gerät ausgeführt wird. Hier sehen Sie die tatsächlichen Auswirkungen auf den Arbeitsspeicher (4 Byte pro Anweisung).dex2oatkann auf verschiedene ISAs ausgerichtet sein, aber ARM64 ist die häufigste für Mobiltelefone.
- Quellcode<> Ausgabe hervorheben: Wenn Sie den Mauszeiger auf eine Codezeile bewegen, werden die entsprechenden Bytecode- oder Maschinencodeanweisungen hervorgehoben. So lässt sich die Auswirkung bestimmter Anweisungen leicht nachvollziehen.
- Optimierungs-Pipeline: In der Ansicht „Zerlegung“ können Sie auf Neu hinzufügen… klicken. > Opt-in-Pipeline. So können Sie die internen Schritte des Compilers sehen. Sie können sich ansehen, wie die interne Darstellung in jeder Phase transformiert wird (z.B. zwischen den Schritten „Inliner (vorher)“ und „Inliner (nachher)“), bevor sie in den endgültigen ARM64-Maschinencode umgewandelt wird.

Warum ist das für das Gedächtnis wichtig?
Jede Anweisung in der dex2oat-Ausgabe, die auf die ARM64-ISA ausgerichtet ist, belegt 4 Byte in der ausführbaren Datei (.odex oder .oat) Ihrer App.
Geben Sie Code ein, der verschiedene Sprachfunktionen nutzt, und sehen Sie sich die Ausgabe des Compilers an:
- Array-Zugriff im Vergleich zu Listeniteratoren:
- Eine einfache Array-Schleife über
int[]wird möglicherweise in etwa 10 Anweisungen (~40 Byte) kompiliert. - Eine foreach-Schleife für ein
Listverwendet implizit einIterator. Dies kann aufgrund der zusätzlichen Methodenaufrufe (hasNext(),next()) und der Zuweisung des Iterator-Objekts selbst zu 30–40 Anweisungen (~160 Byte) führen. - R8-Optimierung: Unter den richtigen Bedingungen (z. B. wenn
Listnachweislich einArrayListist) kann der R8-Optimierer eine foreach-Schleife wieder in eine einfache indexierte Schleife umwandeln. Dadurch wird der Iterator-Overhead eliminiert und sowohl die Codegröße als auch die Speichernutzung zur Laufzeit werden reduziert.
- Eine einfache Array-Schleife über
- Virtuelle Methodenaufrufe: Hierbei wird die Klasse des Objekts geladen, die Methode in der
vtablegesucht und dann verzweigt. Dazu sind in der Regel 4–5 Anweisungen (ca. 20 Byte) erforderlich. - Direkte/statische Aufrufe: Werden oft in eine einzelne
bl-Anweisung (Branch with Link) (4 Byte) übersetzt. - Kotlin-Lambdas: Können ganze anonyme Klassen und zusätzliche Bridge-Methoden generieren, wodurch für einen einfachen funktionalen Block Hunderte von Byte an Code und Metadaten-Aufwand hinzukommen.
Mit Compiler Explorer können Sie sehen, wie sich anspruchsvolle Sprachfunktionen wie Kotlin-Lambdas, Stream-APIs oder die intensive Verwendung von Generics auf die endgültige kompilierte Größe Ihrer Anwendung auswirken und wie Optimierer wie R8 in einigen Fällen die Kosten von Sprachabstraktionen ausgleichen können. Dieses Tool kann Ihnen helfen, fundierte Kompromisse beim Entwerfen und Implementieren einer App einzugehen.
Im Allgemeinen führt mehr Komplexität im Code Ihrer App zu einer höheren Arbeitsspeichernutzung. Umgekehrt führt einfacherer Code oder Code, der von R8 vereinfacht wird, zu einer kleineren Darstellung als CPU-Anweisungen und Bytes im Speicher und RAM.
Auswirkungen von Code mit meminfo und showmap messen
Mit den standardmäßigen Android-Arbeitsspeichertools können Sie sehen, wie viel Arbeitsspeicher der Code Ihrer App belegt.
dumpsys meminfo
Wenn Sie adb shell dumpsys meminfo <package> ausführen, bietet die Kategorie Code im Bereich App-Zusammenfassung einen allgemeinen Überblick über den speicherbezogenen Code:
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
Für eine detailliertere Ansicht verwenden Sie showmap. Es werden Regionen außerhalb bestimmter Dateien angezeigt, die dem Arbeitsspeicher zugeordnet sind.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
Sie sehen Einträge für den kompilierten Code Ihrer Anwendung:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
Nicht erreichbarer Code und R8
Da jede ausgeführte Methode Speicherplatz belegt, kann eine „aufgeblähte“ App mit unnötigen Initialisierungen oder nicht verwendeten Bibliotheken die Startleistung und die grundlegende Arbeitsspeichernutzung erheblich beeinträchtigen.
Deshalb sind Tools wie R8 (ProGuard) so wichtig. R8 analysiert den Bytecode Ihrer Anwendung und entfernt alle Klassen oder Methoden, die nie aufgerufen werden („Dead Code Stripping“).
Praxisübung: Die Kosten von Bloat
Um die Auswirkungen der Codegröße zu veranschaulichen, betrachten wir einen Test, bei dem zwei Builds einer Anwendung mit 300 generierten Klassen (jeweils mit 500 Methoden) verglichen werden:
- CodeBloat (nicht optimiert): Der Standard-Build ohne Optimierung, der alle generierten Klassen und eindeutigen Strings enthält.
- CodeBloatOptimized: Derselbe Quellcode, aber mit aktivierter R8-Verkleinerung kompiliert.
1. AOT-Kompilierung (Ahead-of-Time)
Um die Auswirkungen des dateibasierten Speichers zu maximieren, verwenden wir das Tool cmd package compile, um die Apps vorab (AOT) in .oat-Dateien zu kompilieren.
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
Bitte beachten Sie, dass es sich hierbei um ein synthetisches Beispiel handelt. In der Regel wird für Apps der Kompilierungsmodus speed-profile verwendet (siehe unten).
2. Einführen und vergleichen
Um einen wirklich kalten Start zu simulieren, bei dem das System Code aus dem Speicher lesen muss, leeren wir den Seitencache des Kernels, bevor wir jede App starten. Dazu ist Root-Zugriff erforderlich.
Starten Sie die nicht optimierte App:
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
Wiederholen Sie den Vorgang für die optimierte App:
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
Die Ergebnisse
Wenn Sie sich im Abschnitt App Summary die Zeile Code ansehen, werden Sie einen großen Unterschied feststellen:
- Nicht optimiert
Code: ca. 30.000 KB (30 MB) - Optimiert
Code: ca. 2.000 KB (2 MB)
Da R8 festgestellt hat, dass die 500 Methoden in diesen Klassen nie etwas Nützliches bewirkt haben (die doSomething()-Methode ruft nur method0() auf und die Ergebnisse werden ignoriert), wurde fast der gesamte künstlich generierte Code aus dem endgültigen APK entfernt.
3. Auswirkungen in Perfetto ansehen
Die Auswirkungen von Code-Bloat sind in der ersten Ladephase der Anwendung deutlich sichtbar. Suchen Sie insbesondere nach dem Segment bindApplication im Hauptthread und nach verschachtelten Segmenten, die mit madvising beginnen. Diese geben an, dass das System Dateien aus dem APK und dem kompilierten Code (.odex) lädt.
Bei einem interaktiven Kaltstart werden mmap()- und madvise()-Code sowie
andere Daten aus diesen Dateien, die zum Laden und Ausführen der App erforderlich sind, . Der Wert nach „size=“ in den madvising-Slices gibt an, wie viele Daten geladen werden müssen. Das Vorabrufen des App-Codes soll den Start der App beschleunigen.
Aus dem Vergleich geht hervor, dass die Menge an App-Code, die vom Speicher in den RAM geladen werden musste, bei der aufgeblähten App viel größer war. Dies führte zu längeren Zeiträumen, die zu einem langsameren App-Start beitrugen. Außerdem enthält der Trace des Starts der aufgeblähten App Abschnitte zum Laden sekundärer DEX-Dateien (classes2.dex, classes3.dex), in die die aufgeblähte App „ausgelagert“ werden musste, da sie nicht in eine DEX-Datei passte.
Im Vergleich (Kaltstart auf dem Pixel 10a)
| Messwert | Nicht optimiert (Code-Bloat) | Optimiert (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) | – |
classes3.dex madvise size |
~7,3 MB (8,0 ms) | – |
Gesamtdauer von madvising |
~21 ms | ~0,004 ms |
Nicht optimierte App-Ladeleistung

Optimierte App-Ladeleistung

Die Auswirkungen von Code-Bloat variieren je nach Größe der App, den Eigenschaften des Geräts des Nutzers und der Systemlast.
PerfettoSQL für die Ladeanalyse
Mit den folgenden Abfragen können Sie diese Messwerte aus Ihren Traces extrahieren.
1. App-Startdauer
Hier wird die Zeitspanne zwischen dem Start einer Aktivität einer App und dem Rendern des ersten Frames der Aktivität angezeigt.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
Weitere Informationen finden Sie unter Verschiedene App-Startzustände.
Die Startdauer einer App hängt von vielen Faktoren ab, die in diesem Leitfaden nicht behandelt werden.
2. madvising-Größen und ‑Dauern extrahieren
In dieser Abfrage wird der oben gezeigte Teil madvising genauer betrachtet.
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. Aufschlüsselung des Hauptthread-Status (Gesamtdauer pro Status)
Diese Abfrage zeigt, wie viel Zeit der Hauptthread der App in verschiedenen Status verbracht hat.
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;
Sie können die Abfrage so anpassen, dass nur der Status des Hauptthreads während der Startdauer der App berücksichtigt wird.
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;
Dabei können einige interessante Probleme auftreten, z. B.:
- Lange Zeit im Status „Runnable“ (R), aber nicht „Running“: Dies deutet darauf hin, dass der Start der App durch CPU-Konflikte verzögert wurde. Der Hauptthread der App konnte nicht ausgeführt werden, weil andere Threads (möglicherweise von anderen Apps) die CPUs belegten.
- Lange Zeit im unterbrechbaren Schlafzustand (D): Dies deutet in der Regel auf langsame E/A-Vorgänge oder Speicherdruck hin, der den Start der App verzögert.
- Lange Schlafzeit (S): Das bedeutet, dass der Hauptthread darauf gewartet hat, dass andere Threads ihre Arbeit erledigen. Manchmal deutet dies auf Konflikte bei der Sperrung im Startpfad der App hin. Das bedeutet, dass der Hauptthread durch eine exklusive Ressource blockiert wurde, die von einem anderen Thread in der App belegt war.
4. Maximaler dateibasierter Arbeitsspeicher (RSS-Datei)
Dieser Messwert korreliert gut damit, wie viel Code und Daten die App beim Start lädt. Eine „aufgeblähte“ App erreicht hier eine höhere Zahl, was zu einer hohen Speicherauslastung des Systems führt. Dieser Druck kann wiederum den Start der App verzögern, da das System Schwierigkeiten hat, Zuweisungsanforderungen zu erfüllen, oder CPU-Zeit vom Starten der App abzweigt und stattdessen Speicher von anderen Prozessen zurückfordert, um die unmittelbaren Anforderungen der startenden App zu erfüllen.
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;
ART-Kompilierungsmodi und Arbeitsspeicher
Die Android Runtime (ART) kann Ihren Anwendungscode in einem von mehreren verschiedenen Modi kompilieren, die auch als Compilerfilter bezeichnet werden. Der ausgewählte Compilerfilter hat einen direkten Einfluss auf den Speicherbedarf Ihrer App.
verify: ART führt nur eine Bytecode-Überprüfung durch. Es wird keine AOT-Kompilierung durchgeführt. Code wird über den Interpreter ausgeführt oder zur Laufzeit vom JIT-Compiler kompiliert.- Speicherauswirkungen: Kleinste Größe auf der Festplatte. Die Arbeitsspeichernutzung von nativem Code wird in
JIT Cache(anonymer Dirty-Speicher) übertragen.
- Speicherauswirkungen: Kleinste Größe auf der Festplatte. Die Arbeitsspeichernutzung von nativem Code wird in
speed: ART führt eine vollständige AOT-Kompilierung aller Methoden durch.- Arbeitsspeicherauswirkungen: Größte
.odex-Größe. Maximiert die Nutzung von dateibasiertem (sauberem) Arbeitsspeicher.
- Arbeitsspeicherauswirkungen: Größte
speed-profile: ART kompiliert nur Methoden, die in einem JIT-Profil als „hot“ markiert wurden.- Arbeitsspeicherauswirkungen: Ausgewogener Ansatz. Nur der wichtigste Code wird AOT-kompiliert.
Der häufigste Filter ist speed-profile, der bei der Installation von Nutzer-Apps verwendet wird. Dies wird in den Systemattributen pm.dexopt.install und pm.dexopt.bg-dexopt konfiguriert und in der Regel in build/make/target/product/runtime_libart.mk festgelegt.
Einige System-Apps verwenden die speed-Kompilierung und werden auch beim Erstellen des System-Images kompiliert. verify wird in der Regel nur in Entwicklungsanwendungsfällen verwendet.
| Anwendungsfall | Typischer Compilerfilter |
|---|---|
| Entwicklung | verify |
| System image | speed |
| Nutzer-Apps | speed-profile |
Praktische Übung: Kompilierungsmodi und Speicher
Mit der CodeBloat App können wir sehen, wie sich diese Filter auf den Speicher auswirken. So reproduzierst du diese Messungen:
- Erzwingen Sie die Neukompilierung der App in den Zielmodus.
- Erzwingen Sie das Beenden der App und starten Sie sie neu.
- Warten Sie, bis der Hintergrundthread die Bearbeitung der Klassen abgeschlossen hat (sehen Sie sich Logcat an oder warten Sie 5 Sekunden).
- Führen Sie
adb shell dumpsys meminfo com.android.codebloataus.
Modus: verify (kein 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
Im verify-Modus wird in der App-Zusammenfassung Folgendes angezeigt: * Code PSS: ca. 8.000 KB * Dalvik Other (JIT): ca. 25.000 KB
Da kein Code AOT-kompiliert wird, muss die Laufzeit Hot-Methoden JIT-kompilieren und in den JIT-Cache schreiben. Das wird als dirty anonymous memory (Dalvik
Other) angezeigt.
Modus: speed (vollständige 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
Im speed-Modus verschieben sich die Ergebnisse erheblich: * Code PSS: ~24.000 KB *
Dalvik Other (JIT): ~5.000 KB
Der Code der Anwendung wird jetzt aus der Datei .odex als clean file-backed memory (sauberer dateigestützter Speicher) zugeordnet. Dadurch wird der JIT-Cache entlastet und der Speicher kann bei Bedarf geleert werden, anstatt als „dirty“ (nicht bereinigt) im RAM zu verbleiben.
Modus: speed-profile (selektive AOT)
Moderne Apps können ein baseline.prof-Baseline-Profil enthalten. ART verwendet diese Informationen, um nur den Code selektiv zu kompilieren, der für einen schnellen, speichereffizienten Start benötigt wird.
In dieser Übung erstellen wir ein Baseline-Profil, um die Startklassen der App aufzulisten. In der Realität kann der Compiler jedoch auch Profile aus externen Quellen wie dem App-Store („Cloud-Profile“) erhalten, die Crowdsourcing-JIT-Profile für Apps bereitstellen, unabhängig davon, ob der Entwickler auch ein von ihm generiertes Baseline-Profil gebündelt hat.
Geräteprofile erstellen und verwenden
So sehen Sie die Auswirkungen von speed-profile:
Zurücksetzen und starten:
adb shell am force-stop com.android.codebloatInteragieren: Starten Sie die App und lassen Sie sie ihre Startsequenz durchlaufen.
Dump-Profil:
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)Dadurch wird die App gezwungen, ihr aktuelles Profil auf die Festplatte zu schreiben.
Profil installieren:
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profKompilieren:
adb shell cmd package compile -m speed-profile -f com.android.codebloat
Wenn Sie die App noch einmal starten, sehen Sie ein Gleichgewicht: Code PSS ist niedriger als speed (z. B. ca. 16.000 KB), da nur die „heißen“ Startmethoden kompiliert wurden. Der Rest wird nur dann vom Interpreter oder JIT verarbeitet, wenn er tatsächlich verwendet wird.
Weitere Informationen:
Kompilierten Code im Detail ansehen
Wenn Sie genau sehen möchten, welche Anweisungen von ART generiert werden, lesen Sie art/DISASSEMBLY_GUIDE.md.
Sie enthält detaillierte Anleitungen zur Verwendung von:
oatdump: Um ARM64-Anweisungen in einer vorhandenen.odex-Datei zu sehen.dex2oat: Zum Simulieren der Kompilierung mit ausführlichen Debug-Flags.
Übung: Code-Inlining
Ein Grund dafür, dass kompilierter Code unerwartet an Größe zunehmen kann, ist Methoden-Inlining. Der Compiler kann den Textkörper einer kleinen, häufig aufgerufenen Methode direkt in die Aufrufer kopieren.
In unserer CodeBloat-App ruft die Methode doSomething() in jeder generierten Klasse einfach method0() auf. Wenn der Code im speed-Modus kompiliert wird, wird der Optimierungscompiler von ART method0() wahrscheinlich in doSomething() inline einfügen.
Übung:So prüfen Sie das mit oatdump auf Ihrem Gerät:
# 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
Suchen Sie in der Ausgabe nach der Methode doSomething. Wenn sie inline eingefügt wurde, sehen Sie die Anleitung zum Laden der langen Stringkonstante direkt in doSomething statt einer bl-Anweisung für method0.
Optimierung visualisieren (CFG)
Um genau zu sehen, wann der Compiler die Methode inline ausgeführt hat, können Sie ein Kontrollflussdiagramm (Control Flow Graph, CFG) erstellen. Hier wird der Status des Codes in jeder Phase der Optimierungspipeline angezeigt, mit jeder Transformation über die Zwischenrepräsentation (Intermediate Representation, IR) des Compilers, bis der Code auf die Ziel-ISA (z.B. ARM64) reduziert wird.
dex2oatmit Dump-Flags ausführen: Verwenden Sie das Flag--verbose-methods, um die Ausgabe auf bestimmte Methoden zu beschränken. Andernfalls kann die.cfg-Datei für eine große App mehrere Gigabyte groß werden.# 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=doSomethingAbrufen und ansehen: Rufen Sie die Datei
.cfgauf Ihre Workstation ab und öffnen Sie sie mit IR Hydra.Inliner suchen: Laden Sie in IR Hydra die Kompilierungsartefakte und suchen Sie nach
doSomething. Vergleichen Sie die Darstellung vor und nach dem Inliner-Durchlauf. Die Grafik wird erweitert, wenn die Anweisungen ausmethod0in den Anrufer eingefügt werden.
Alternativ können Sie das Tool Opt Pipeline in Compiler Explorer verwenden (wie oben beschrieben) und ähnlichen Code eingeben, um eine ähnliche Transformation im Inliner-Pass zu sehen.
Übung: Flüchtige Felder und Memory Barriers
In der MemoryLab App ist das Feld mGarbageSink als volatile markiert. So wird sichergestellt, dass der Compiler unsere Garbage-Zuweisungen nicht optimiert.
public volatile byte[] mGarbageSink;
In der ARM64-Disassemblierung sehen Sie, dass jeder Speichervorgang für dieses Feld von einer Memory Barrier (dmb ish) begleitet wird oder Load-Acquire-/Store-Release-Anweisungen (ldar/stlr) verwendet werden. Dadurch wird die Sichtbarkeit von Threads gewährleistet, aber jeder Zugriff erfordert einige zusätzliche Anweisungen, was die Codegröße im Vergleich zu einem regulären Feld leicht erhöht.
Übung:Suchen Sie in der Disassemblierung nach den Feldzugriffen und den zugehörigen Memory Barriers.
Übung: Implizite Sperrungsprüfungen
Wenn Sie eine Schleife wie die in generateAllocationChurn zerlegen, werden Sie am Ende des Schleifenkörpers eine interessante Anweisung bemerken:
ldr x21, [x21]
Dies ist eine implizite Sperrprüfung. ART verwendet dies, damit der Garbage Collector Threads sicher pausieren kann. Das Register x21 verweist normalerweise auf sich selbst.
Wenn die Garbage Collection den Thread anhalten muss, wird dieser Speicherort „vergiftet“. Wenn der Thread das nächste Mal ldr ausführt, wird ein Fehler ausgelöst, den die Laufzeit abfängt und verwendet, um den Thread in den Status „Ausgesetzt“ zu versetzen.
Dieses Muster wird in jeder Schleife und am Anfang jeder Methode wiederholt, was zur Gesamtgröße des Codes Ihrer Anwendung beiträgt.
Übung:Suchen Sie im Disassembly der Methode nach allen impliziten Suspend-Prüfungen und versuchen Sie, sie mit dem ursprünglichen Quellcode in Beziehung zu setzen.
← WebView | ↑ Nach oben | Threads →