R8 Optimized Resource Shrinking no AGP 9: Quando a análise estática encontra o acesso dinâmico
por Nikolay Vlasov
A transição para o Android Gradle Plugin (AGP) 9.0 introduziu uma mudança significativa na forma como a redução de recursos (resource shrinking) funciona. Quando isShrinkResources = true está habilitado no seu tipo de build, o AGP utiliza agora o optimized resource shrinking. Este mecanismo vincula estreitamente a análise de recursos com a análise de código, removendo todos os recursos referenciados apenas por código não utilizado. Embora isto leve a APKs mais pequenos, também pode causar problemas em projetos que dependem do acesso dinâmico a recursos — problemas que poderiam ter passado despercebidos em versões anteriores. (Android Developers)
Por que os recursos desaparecem nos Dynamic Feature Modules?
Os redutores de recursos (resource shrinkers) modernos dependem fortemente da análise estática. Se um recurso é referenciado diretamente no seu código (por exemplo, R.raw.my_resource), o redutor pode facilmente rastrear o seu uso. No entanto, quando acede a recursos dinamicamente utilizando nomes de strings e pesquisas em tempo de execução (runtime lookups), a análise estática já não pode provar de forma fiável que o recurso é necessário. Com o AGP 9, isto torna-se ainda mais pronunciado, uma vez que a redução otimizada de recursos faz agora parte do pipeline unificado de otimização de código e recursos. (Android Developers)
Em projetos que utilizam Dynamic Feature Modules (DFM), o módulo base muitas vezes carece de dependências em tempo de compilação nas classes R da funcionalidade, especialmente quando os módulos são entregues separadamente através do Play Feature Delivery. Embora o novo redutor de recursos suporte funcionalidades dinâmicas há algum tempo, é precisamente nesta arquitetura que os problemas de "recursos em falta" ocorrem com mais frequência devido às pesquisas dinâmicas. (Android Developers)
Tomemos como exemplo um excerto do projeto Smart Directory Agent Center. O carregamento de um índice de pesquisa de texto completo é implementado da seguinte forma:
private fun getFtsId(): Int {
return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
"fts",
"raw",
featuresManager.ftsModulePackage,
)
}
Para o redutor de recursos, esta chamada é uma "caixa preta": o nome do recurso é passado como uma string em vez de uma referência estática. Como resultado, as ferramentas de build podem assumir incorretamente que o recurso não é utilizado e removê-lo durante o build de lançamento, levando a uma Resources$NotFoundException em tempo de execução. (Android Developers)
A evolução do Resource Shrinking no AGP
É importante notar que o AGP 9.0 não reinventou a redução de recursos do zero; pelo contrário, tornou o seu comportamento mais rigoroso e previsível dentro do pipeline otimizado. O Precise resource shrinking já estava ativado por predefinição no AGP 8.3. No AGP 9.0, o optimized resource shrinking é acionado automaticamente quando isShrinkResources = true. Para versões entre a 8.6 e a 9.0, este comportamento exigia habitualmente o flag explícito android.r8.optimizedResourceShrinking=true. (Android Developers)
Tenha atenção à regressão no AGP 8.9: as notas de lançamento documentaram um erro de redução de recursos que causava o desaparecimento de recursos em módulos de funcionalidades dinâmicas. Isto foi corrigido posteriormente nas versões 8.9.2 e 8.10. Se encontrar uma falha no seu build de lançamento, pode não ser um defeito de arquitetura — pode ser um erro numa versão específica do redutor. (Android Developers)
A solução real: arquivos keep e retenção explícita de recursos
A forma correta de proteger os recursos utilizados indiretamente é definir um arquivo keep nos recursos do seu projeto. A documentação recomenda a criação de um arquivo XML em res/raw/ (por exemplo, res/raw/my.package.keep.xml) em vez de confiar num "keep.xml global" genérico como um mecanismo especial. A utilização de um nome de arquivo único para o arquivo keep evita conflitos durante a fusão de recursos (resource merging) em projetos multi-módulo. (Android Developers)
No nosso caso, a correção foi a seguinte:
<!-- Caminho: 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" />
Esta abordagem torna a sua intenção explícita: o recurso @raw/fts não deve ser removido, mesmo que seja apenas acessível através da resolução dinâmica de nomes. Isto é fundamental para quaisquer recursos lidos através de Resources.getIdentifier(). (Android Developers)
Melhores práticas para AGP 9 e R8
Primeiro: para recursos acedidos dinamicamente, é melhor listá-los explicitamente em tools:keep do que confiar em wildcards genéricos como @raw/*. Quanto mais específicas forem as suas regras, menor será a probabilidade de manter recursos desnecessários e mais previsível será o comportamento do seu build. (Android Developers)
Segundo: tenha em mente o shrinkMode. Por predefinição, é utilizado o safe mode, que tenta preservar os recursos que podem ser usados dinamicamente através de getIdentifier(). Se mudar para tools:shrinkMode="strict", apenas os recursos referenciados explicitamente são mantidos, o que torna a precisão das suas regras keep absolutamente crítica. (Android Developers)
Terceiro: não confunda a causa com o sintoma. O problema não é que o seu projeto utiliza DFM; é que alguns recursos são apenas alcançáveis através de pesquisas em tempo de execução e permanecem invisíveis para a análise estática. O DFM simplesmente torna este cenário mais comum e mais evidente durante a otimização. (Android Developers)
Resumo
A falta de recursos no AGP 9 não é o resultado de magia ou erro aleatório. Eles são uma consequência lógica de uma análise estática mais rigorosa na redução otimizada de recursos. Se a sua arquitetura depende do acesso dinâmico a recursos, deve proteger explicitamente esses recursos utilizando arquivos keep e verificar o comportamento em relação ao modo de redução escolhido. Se aparecerem falhas especificamente após a atualização para o AGP 8.9–9.0, verifique novamente a versão do seu plugin, pois regressões conhecidas nesta área já foram abordadas em patches subsequentes. (Android Developers)
Recursos úteis
- 1 Enable app optimization with R8 — Guia oficial para configurar o R8.
- 2 Android Gradle Plugin 7.1.0 Release Notes — Registo de alterações e suporte para Dynamic Features.
- 3 Tools attributes reference — Descrição dos atributos
keepeshrinkMode. - 4 Android Gradle Plugin 8.3.0 Release Notes — Detalhes sobre o Precise Resource Shrinking.
- 5 Android Gradle Plugin 8.9.0 Release Notes — Informações sobre correções de erros na redução de recursos.
- 6 Customize which resources to keep — Guia detalhado sobre como trabalhar com o
keep.xml.