Il codice dell'app è memoria

Il codice che scrivi è una forma di utilizzo della memoria. Ogni classe, metodo e costante stringa nella tua applicazione deve essere caricata nella RAM durante l'esecuzione. Più grande è la base di codice della tua applicazione, più memoria consumerà solo per esistere.

Memoria basata su file e paging su richiesta

Android carica codice eseguibile dal tuo .apk (ad esempio file .oat o .so) utilizzando mmap. Ciò significa che il codice è basato su file.

Fondamentalmente, Android utilizza il paging su richiesta. Quando l'app viene avviata, il kernel non carica immediatamente l'intero APK nella RAM. ma esegue solo la mappatura del file nello spazio di indirizzi virtuale del processo. Quando l'app viene eseguita e la CPU passa a una nuova funzione, viene attivato un "errore di pagina". Il kernel mette in pausa il thread, legge quella specifica pagina di codice di 4 KB dallo spazio di archiviazione nella RAM fisica e riprende l'esecuzione.

Un diagramma che illustra la paginazione su richiesta, che mostra le pagine virtuali mappate alle
pagine RAM fisiche solo quando vengono utilizzate

Ciò significa che il codice che pacchettizzi ma che non esegui non utilizza la memoria fisica per le pagine di codice. Tuttavia, le librerie inutilizzate aumentano comunque le dimensioni complessive dell'APK e possono aumentare significativamente la memoria utilizzata dai metadati interni del sistema (come gli indici DEX e i descrittori di classe), che devono essere letti anche solo per sapere che il codice esiste. Inoltre, molte librerie contengono inizializzatori statici o vengono toccate dai framework di inserimento delle dipendenze durante l'avvio dell'app, causando comunque il loro paging nella RAM.

Eviction e rallentamenti delle pagine

Poiché la memoria basata su file può sempre essere riletta dallo spazio di archiviazione, il kernel considera queste pagine "pulite". Quando il sistema subisce una pressione della memoria, il kernel espelle (elimina) queste pagine di codice pulito dalla RAM per fare spazio ad altre operazioni.

Se in un secondo momento l'app deve eseguire di nuovo il codice, la CPU genererà un errore e il kernel dovrà rileggere la pagina dallo spazio di archiviazione. Più codice contiene la tua app, più è vulnerabile all'eliminazione del codice. Quando un utente torna alla tua app gonfia dopo aver utilizzato altre app, si verificano rallentamenti e scatti casuali perché la CPU si blocca costantemente in attesa che il codice venga riportato dalla memoria.

Il costo di un errore di pagina:sebbene vari notevolmente in base alla velocità di archiviazione del dispositivo (UFS vs. eMMC) e allo stato del kernel, un errore di pagina grave (lettura di 4 KB dall'archiviazione) può costare da 0,5 ms a 5 ms. Se il percorso di avvio tocca 500 pagine diverse di codice non ottimizzato, potresti facilmente introdurre diverse centinaia di millisecondi di latenza I/O pura nel tempo di avvio dell'app.

Esplorare le dimensioni del codice con Compiler Explorer

Per farti un'idea di come il tuo codice Java o Kotlin si traduce in codice macchina nativo (e quindi in byte di memoria), puoi utilizzare Compiler Explorer.

Il supporto di Android è integrato direttamente in Godbolt. Consente di vedere come le diverse parti della toolchain Android (D8, R8 e dex2oat) trasformano il codice sorgente.

Come utilizzare Compiler Explorer con Android

  1. Vai alla pagina godbolt.org.
  2. Seleziona Android Java o Android Kotlin dal menu a discesa della lingua (in alto a sinistra).
  3. Nel menu a discesa del compilatore (in alto a destra nel riquadro del codice), puoi scegliere tra diversi strumenti:
    • d8: mostra il bytecode Dalvik (.dex). Questa è la rappresentazione più vicina al codice originale ed è più facile da leggere.
    • r8: mostra come lo strumento di ottimizzazione R8 riduce e ottimizza il bytecode.
    • dex2oat: mostra il codice macchina ARM64 finale che viene effettivamente eseguito sul dispositivo. Qui puoi vedere l'impatto reale sulla memoria (4 byte per istruzione). dex2oat può scegliere come target diverse ISA, ma ARM64 è la più comune per i cellulari.
  4. Evidenziazione sorgente<>output: se passi il mouse sopra una riga di codice, verranno evidenziate le istruzioni corrispondenti di bytecode o codice macchina, in modo da poter tracciare facilmente l'impatto di istruzioni specifiche.
  5. Pipeline di ottimizzazione: nella visualizzazione di disassemblaggio, puoi fare clic su Aggiungi nuovo… -> Opt Pipeline. In questo modo puoi visualizzare i passaggi interni eseguiti dal compilatore. Puoi esaminare come viene trasformata la rappresentazione interna (IR) in ogni fase (ad esempio, tra i passaggi "Inliner (prima)" e "Inliner (dopo)") prima che venga convertita nel codice macchina ARM64 finale.

Uno screenshot della UI di Compiler Explorer che mostra un programma di esempio e il relativo
disassemblaggio dell'output dex2oat e la pipeline di ottimizzazione con il passaggio di inlining
mostrato

Perché è importante per la memoria

Ogni istruzione visualizzata nell'output dex2oat che ha come target l'ISA ARM64 occupa 4 byte nel file eseguibile (.odex o .oat) della tua app.

Prova a inserire un codice che utilizzi funzionalità di linguaggio diverse e studia l'output del compilatore:

  • Accesso agli array e iteratori di elenchi:
    • Un semplice ciclo di array su int[] potrebbe essere compilato in circa 10 istruzioni (circa 40 byte).
    • Un ciclo foreach su un List utilizza implicitamente un Iterator. Ciò può comportare 30-40 istruzioni (~160 byte) a causa delle chiamate di metodi aggiuntive (hasNext(), next()) e dell'allocazione dell'oggetto iteratore stesso.
    • Ottimizzazione R8: in determinate condizioni (ad es. quando List si dimostra essere un ArrayList), l'ottimizzatore R8 può trasformare un ciclo foreach in un semplice ciclo indicizzato, eliminando l'overhead dell'iteratore e riducendo sia le dimensioni del codice che la saturazione della memoria di runtime.
  • Chiamate di metodi virtuali: comportano il caricamento della classe dell'oggetto, la ricerca del metodo in vtable e poi la ramificazione. In genere, questa operazione richiede 4-5 istruzioni (~20 byte).
  • Chiamate dirette/statiche: spesso si traducono in una singola istruzione bl (Branch with Link) (4 byte).
  • Lambda Kotlin: possono generare intere classi anonime e metodi bridge aggiuntivi, aggiungendo centinaia di byte di codice e overhead di metadati per un semplice blocco funzionale.

Utilizzando Compiler Explorer, puoi vedere come le funzionalità avanzate del linguaggio (come le espressioni lambda Kotlin, le API di stream o l'uso intensivo di generici) influiscono sulle dimensioni finali compilate della tua applicazione e come gli ottimizzatori come R8 possono contrastare il costo delle astrazioni del linguaggio in alcuni casi. Questo strumento può aiutarti a fare scelte informate nella progettazione e nell'implementazione di un'app.

In generale, una maggiore complessità nel codice della tua app comporta un maggiore utilizzo della memoria. Al contrario, un codice più semplice o semplificato da R8 genera una rappresentazione più piccola come istruzioni della CPU e byte in memoria e RAM.

Misurare l'impatto del codice con meminfo e showmap

Puoi utilizzare gli strumenti di memoria Android standard per vedere quanta memoria consuma il codice della tua app.

dumpsys meminfo

Quando esegui adb shell dumpsys meminfo <package>, la categoria Codice nella sezione Riepilogo app fornisce una panoramica generale della memoria correlata al codice:

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     3244
         Native Heap:     5412
                Code:    24512  # <--- Sum of .so, .dex, .oat, .art, etc.

showmap

Per una visualizzazione più granulare, utilizza showmap. Mostra le regioni al di fuori di file specifici che vengono mappati alla memoria.

adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"

Vedrai le voci relative al codice compilato della tua applicazione:

   size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
  12288     8192     8192     8192        0        0        0        0        0 /data/app/.../base.odex

Codice inutilizzato e R8

Poiché ogni metodo eseguito occupa memoria, un'app "gonfia" con inizializzazioni non necessarie o librerie inutilizzate può influire gravemente sulle prestazioni di avvio e sulla memoria utilizzata di base.

Per questo motivo, strumenti come R8 (ProGuard) sono fondamentali. R8 analizza il bytecode dell'applicazione e rimuove tutte le classi o i metodi che non vengono mai chiamati ("rimozione del codice inutilizzato").

Esercizio pratico: il costo del bloat

Per dimostrare l'impatto delle dimensioni del codice, considera un esperimento che confronta due build di un'applicazione contenente 300 classi generate (ciascuna con 500 metodi):

  • CodeBloat (non ottimizzato): la build standard non ottimizzata contenente tutte le classi generate e le stringhe uniche.
  • CodeBloatOptimized: lo stesso codice sorgente, ma compilato con la riduzione R8 attivata.

1. Compilazione ahead-of-time (AOT)

Per massimizzare l'impatto della memoria basata su file, utilizzeremo lo strumento cmd package compile per compilare in anticipo (AOT) le app in file .oat.

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized

Tieni presente che questo è un esempio sintetico. In genere, le app utilizzano la modalità di compilazione speed-profile (vedi più avanti).

2. Avvia e confronta

Per vedere un avvio veramente freddo in cui il sistema deve leggere il codice dalla memoria, elimineremo la cache delle pagine del kernel prima di avviare ogni app. Questa operazione richiede l'accesso root.

Avvia l'app non ottimizzata:

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

Ora esegui la stessa operazione per l'app ottimizzata:

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
I risultati

Se esamini la riga Codice nella sezione App Summary, noterai una differenza enorme:

  • Non ottimizzata Code: circa 30.000 KB (30 MB)
  • Ottimizzata Code: circa 2000 KB (2 MB)

Poiché R8 ha stabilito che i 500 metodi all'interno di queste classi non stavano facendo nulla di utile (il metodo doSomething() chiama solo method0() e i risultati vengono ignorati), ha rimosso quasi tutto il codice generato artificialmente dall'APK finale.

3. Visualizzare l'impatto in Perfetto

L'impatto del codice bloat è chiaramente visibile durante la fase di caricamento iniziale dell'applicazione. In particolare, cerca la sezione bindApplication nel thread principale e le sezioni nidificate che iniziano con madvising, che indicano che il sistema si sta preparando a caricare i file dall'APK e il relativo codice compilato (.odex).

In un avvio a freddo interattivo, il sistema mmap() e madvise() il codice e altri dati di questi file necessari per il caricamento e l'esecuzione dell'app. Il valore dopo "size=" nelle sezioni madvising indica la quantità di dati da caricare. Il precaricamento del codice dell'app viene eseguito per accelerare l'avvio dell'app.

Dal confronto possiamo notare che la quantità di codice dell'app che doveva essere caricata dallo spazio di archiviazione alla RAM era molto maggiore nel caso dell'app bloated, con conseguente durate più lunghe che hanno contribuito a un avvio più lento dell'app. Inoltre, la traccia dell'avvio dell'app bloated mostra segmenti per il caricamento dei file DEX secondari (classes2.dex, classes3.dex) che l'app bloated è stata costretta a "riversare" in perché non rientravano in un unico file DEX.

A confronto (avvio a freddo su Pixel 10a)
Metrica Non ottimizzato (CodeBloat) Ottimizzata (CodeBloatOptimized)
base.odex madvise size ~7,9 MB (2 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) N/D
classes3.dex madvise size ~7,3 MB (8 ms) N/D
Durata totale madvising ~21 ms ~0,004 ms
Prestazioni di caricamento delle app non ottimizzate

Screenshot dell'UI di Perfetto che mostra il processo com.android.codebloat con
le sezioni madvising per i file DEX principali e secondari

Prestazioni di caricamento delle app ottimizzate

Screenshot della UI di Perfetto che mostra il processo com.android.codebloat.optimized
con una singola e piccola sezione
madvising

L'impatto del codice ridondante varia in base alle dimensioni dell'app, alle caratteristiche del dispositivo dell'utente e al carico del sistema.

PerfettoSQL per l'analisi del caricamento

Puoi utilizzare le seguenti query per estrarre queste metriche dalle tue tracce.

1. Durata dell'avvio dell'app

Mostra il tempo che intercorre tra l'avvio di un'attività dell'app e il disegno del primo frame dell'attività.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';

Vedi: Informazioni sui diversi stati di avvio dell'app

La durata di avvio di un'app è sensibile a molti fattori diversi da quelli trattati in questa guida.

2. Estrarre le dimensioni e le durate di madvising

Questa query si concentra sulla parte madvising che abbiamo visto sopra.

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. Suddivisione dello stato del thread principale (durata totale per stato)

Questa query mostra quanto tempo ha trascorso il thread principale dell'app in diversi stati.

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;

Puoi perfezionare la query per esaminare solo gli stati del thread principale durante la durata dell'avvio dell'app.

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;

In questo modo, possono emergere alcuni problemi interessanti, ad esempio:

  • Runnable (R) con tempo di esecuzione elevato, ma non in esecuzione: indica che l'avvio dell'app è stato ritardato a causa della contesa della CPU, ovvero il thread principale dell'app non è stato eseguito perché altri thread (probabilmente di altre app) occupavano le CPU.
  • Tempo elevato trascorso in modalità Sospensione interrompibile (D): in genere indica I/O lento o utilizzo intensivo della memoria che blocca l'avvio dell'app.
  • Tempo elevato trascorso in stato di sospensione (S): significa che il thread principale era in attesa che altri thread svolgessero il lavoro. A volte questo indica una contesa di blocchi nel percorso di avvio dell'app (ovvero il thread principale è stato bloccato su una risorsa esclusiva occupata da un altro thread nell'app).
4. Memoria supportata da file massima (file RSS)

Questa metrica è strettamente correlata alla quantità di codice e dati caricati dall'app all'avvio. Un'app più "gonfia" raggiungerà un numero più alto qui, causando una pressione sulla memoria del sistema. Questa pressione può a sua volta ritardare l'avvio dell'app, in quanto il sistema fatica a soddisfare le richieste di allocazione o devia il tempo della CPU dall'avvio dell'app al recupero di memoria da altri processi per soddisfare le esigenze immediate dell'app di avvio.

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;

Modalità di compilazione ART e memoria

Android Runtime (ART) può compilare il codice dell'applicazione in una delle diverse modalità, note anche come filtri del compilatore. Il filtro del compilatore selezionato ha un impatto diretto sul footprint della memoria dell'app.

  • verify: ART esegue solo la verifica del bytecode. Non viene eseguita la compilazione AOT. Il codice viene eseguito tramite l'interprete o compilato in fase di runtime dal compilatore JIT.
    • Impatto sulla memoria: dimensioni più ridotte su disco. L'utilizzo della memoria del codice nativo viene inserito in JIT Cache (memoria sporca anonima).
  • speed: ART esegue la compilazione AOT completa di tutti i metodi.
    • Impatto sulla memoria: dimensioni .odex maggiori. Massimizza l'utilizzo della memoria basata su file (pulita).
  • speed-profile: ART compila solo i metodi contrassegnati come "hot" in un profilo JIT.
    • Impatto sulla memoria: approccio bilanciato. Viene compilato AOT solo il codice più critico.

Il filtro più comune è speed-profile, che viene utilizzato durante l'installazione delle app utente. Questa impostazione è configurata nelle proprietà di sistema pm.dexopt.install e pm.dexopt.bg-dexopt e in genere viene impostata in build/make/target/product/runtime_libart.mk.

Alcune app di sistema utilizzeranno la compilazione speed e verranno compilate anche al tempo di compilazione dell'immagine di sistema. verify viene in genere utilizzato solo nei casi d'uso di sviluppo.

Caso d'uso Filtro del compilatore tipico
Sviluppo verify
Immagine di sistema speed
App utente speed-profile

Esercizio pratico: modalità di compilazione e memoria

Possiamo utilizzare l'app CodeBloat per vedere in che modo questi filtri influiscono sulla memoria. Per riprodurre queste misurazioni:

  1. Forza la ricompilazione dell'app nella modalità di destinazione.
  2. Forza l'interruzione e l'avvio a freddo dell'app.
  3. Attendi il completamento dell'accesso alle classi da parte del thread in background (guarda logcat o attendi 5 secondi).
  4. Esegui adb shell dumpsys meminfo com.android.codebloat.

Modalità: verify (nessun 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

In modalità verify, il riepilogo dell'app mostra: * Code PSS: circa 8000 KB * Dalvik Other (JIT): circa 25.000 KB

Poiché non viene compilato alcun codice AOT, il runtime deve compilare JIT i metodi hot nella cache JIT, che viene visualizzata come memoria anonima modificata (Dalvik Other).

Modalità: speed (AOT completo)

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

In modalità speed, i risultati cambiano drasticamente: * Code PSS: circa 24.000 kB * Dalvik Other (JIT): circa 5000 kB

Il codice dell'applicazione ora viene mappato dal file .odex come memoria pulita supportata da file. Ciò riduce la pressione sulla cache JIT e rende la memoria idonea all'eliminazione in caso di pressione, anziché rimanere "bloccata" come RAM modificata.

Modalità: speed-profile (AOT selettivo)

Le app moderne possono includere un profilo di base baseline.prof. ART lo utilizza per compilare selettivamente solo il codice necessario per un avvio rapido ed efficiente in termini di memoria.

In questo esercizio creeremo un profilo di base per elencare le classi di avvio dell'app. Tuttavia, in realtà il compilatore potrebbe ricevere profili anche da fonti esterne come lo store di applicazioni ("profili cloud"), che possono fornire profili JIT crowdsourcing per le app indipendentemente dal fatto che lo sviluppatore abbia anche incluso un profilo di base generato.

Generare e utilizzare profili sul dispositivo

Per vedere l'impatto di speed-profile, puoi generare il tuo profilo sul dispositivo:

  1. Reimposta e avvia:

    adb shell am force-stop com.android.codebloat
    
  2. Interagisci: avvia l'app e lascia che esegua la sequenza di avvio.

  3. Dump Profile:

    adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)
    

    In questo modo l'app scrive il profilo attuale sul disco.

  4. Installa profilo:

    adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \
    /data/misc/profiles/ref/com.android.codebloat/primary.prof
    
  5. Compila:

    adb shell cmd package compile -m speed-profile -f com.android.codebloat
    

Al riavvio, vedrai un bilanciamento: Code PSS sarà inferiore a speed (ad es.~16.000 KB) perché sono stati compilati solo i metodi di avvio "hot ", mentre il resto viene gestito dall'interprete o JIT solo se vengono effettivamente utilizzati.

Vedi:

Analisi approfondita del codice compilato

Se vuoi vedere esattamente quali istruzioni genera ART, consulta art/DISASSEMBLY_GUIDE.md.

Fornisce istruzioni dettagliate sull'utilizzo di:

  • oatdump: per visualizzare le istruzioni ARM64 all'interno di un file .odex esistente.
  • dex2oat: per simulare la compilazione con flag di debug dettagliati.

Esercizio: incorporamento del codice

Uno dei motivi per cui il codice compilato può aumentare in modo imprevisto è l'inlining dei metodi. Il compilatore può decidere di copiare il corpo di un metodo piccolo e chiamato di frequente direttamente nei chiamanti.

Nella nostra app CodeBloat, il metodo doSomething() in ogni classe generata chiama semplicemente method0(). Quando viene compilato in modalità speed, il compilatore di ottimizzazione di ART probabilmente inserirà method0() in doSomething().

Allenamento:verifica questa informazione utilizzando oatdump sul tuo dispositivo:

# 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

Cerca il metodo doSomething nell'output. Se è stato incorporato, vedrai le istruzioni per caricare la costante stringa lunga direttamente all'interno di doSomething, anziché un'istruzione bl che ha come target method0.

Visualizzare l'ottimizzazione (CFG)

Per vedere esattamente quando il compilatore ha deciso di incorporare il metodo, puoi produrre un grafico del flusso di controllo (CFG). Mostra lo stato del codice in ogni fase della pipeline di ottimizzazione, con ogni trasformazione nella rappresentazione intermedia (IR) del compilatore fino a quando il codice non viene ridotto all'ISA di destinazione (ad es. ARM64).

  1. Esegui dex2oat con i flag di dump: utilizza il flag --verbose-methods per limitare l'output a metodi specifici; in caso contrario, il file .cfg per un'app di grandi dimensioni può raggiungere diversi gigabyte.

    # 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=doSomething
    
  2. Estrai e visualizza: estrai il file .cfg sulla workstation e aprilo con IR Hydra.

  3. Trova l'inliner: in IR Hydra, carica gli artefatti di compilazione e cerca doSomething. Confronta la rappresentazione prima e dopo il passaggio dell'Inliner. Vedrai il grafico espandersi man mano che le istruzioni di method0 vengono unite al chiamante.

In alternativa, utilizza lo strumento Opt Pipeline in Compiler Explorer (come descritto nella sezione precedente) e inserisci un codice simile per visualizzare una trasformazione simile eseguita nel passaggio Inliner.

Esercizio: campi volatili e barriere di memoria

Nell'app MemoryLab, il campo mGarbageSink è contrassegnato come volatile. In questo modo, il compilatore non ottimizza le nostre allocazioni di spazzatura.

public volatile byte[] mGarbageSink;

Nel disassemblaggio ARM64, vedrai che ogni memorizzazione in questo campo è accompagnata da una barriera di memoria (dmb ish) o dall'uso di istruzioni Load-Acquire/Store-Release (ldar/stlr). Ciò garantisce la visibilità dei thread, ma aggiunge alcune istruzioni aggiuntive a ogni accesso, aumentando leggermente le dimensioni del codice rispetto a un campo normale.

Esercizio: trova gli accessi ai campi e le barriere di memoria associate nel disassemblaggio.

Esercizio: controlli di sospensione implicita

Se smonti un ciclo, come quello in generateAllocationChurn, noterai un'istruzione curiosa alla fine del corpo del ciclo:

ldr x21, [x21]

Questo è un controllo di sospensione implicita. ART lo utilizza per consentire al Garbage Collector di sospendere i thread in modo sicuro. Il registro x21 in genere punta a se stesso. Quando il GC deve sospendere il thread, "avvelena" la posizione di memoria. La prossima volta che il thread esegue ldr, verrà attivato un errore che il runtime rileva e utilizza per far passare il thread a uno stato sospeso.

Questo pattern viene ripetuto in ogni ciclo e all'inizio di ogni metodo, contribuendo alle dimensioni totali del codice dell'applicazione.

Esercizio:trova tutti i controlli di sospensione implicita nel disassembly del metodo e prova a metterli in correlazione con il codice sorgente originale.


← WebView | ↑ Su | Thread →