Notícias sobre produtos

Configurar e resolver problemas de regras de manutenção do R8

Leitura de 7 min

No desenvolvimento moderno para Android, o lançamento de um aplicativo pequeno, rápido e seguro é uma expectativa fundamental do usuário. A principal ferramenta do sistema de build do Android para isso é o otimizador R8 , o compilador que lida com a remoção de recursos e códigos inativos para redução, renomeação ou minificação de código e otimização de apps.

Ativar o R8 é uma etapa essencial na preparação de um app para lançamento, mas exige que os desenvolvedores forneçam orientações na forma de "regras de manutenção".

Depois de ler este artigo, confira o vídeo da Semana de Destaque de Performance sobre como ativar, depurar e resolver problemas do otimizador R8 no YouTube.

 

 

Por que as regras de retenção são necessárias

A necessidade de escrever regras de manutenção decorre de um conflito central: o R8 é uma ferramenta de análise estática, mas os apps Android geralmente dependem de padrões de execução dinâmica, como reflexão ou chamadas para dentro e fora do código nativo usando a JNI (Java Native Interface).

O R8 cria um gráfico de código usado analisando chamadas diretas. Quando o código é acessado de forma dinâmica, a análise estática do R8 não consegue prever isso e identifica o código como não utilizado e o remove, o que leva a falhas no ambiente de execução.

Uma regra de manutenção é uma instrução explícita para o compilador R8, que diz: "Esta classe, método ou campo específico é um ponto de entrada que será acessado dinamicamente no momento da execução. Você precisa mantê-lo, mesmo que não encontre uma referência direta a ele."

Consulte o guia oficial para mais detalhes sobre as regras de manutenção.

Onde escrever regras de manutenção

As regras de retenção personalizadas de um aplicativo são escritas em um arquivo de texto. Por convenção, esse arquivo é chamado de proguard-rules.pro e está localizado na raiz do módulo do app ou da biblioteca. Esse arquivo é especificado no tipo de build release do arquivo build.gradle.kts do módulo.

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

Usar o arquivo padrão correto

O método getDefaultProguardFile importa um conjunto padrão de regras fornecido pelo SDK do Android. Ao usar o arquivo errado, seu app pode não ser otimizado. Use proguard-android-optimize.txt. Esse arquivo fornece as regras de manutenção padrão para componentes padrão do Android e ativa as otimizações de código do R8. O proguard-android.txt desatualizado só fornece as regras de manutenção, mas não ativa as otimizações do R8.

progaurd.png

Como esse é um problema sério de desempenho, estamos começando a alertar os desenvolvedores sobre o uso do arquivo errado, a partir da atualização de recursos do Android Studio Narwhal 3. A partir da versão 9.0 do Plug-in do Android para Gradle, não vamos mais oferecer suporte ao arquivo proguard-android.txt desatualizado. Por isso, faça upgrade para a versão otimizada.

Como escrever regras de manutenção

Uma regra de retenção consiste em três partes principais:

  1. Uma opção como -keep ou -keepclassmembers
  2. Modificadores opcionais, como allowshrinking
  3. Uma especificação de classe que define o código a ser correspondido.

Para ver a sintaxe completa e exemplos, consulte as orientações para adicionar regras de manutenção.

Antipadrões da regra Keep

É importante conhecer as práticas recomendadas, mas também os antipadrões. Esses antipadrões geralmente surgem de mal-entendidos ou atalhos de solução de problemas e podem ser catastróficos para o desempenho de um build de produção.

Opções globais

Essas flags são alternâncias globais que nunca podem ser usadas em um build de lançamento. Eles servem apenas para depuração temporária para isolar um problema.

O uso de -dontotptimize desativa as otimizações de desempenho do R8, o que deixa o app mais lento.

Ao usar -dontobfuscate, você desativa todas as renomeações, e o uso de -dontshrink desativa a remoção de código inativo. Ambas as regras globais aumentam o tamanho do app.

Evite usar essas flags globais em um ambiente de produção sempre que possível para uma experiência do usuário do app com melhor desempenho.

Regras de retenção muito amplas

A maneira mais fácil de anular os benefícios do R8 é escrever regras de manutenção excessivamente amplas. Regras de manutenção como a mostrada abaixo instruem o otimizador do R8 a não reduzir, ofuscar ou otimizar nenhuma classe neste pacote ou em qualquer um dos subpacotes. Isso remove completamente os benefícios do R8 para todo o pacote. Em vez disso, tente escrever regras de manutenção restritas e específicas.
 

-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

O operador de inversão (!)

O operador de inversão (!) parece uma maneira poderosa de excluir um pacote de uma regra. Mas não é tão simples assim. Aqui está um exemplo.

-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

Você pode pensar que essa regra significa "não manter classes emcom.example.package". No entanto, ela significa "manter todas as classes, métodos e propriedades em todo o aplicativo que não estejam em com.example.package". Se isso for uma surpresa para você, verifique se há negações na sua configuração do R8.

Regras redundantes para componentes do Android

Outro erro comum é adicionar manualmente regras de manutenção para o Activities, Services ou BroadcastReceivers do app. Isso é desnecessário. O arquivo proguard-android-optimize.txt padrão já inclui as regras relevantes para que esses componentes padrão do Android funcionem sem problemas.

Além disso, muitas bibliotecas trazem as próprias regras de manutenção. Portanto, não é necessário escrever suas próprias regras para eles. Se houver um problema com as regras de manutenção de uma biblioteca que você está usando, entre em contato com o autor da biblioteca para saber qual é o problema.

Práticas recomendadas para regras de guarda

Agora que você sabe o que não fazer, vamos falar sobre as práticas recomendadas.

Escrever regras de manutenção restritas

As regras de retenção devem ser o mais restritas e específicas possível. Elas devem preservar apenas o que é necessário, permitindo que o R8 otimize todo o restante.
 

RegraQualidade

 

-keep class com.example.** { ; }

 

Baixa:mantém um pacote inteiro e os subpacotes dele.

 

-keep class com.example.MyClass { ; }

 

Baixo:mantém uma classe inteira que provavelmente ainda é muito ampla.
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
Alta:somente os métodos e as propriedades relevantes de uma classe específica são mantidos.

Usar ancestrais comuns

Em vez de escrever regras de manutenção separadas para vários modelos de dados diferentes, escreva uma regra que tenha como destino uma classe de base ou interface comum. A regra abaixo instrui o R8 a manter todos os membros de classes que implementam essa interface e é altamente escalonável.

# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

Usar anotações para segmentar várias classes

Crie uma anotação personalizada (por exemplo, @Serialize) e use-a para "marcar" as classes que precisam ter os campos preservados. Esse é outro padrão limpo, declarativo e altamente escalonável. Também é possível criar regras de manutenção para anotações já existentes de frameworks que você está usando.

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

Escolher a opção de retenção certa

A opção "Manter" é a parte mais importante da regra. Escolher a opção errada pode desativar a otimização sem necessidade.

Opção de manterO que ele faz
-keepImpede que a classe e os membros mencionados na declaração sejam removidos ou renomeados.
-keepclassmembersImpede que os membros especificados sejam removidos ou renomeados, mas permite que a classe em si seja removida, mas apenas em classes que não sejam removidas de outra forma.
-keepclasseswithmembersUma combinação: mantém a classe e os membros dela, somente se todos os membros especificados estiverem presentes.

Saiba mais sobre a opção de manter na documentação sobre as opções de manter.

Permitir otimização com modificadores

Modificadores como allowshrinking e allowobfuscation flexibilizam uma regra -keep ampla, devolvendo o poder de otimização ao R8. Por exemplo, se uma biblioteca legada forçar você a usar -keep em uma classe inteira, talvez seja possível recuperar alguma otimização permitindo a redução e a ofuscação:

# Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

Adicionar opções globais para otimização extra

Além das regras de manutenção, você pode adicionar flags globais ao arquivo de configuração do R8 para incentivar ainda mais otimização.

-repackageclasses é uma opção poderosa que instrui o R8 a mover todas as classes ofuscadas para um único pacote. Isso economiza um espaço significativo no arquivo DEX ao remover strings de nome de pacote redundantes.

-allowaccessmodification permite que o R8 amplie o acesso (por exemplo, private para public) para permitir uma incorporação mais agressiva. Agora, isso é ativado por padrão ao usar proguard-android-optimize.txt.

Aviso:os autores de bibliotecas nunca podem adicionar essas flags de otimização global às regras do consumidor, porque elas seriam aplicadas de forma forçada a todo o app.

Para deixar isso ainda mais claro, na versão 9.0 do Plug-in do Android para Gradle, vamos começar a ignorar completamente as flags de otimização global das bibliotecas. 

Práticas recomendadas para bibliotecas

Todo app Android depende de bibliotecas de uma forma ou de outra. Vamos falar sobre as práticas recomendadas para bibliotecas.

Para desenvolvedores de bibliotecas

Se a biblioteca usa reflexão ou JNI, você precisa fornecer as regras de retenção necessárias aos consumidores. Essas regras são colocadas em um arquivo consumer-rules.pro, que é empacotado automaticamente dentro do arquivo AAR da biblioteca.

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

Para consumidores de bibliotecas

Filtrar regras de retenção problemáticas

Se você precisar usar uma biblioteca que inclua regras de manutenção problemáticas, filtre-as no arquivo build.gradle.kts a partir do AGP 9.0. Isso instrui o R8 a ignorar as regras de uma dependência específica.

release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

A melhor regra de retenção é não ter uma regra de retenção

A melhor estratégia de configuração do R8 é eliminar a necessidade de escrever regras de manutenção. Para muitos apps, isso pode ser feito escolhendo bibliotecas modernas que priorizam a geração de código em vez da reflexão. Com a geração de código, o otimizador pode determinar com mais facilidade qual código é realmente usado no ambiente de execução e qual pode ser removido. Além disso, não usar nenhuma reflexão dinâmica significa que não há pontos de entrada "ocultos" e, portanto, não são necessárias regras de manutenção. Ao escolher uma nova biblioteca, sempre prefira uma solução que use a geração de código em vez da reflexão.

Para mais informações sobre como escolher bibliotecas, consulte Escolher bibliotecas com sabedoria.

Como depurar e solucionar problemas na configuração do R8

Quando o R8 remove um código que deveria ter sido mantido ou o APK é maior do que o esperado, use essas ferramentas para diagnosticar o problema.

Encontrar regras Keep duplicadas e globais

Como o R8 mescla regras de dezenas de fontes, pode ser difícil saber qual é o conjunto de regras final. Adicionar essa flag ao arquivo proguard-rules.pro gera um relatório completo:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

Você pode pesquisar esse arquivo para encontrar regras redundantes ou rastrear uma regra problemática (como -dontoptimize) até a biblioteca específica que a incluiu.

Pergunte ao R8: por que você está guardando isso?

Se uma classe que você esperava que fosse removida ainda estiver no app, o R8 pode explicar o motivo. Basta adicionar esta regra:

# Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

Durante o build, o R8 vai mostrar a cadeia exata de referências que fez com que ele mantivesse essa classe, permitindo que você rastreie a referência e ajuste suas regras.

Para um guia completo, consulte a seção Resolver problemas do R8.

Próximas etapas

O R8 é uma ferramenta poderosa para melhorar o desempenho do app Android. A eficácia dele depende de uma compreensão correta da operação como um mecanismo de análise estática.

Ao escrever regras específicas no nível do membro, usar ancestrais e anotações e escolher cuidadosamente as opções de retenção certas, você pode preservar exatamente o que é necessário. A prática mais avançada é eliminar totalmente a necessidade de regras, escolhendo bibliotecas modernas baseadas em codegen em vez das predecessoras baseadas em reflexão.

Enquanto acompanha a Semana de Destaque de Performance, confira o vídeo de hoje no YouTube e continue com nosso desafio do R8. Use #optimizationEnabled para qualquer dúvida sobre como ativar ou resolver problemas do R8. Estamos aqui para ajudar.

É hora de conferir os benefícios por conta própria.

Ative o modo completo do R8 no seu app hoje mesmo.

  1. Siga nossos guias para desenvolvedores para começar: Ativar a otimização de apps.
  2. Verifique se você ainda usa proguard-android.txt e substitua por proguard-android-optimize.txt.
  3. Em seguida, meça o impacto. Não basta sentir a diferença, verifique. Meça os ganhos de desempenho adaptando o código do nosso app de exemplo da Macrobenchmark no GitHub para medir os tempos de inicialização antes e depois.

Temos certeza de que você vai notar uma melhora significativa no desempenho do seu app.

Aproveite para usar a hashtag #AskAndroid para enviar suas perguntas. Durante a semana, nossos especialistas monitoram e respondem às suas perguntas.

Fique de olho no que vem por aí amanhã, quando vamos falar sobre a otimização guiada por perfil com perfis de referência e de inicialização, compartilhar como o desempenho de renderização do Compose melhorou nas últimas versões e compartilhar considerações de desempenho para o trabalho em segundo plano.

Escrito por:
Continue lendo