Menu
R8 Optimized Resource Shrinking w AGP 9: Gdy analiza statyczna spotyka się z dostępem dynamicznym

R8 Optimized Resource Shrinking w AGP 9: Gdy analiza statyczna spotyka się z dostępem dynamicznym

przez Nikolay Vlasov

Przejście na Android Gradle Plugin (AGP) 9.0 przyniosło znaczącą zmianę w sposobie działania mechanizmu usuwania nieużywanych zasobów (resource shrinking). Gdy w build type włączona jest opcja isShrinkResources = true, AGP korzysta teraz z optimized resource shrinking. Mechanizm ten ściśle łączy analizę zasobów z analizą kodu, usuwając wszystkie zasoby, do których odwołania znajdują się wyłącznie w nieużywanym kodzie. Choć przekłada się to na mniejszy rozmiar plików APK, może również powodować problemy w projektach opierających się na dynamicznym dostępie do zasobów — problemy, które w starszych wersjach mogły pozostawać niezauważone. (Android Developers)

Dlaczego zasoby znikają w Dynamic Feature Modules?

Współczesne narzędzia typu resource shrinker w dużym stopniu polegają na analizie statycznej. Jeśli zasób jest przywoływany bezpośrednio w kodzie (np. R.raw.my_resource), shrinker może łatwo śledzić jego użycie. Jeśli jednak dostęp odbywa się dynamicznie, poprzez nazwy tekstowe i wyszukiwanie w czasie wykonywania (runtime lookup), analiza statyczna nie jest już w stanie wiarygodnie udowodnić, że zasób jest potrzebny. W AGP 9 staje się to jeszcze bardziej widoczne, ponieważ zoptymalizowane usuwanie zasobów stanowi teraz część wspólnego procesu optymalizacji kodu i zasobów. (Android Developers)

W projektach wykorzystujących Dynamic Feature Modules (DFM), moduł bazowy często nie posiada zależności w czasie kompilacji (compile-time dependencies) od klas R danej funkcji, zwłaszcza gdy moduły są dostarczane oddzielnie poprzez Play Feature Delivery. Choć nowy resource shrinker od pewnego czasu wspiera dynamiczne funkcje, to właśnie taka architektura najczęściej prowadzi do sytuacji, w której część zasobów okazuje się „brakująca” z powodu dynamicznego wyszukiwania. (Android Developers)

Rozpatrzmy przykład z projektu Smart Directory Agent Center. Ładowanie indeksu wyszukiwania pełnotekstowego zaimplementowano w następujący sposób:

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

Dla mechanizmu usuwania zasobów takie wywołanie to „czarna skrzynka”: nazwa zasobu przekazywana jest jako ciąg znaków, a nie poprzez statyczną referencję. W rezultacie narzędzia budowania mogą błędnie uznać zasób za nieużywany i usunąć go podczas budowania wersji produkcyjnej, co prowadzi do błędu Resources$NotFoundException w czasie działania aplikacji. (Android Developers)

R8 Optimized Resource Shrinking
Zoptymalizowane usuwanie zasobów ściśle łączy analizę kodu z dostępnymi zasobami.

Ewolucja Resource Shrinking w AGP

Warto zrozumieć, że AGP 9.0 nie wynalazł mechanizmu usuwania zasobów na nowo, lecz nadał mu bardziej restrykcyjny i przewidywalny charakter w ramach zoptymalizowanego procesu. Funkcja Precise resource shrinking była już domyślnie włączona w AGP 8.3. W wersji AGP 9.0 optimized resource shrinking uruchamia się automatycznie przy ustawieniu isShrinkResources = true. W przypadku wersji od 8.6 do 9.0 zachowanie to zazwyczaj wymagało jawnego ustawienia flagi android.r8.optimizedResourceShrinking=true. (Android Developers)

Należy pamiętać o regresji w AGP 8.9: w informacjach o wydaniu odnotowano błąd resource shrinking, przez który w dynamicznych modułach funkcji mogły znikać zasoby. Poprawki wprowadzono w wersjach 8.9.2 i 8.10. Dlatego błąd w wersji produkcyjnej nie zawsze oznacza błąd w architekturze projektu — czasem jest to faktycznie błąd konkretnej wersji narzędzia. (Android Developers)

Prawdziwe rozwiązanie: pliki keep i jawne zachowywanie zasobów

Właściwym sposobem na ochronę zasobów używanych pośrednio jest zdefiniowanie pliku keep w zasobach projektu. Dokumentacja zaleca tworzenie pliku XML w katalogu res/raw/ (np. res/raw/my.package.keep.xml), zamiast polegania na generycznym „globalnym keep.xml” jako specjalnym mechanizmie. Użycie unikalnej nazwy pliku keep zapobiega konfliktom podczas scalania zasobów w projektach wielomodułowych. (Android Developers)

W naszym przypadku rozwiązanie wyglądało następująco:

<!-- Ścieżka: 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" />

Takie podejście sprawia, że intencja staje się jawna: zasób @raw/fts nie może zostać usunięty, nawet jeśli jest dostępny wyłącznie poprzez dynamiczne rozpoznawanie nazw. Jest to krytyczne dla wszystkich zasobów odczytywanych za pomocą Resources.getIdentifier(). (Android Developers)

Najlepsze praktyki dla AGP 9 i R8

Po pierwsze: w przypadku zasobów, do których dostęp jest dynamiczny, najlepiej jest wymieniać je jawnie w tools:keep, zamiast polegać na szerokich maskach typu @raw/*. Im bardziej precyzyjne są reguły, tym mniejsze ryzyko zachowania niepotrzebnych zasobów i tym bardziej przewidywalne staje się zachowanie podczas budowania. (Android Developers)

Po drugie: pamiętaj o trybie shrinkMode. Domyślnie używany jest safe mode, który stara się zachować zasoby mogące być użyte dynamicznie poprzez getIdentifier(). Jeśli włączysz tools:shrinkMode="strict", pozostawione zostaną tylko jawnie cytowane zasoby, przez co precyzja reguł keep staje się absolutnie kluczowa. (Android Developers)

Po trzecie: nie myl przyczyny z objawem. Problem nie wynika z faktu, że projekt korzysta z DFM jako takich, lecz z tego, że część zasobów jest dostępna wyłącznie poprzez wyszukiwanie w czasie wykonywania i jest niewidoczna dla analizy statycznej. DFM sprawia po prostu, że ten scenariusz jest częstszy i bardziej odczuwalny podczas optymalizacji. (Android Developers)

Podsumowanie

Brakujące zasoby w AGP 9 nie są wynikiem magii ani przypadku. Są logiczną konsekwencją bardziej rygorystycznej analizy statycznej w zoptymalizowanym usuwaniu zasobów. Jeśli architektura opiera się na dynamicznym dostępie do zasobów, należy je jawnie chronić za pomocą plików keep i weryfikować zachowanie w odpowiednim trybie shrinkera. A jeśli błąd pojawił się właśnie po aktualizacji do AGP 8.9–9.0, warto dodatkowo sprawdzić wersję wtyczki: w tym obszarze istniały znane regresje, które zostały już naprawione w nowszych patchach. (Android Developers)

Przydatne zasoby