Meny
R8 Optimized Resource Shrinking i AGP 9: När statisk analys möter dynamisk åtkomst

R8 Optimized Resource Shrinking i AGP 9: När statisk analys möter dynamisk åtkomst

av Nikolay Vlasov

Övergången till Android Gradle Plugin (AGP) 9.0 innebar en betydande förändring i hur resursförminskning (resource shrinking) fungerar. När isShrinkResources = true är aktiverat i din build-typ, använder AGP nu optimized resource shrinking. Denna mekanism kopplar samman resursanalys tätt med kodanalys och tar bort alla resurser som endast refereras till av oanvänd kod. Även om detta leder till mindre APK-filer, kan det också orsaka problem i projekt som är beroende av dynamisk resursåtkomst – problem som kan ha gått obemärkt förbi i tidigare versioner. (Android Developers)

Varför försvinner resurser i Dynamic Feature Modules?

Moderna resursförminskare (resource shrinkers) förlitar sig tungt på statisk analys. Om en resurs refereras direkt i din kod (t.ex. R.raw.my_resource), kan förminskaren enkelt spåra dess användning. Men när du får åtkomst till resurser dynamiskt med hjälp av strängnamn och runtime-uppslag (runtime lookup), kan statisk analys inte längre tillförlitligt bevisa att resursen behövs. Med AGP 9 blir detta ännu tydligare, eftersom optimized resource shrinking nu är en del av den förenade optimeringspipelinen för kod och resurser. (Android Developers)

I projekt som använder Dynamic Feature Modules (DFM), saknar basmodulen ofta compile-time-beroenden till funktionens R-klasser, särskilt när moduler levereras separat via Play Feature Delivery. Även om den nya resursförminskaren har stöttat dynamiska funktioner under en tid, är det just i denna arkitektur som problem med "saknade resurser" oftast uppstår på grund av dynamiska uppslag. (Android Developers)

Ta till exempel ett utdrag från projektet Smart Directory Agent Center. Laddning av ett fulltextsökningsindex är implementerat enligt följande:

private fun getFtsId(): Int {
    return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
        "fts",
        "raw",
        featuresManager.ftsModulePackage,
    )
}

För resursförminskaren är detta anrop en "svart låda": resursnamnet skickas som en sträng istället för via en statisk referens. Som ett resultat kan byggverktygen felaktigt anta att resursen är oanvänd och ta bort den under release-build, vilket leder till en Resources$NotFoundException vid körning. (Android Developers)

R8 Optimized Resource Shrinking
Optimerad resursförminskning kopplar samman kodanalys tätt med tillgängliga resurser.

Evolutionen av resursförminskning i AGP

Det är viktigt att förstå att AGP 9.0 inte uppfann resursförminskning på nytt från grunden; istället gjorde den beteendet mer strikt och förutsägbart inom den optimerade pipelinen. Precise resource shrinking var redan aktiverat som standard i AGP 8.3. I AGP 9.0 utlöses optimized resource shrinking automatiskt när isShrinkResources = true. För versioner mellan 8.6 och 9.0 krävde detta beteende vanligtvis den explicita flaggan android.r8.optimizedResourceShrinking=true. (Android Developers)

Var uppmärksam på regressionen i AGP 8.9: release notes dokumenterade ett fel i resursförminskning som ledde till att resurser försvann i dynamiska funktionsmoduler. Detta korrigerades senare i versionerna 8.9.2 och 8.10. Om du upplever att appen kraschar i en release-build, behöver det inte vara ett arkitektoniskt fel – det kan vara en bugg i en specifik version av förminskaren. (Android Developers)

Den verkliga lösningen: keep-filer och explicit resursbevarande

Det rätta sättet att skydda resurser som används indirekt är att definiera en keep-fil i projektets resurser. Dokumentationen rekommenderar att man skapar en XML-fil i res/raw/ (t.ex. res/raw/my.package.keep.xml) istället for att förlita sig på en generisk "global keep.xml" som en speciell mekanism. Användning av ett unikt filnamn för keep-filen förhindrar konflikter under resurssammanslagning (merging) i multimodulprojekt. (Android Developers)

I vårt fall såg rättningen ut så här:

<!-- Sökväg: 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" />

Detta tillvägagångssätt gör din avsikt explicit: @raw/fts-resursen får inte tas bort, även om den bara är tillgänglig via dynamisk namnuppslagning. Detta är kritiskt för alla resurser som läses via Resources.getIdentifier(). (Android Developers)

Best practices för AGP 9 och R8

För det första: För resurser man har åtkomst till dynamiskt är det bäst att föra upp dem explicit i tools:keep istället för att förlita sig på breda jokertecken som @raw/*. Ju mer specifika dina regler är, desto mindre är risken att behålla onödiga resurser, och desto mer förutsägbart blir byggbeteendet. (Android Developers)

För det andra: Kom ihåg shrinkMode. Som standard används safe mode, som försöker bevara resurser som kan tänkas användas dynamiskt via getIdentifier(). Om du byter till tools:shrinkMode="strict", behålls endast resurser det refereras explicit till, vilket gör noggrannheten i dina keep-regler helt kritisk. (Android Developers)

För det tredje: Förväxla inte orsaken med symptomet. Problemet är inte att ditt projekt använder DFM; det är att vissa resurser endast är nåbara via runtime-uppslag och förblir osynliga för statisk analys. DFM gör helt enkelt detta scenario vanligare och tydligare under optimering. (Android Developers)

Sammanfattning

Saknade resurser i AGP 9 är inte ett resultat av magi eller slumpmässiga fel. De är en logisk konsekvens av striktare statisk analys i optimized resource shrinking. Om din arkitektur baserar sig på dynamisk resursåtkomst, måste du explicit skydda dessa resurser med hjälp av keep-filer och verifiera beteendet mot ditt valda förminskarläge. Om krascher uppstår specifikt efter uppdatering till AGP 8.9–9.0, dubbelkolla din plugin-version, eftersom kända regressioner på detta område redan har adresserats i efterföljande uppdateringar. (Android Developers)

Användbara resurser