Menü
R8 Optimized Resource Shrinking in AGP 9: Wenn statische Analyse auf dynamischen Zugriff trifft

R8 Optimized Resource Shrinking in AGP 9: Wenn statische Analyse auf dynamischen Zugriff trifft

von Nikolay Vlasov

Der Übergang zum Android Gradle Plugin (AGP) 9.0 hat die Art und Weise, wie das Resource Shrinking funktioniert, grundlegend verändert. Wenn isShrinkResources = true in Ihrem Build-Typ aktiviert ist, verwendet AGP nun das Optimized Resource Shrinking. Dieser Mechanismus verknüpft die Ressourcenanalyse eng mit der Codeanalyse und entfernt alle Ressourcen, auf die nur von nicht verwendetem Code verwiesen wird. Dies führt zwar zu kleineren APKs, kann aber auch Probleme in Projekten verursachen, die auf dynamischen Ressourcenzugriff angewiesen sind – Probleme, die in früheren Versionen möglicherweise unbemerkt geblieben wären. (Android Developers)

Warum verschwinden Ressourcen in Dynamic Feature Modules?

Moderne Resource Shrinker verlassen sich stark auf die statische Analyse. Wenn eine Ressource direkt in Ihrem Code referenziert wird (z. B. R.raw.my_resource), kann der Shrinker deren Verwendung leicht verfolgen. Wenn Sie jedoch dynamisch über String-Namen und Runtime-Lookups auf Ressourcen zugreifen, kann die statische Analyse nicht mehr zuverlässig beweisen, dass die Ressource benötigt wird. Mit AGP 9 wird dies noch ausgeprägter, da das Optimized Resource Shrinking nun Teil der gemeinsamen Pipeline zur Code- und Ressourcenoptimierung ist. (Android Developers)

In Projekten, die Dynamic Feature Modules (DFM) verwenden, fehlen dem Basismodul oft Compile-Time-Abhängigkeiten von den R-Klassen des Features, insbesondere wenn die Module separat über Play Feature Delivery ausgeliefert werden. Obwohl der neue Resource Shrinker Dynamic Features schon seit einiger Zeit unterstützt, ist es genau diese Architektur, bei der Probleme mit "fehlenden Ressourcen" aufgrund dynamischer Lookups am häufigsten auftreten. (Android Developers)

Nehmen wir als Beispiel einen Ausschnitt aus dem Projekt Smart Directory Agent Center. Das Laden eines Volltextsuchindex ist wie folgt implementiert:

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

Für den Resource Shrinker ist dieser Aufruf eine „Black Box“: Der Ressourcenname wird als String übergeben und nicht über eine statische Referenz. Infolgedessen könnten die Build-Tools fälschlicherweise annehmen, dass die Ressource nicht verwendet wird, und sie beim Release-Build entfernen, was zu einer Resources$NotFoundException zur Laufzeit führt. (Android Developers)

R8 Optimized Resource Shrinking
Das optimierte Ressourcen-Shrinking verbindet die Codeanalyse eng mit den verfügbaren Ressourcen.

Die Entwicklung des Resource Shrinking in AGP

Es ist wichtig zu verstehen, dass AGP 9.0 das Resource Shrinking nicht neu erfunden hat, sondern sein Verhalten im Rahmen der optimierten Pipeline strenger und vorhersehbarer gemacht hat. Precise Resource Shrinking war bereits in AGP 8.3 standardmäßig aktiviert. In AGP 9.0 wird das Optimized Resource Shrinking automatisch ausgelöst, wenn isShrinkResources = true gesetzt ist. Für Versionen zwischen 8.6 und 9.0 war für dieses Verhalten in der Regel das explizite Flag android.r8.optimizedResourceShrinking=true erforderlich. (Android Developers)

Beachten Sie auch die Regression in AGP 8.9: In den Release Notes wurde ein Problem beim Resource Shrinking dokumentiert, das dazu führte, dass Ressourcen in Dynamic Feature Modules verloren gingen. Dies wurde später in den Versionen 8.9.2 und 8.10 behoben. Wenn Sie einen Absturz in Ihrem Release-Build feststellen, muss dies nicht unbedingt an Ihrer Architektur liegen – es könnte sich um einen Bug in einer bestimmten Version des Shrinkers handeln. (Android Developers)

Die echte Lösung: Keep-Dateien und explizites Erhalten von Ressourcen

Der richtige Weg, indirekt verwendete Ressourcen zu schützen, besteht darin, eine Keep-Datei in den Ressourcen Ihres Projekts zu definieren. Die Dokumentation empfiehlt, eine XML-Datei in res/raw/ zu erstellen (z. B. res/raw/my.package.keep.xml), anstatt sich auf eine generische „globale keep.xml“ als speziellen Mechanismus zu verlassen. Die Verwendung eines eindeutigen Dateinamens für die Keep-Datei verhindert Konflikte beim Zusammenführen von Ressourcen in Projekten mit vielen Modulen. (Android Developers)

In unserem Fall sah die Lösung so aus:

<!-- Pfad: 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" />

Dieser Ansatz macht Ihre Absicht explizit: Die Ressource @raw/fts darf nicht entfernt werden, auch wenn sie nur über die dynamische Namensauflösung zugänglich ist. Dies ist entscheidend für alle Ressourcen, die über Resources.getIdentifier() gelesen werden. (Android Developers)

Best Practices für AGP 9 und R8

Erstens: Für Ressourcen, auf die dynamisch zugegriffen wird, ist es am besten, sie explizit in tools:keep aufzuführen, anstatt sich auf allgemeine Platzhalter wie @raw/* zu verlassen. Je spezifischer Ihre Regeln sind, desto geringer ist das Risiko, unnötige Ressourcen beizubehalten, und desto vorhersehbarer wird das Verhalten Ihres Builds. (Android Developers)

Zweitens: Denken Sie an den shrinkMode. Standardmäßig wird der Safe Mode verwendet, der versucht, Ressourcen zu erhalten, die dynamisch über getIdentifier() verwendet werden könnten. Wenn Sie zu tools:shrinkMode="strict" wechseln, werden nur explizit referenzierte Ressourcen beibehalten, was die Genauigkeit Ihrer Keep-Regeln absolut kritisch macht. (Android Developers)

Drittens: Verwechseln Sie nicht die Ursache mit dem Symptom. Das Problem ist nicht, dass Ihr Projekt DFM verwendet, sondern dass einige Ressourcen nur über Runtime-Lookups erreichbar sind und für die statische Analyse unsichtbar bleiben. DFM macht dieses Szenario lediglich häufiger und während der Optimierung deutlicher spürbar. (Android Developers)

Fazit

Fehlende Ressourcen in AGP 9 sind kein Zufall oder magischer Fehler. Sie sind eine logische Folge der strengeren statischen Analyse beim Optimized Resource Shrinking. Wenn Ihre Architektur auf dynamischen Ressourcenzugriff angewiesen ist, müssen Sie diese Ressourcen explizit über Keep-Dateien schützen und das Verhalten für den von Ihnen gewählten Shrinker-Modus überprüfen. Wenn Abstürze speziell nach der Aktualisierung auf AGP 8.9–9.0 auftreten, überprüfen Sie Ihre Plugin-Version, da bekannte Regressionen in diesem Bereich bereits in nachfolgenden Patches behoben wurden. (Android Developers)

Nützliche Ressourcen