R8 Optimized Resource Shrinking in AGP 9: Quando l'analisi statica incontra l'accesso dinamico
di Nikolay Vlasov
Il passaggio ad Android Gradle Plugin (AGP) 9.0 ha introdotto un cambiamento significativo nel funzionamento della riduzione delle risorse (resource shrinking). Quando isShrinkResources = true è abilitato nel build type, AGP utilizza ora l'optimized resource shrinking. Questo meccanismo collega strettamente l'analisi delle risorse all'analisi del codice, rimuovendo tutte le risorse riferite solo da codice non utilizzato. Sebbene ciò porti a file APK più piccoli, può anche causare problemi nei progetti che si affidano all'accesso dinamico alle risorse, problemi che potrebbero essere passati inosservati nelle versioni precedenti. (Android Developers)
Perché le risorse scompaiono nei Dynamic Feature Modules?
I moderni resource shrinker si affidano pesantemente all'analisi statica. Se una risorsa è riferita direttamente nel codice (ad esempio, R.raw.my_resource), lo shrinker può facilmente tracciarne l'utilizzo. Tuttavia, quando si accede alle risorse dinamicamente utilizzando nomi di stringa e lookup a runtime, l'analisi statica non può più provare in modo affidabile che la risorsa sia necessaria. Con AGP 9, questo diventa ancora più marcato poiché l'optimized resource shrinking fa ora parte della pipeline unificata di ottimizzazione del codice e delle risorse. (Android Developers)
Nei progetti che utilizzano i Dynamic Feature Modules (DFM), il modulo base spesso manca di dipendenze a tempo di compilazione sulle classi R della feature, specialmente quando i moduli vengono consegnati separatamente tramite Play Feature Delivery. Sebbene il nuovo resource shrinker supporti le dynamic feature già da tempo, è proprio questa architettura che porta più frequentemente a problemi di "risorse mancanti" a causa dei lookup dinamici. (Android Developers)
Prendiamo, ad esempio, un estratto dal progetto Smart Directory Agent Center. Il caricamento di un indice di ricerca full-text è implementato come segue:
private fun getFtsId(): Int {
return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
"fts",
"raw",
featuresManager.ftsModulePackage,
)
}
Per lo shrinker di risorse, questa chiamata è una "scatola nera": il nome della risorsa viene passato come stringa invece che tramite un riferimento statico. Di conseguenza, gli strumenti di build potrebbero erroneamente assumere che la risorsa sia inutilizzata e rimuoverla durante la build di rilascio, portando a una Resources$NotFoundException a runtime. (Android Developers)
L'evoluzione del Resource Shrinking in AGP
È importante notare che AGP 9.0 non ha reinventato il resource shrinking da zero; piuttosto, ha reso il suo comportamento più rigoroso e prevedibile all'interno della pipeline ottimizzata. Il Precise resource shrinking era già abilitato per impostazione predefinita in AGP 8.3. In AGP 9.0, l'optimized resource shrinking viene attivato automaticamente quando isShrinkResources = true. Per le versioni tra la 8.6 e la 9.0, questo comportamento richiedeva solitamente il flag esplicito android.r8.optimizedResourceShrinking=true. (Android Developers)
Attenzione alla regressione in AGP 8.9: le note di rilascio hanno documentato un bug del resource shrinking che causava la scomparsa di risorse nei dynamic feature module. Questo è stato successivamente corretto nelle versioni 8.9.2 e 8.10. Se riscontrate un crash nella build di rilascio, potrebbe non essere un difetto dell'architettura: potrebbe trattarsi di un bug in una versione specifica dello shrinker. (Android Developers)
La soluzione reale: file keep e conservazione esplicita delle risorse
Il modo corretto per proteggere le risorse utilizzate indirettamente è definire un file keep nelle risorse del progetto. La documentazione raccomanda di creare un file XML in res/raw/ (ad esempio, res/raw/my.package.keep.xml) invece di affidarsi a un generico "keep.xml globale" come meccanismo speciale. L'utilizzo di un nome file unico per il file keep previene conflitti durante il merging delle risorse in progetti multi-modulo. (Android Developers)
Nel nostro caso, la correzione è stata la seguente:
<!-- Percorso: app/src/main/res/raw/com.example.app.keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@style/Theme.StartingScreen_*,@style/AppTheme_*,@raw/fts" />
Questo approccio rende esplicita la vostra intenzione: la risorsa @raw/fts non deve essere rimossa, anche se è accessibile solo tramite la risoluzione dinamica del nome. Questo è fondamentale per tutte le risorse lette tramite Resources.getIdentifier(). (Android Developers)
Best practice per AGP 9 e R8
In primo luogo, per le risorse a cui si accede dinamicamente, è meglio elencarle esplicitamente in tools:keep piuttosto che affidarsi a wildcard generici come @raw/*. Più le regole sono specifiche, minore è il rischio di conservare risorse non necessarie e più prevedibile sarà il comportamento della build. (Android Developers)
In secondo luogo, tenete a mente lo shrinkMode. Per impostazione predefinita viene utilizzata la safe mode, che tenta di preservare le risorse che potrebbero essere utilizzate dinamicamente tramite getIdentifier(). Se si passa a tools:shrinkMode="strict", verranno conservate solo le risorse esplicitamente referenziate, rendendo la precisione delle regole keep assolutamente fondamentale. (Android Developers)
In terzo luogo, non confondete la causa con il sintomo. Il problema non è che il progetto utilizza i DFM; è che alcune risorse sono raggiungibili solo tramite lookup a runtime e restano invisibili all'analisi statica. I DFM rendono semplicemente questo scenario più comune e più evidente durante l'ottimizzazione. (Android Developers)
Riassunto
Le risorse mancanti in AGP 9 non sono il risultato di magia o di errori casuali. Sono una conseguenza logica di un'analisi statica più rigorosa nell'optimized resource shrinking. Se l'architettura si affida all'accesso dinamico alle risorse, è necessario proteggere esplicitamente tali risorse utilizzando i file keep e verificare il comportamento rispetto alla modalità dello shrinker scelta. Se i crash compaiono specificamente dopo l'aggiornamento ad AGP 8.9–9.0, controllate la versione del plugin, poiché le regressioni note in quest'area sono già state affrontate nelle patch successive. (Android Developers)
Risorse utili
- 1 Enable app optimization with R8 — Guida ufficiale per la configurazione di R8.
- 2 Android Gradle Plugin 7.1.0 Release Notes — Registro delle modifiche e supporto alle Dynamic Feature.
- 3 Tools attributes reference — Descrizione degli attributi
keepeshrinkMode. - 4 Android Gradle Plugin 8.3.0 Release Notes — Dettagli su Precise Resource Shrinking.
- 5 Android Gradle Plugin 8.9.0 Release Notes — Informazioni sulle correzioni dei bug del resource shrinking.
- 6 Customize which resources to keep — Guida dettagliata sull'uso di
keep.xml.