Em um nível superior, uma regra de preservação especifica uma classe (ou subclasse ou implementação) e, em seguida, membros (métodos, construtores ou campos) dentro dessa classe para preservar.
A sintaxe geral de uma regra de retenção é a seguinte:
-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>
Confira um exemplo de regra de retenção que usa keepclassmembers como a opção de retenção, allowoptimization como o modificador e mantém someSpecificMethod() de com.example.MyClass:
-keepclassmembers,allowoptimization class com.example.MyClass {
void someSpecificMethod();
}
Opção "Manter"
A opção "manter" é a primeira parte da regra de retenção. Ele especifica quais aspectos
de uma classe preservar. Há seis opções de retenção diferentes: keep, keepclassmembers, keepclasseswithmembers, keepnames, keepclassmembernames e keepclasseswithmembernames.
A tabela a seguir descreve essas opções de retenção:
| Opção "Keep" | Description |
|---|---|
keepclassmembers |
Preserva apenas os membros especificados se a classe existir após a otimização. |
keep |
Preserva as classes e os membros (campos e métodos) especificados, impedindo que sejam otimizados. Observação: geralmente, keep só deve ser usado com modificadores de opção de manutenção porque keep por si só impede que otimizações de qualquer tipo ocorram em classes correspondentes. |
keepclasseswithmembers |
Preserva uma classe e os membros especificados somente se a classe tiver todos os membros da especificação. |
keepclassmembernames |
Impede a renomeação de membros de classe especificados, mas não impede a remoção da classe ou dos membros dela. Observação:o significado dessa opção costuma ser mal compreendido. Considere usar o equivalente -keepclassmembers,allowshrinking. |
keepnames |
Impede a mudança de nome de classes e membros, mas não impede que eles sejam removidos completamente se forem considerados não utilizados. Observação:o significado dessa opção costuma ser mal compreendido. Considere usar a opção equivalente -keep,allowshrinking. |
keepclasseswithmembernames |
Impede a renomeação de classes e dos membros especificados, mas apenas se os membros existirem no código final. Ele não impede a remoção do código. Observação:o significado dessa opção costuma ser mal compreendido. Considere usar a opção equivalente -keepclasseswithmembers,allowshrinking. |
Escolher a opção de retenção certa
Escolher a opção de manutenção certa é crucial para determinar a otimização adequada para seu app. Algumas opções de manutenção reduzem o código, um processo em que o código não referenciado é removido, enquanto outras ofuscam ou renomeiam o código. A tabela a seguir indica as ações das várias opções de manter:
| Opção "Keep" | Reduz classes | Ofusca classes | Reduz os membros | Ofusca membros |
|---|---|---|---|---|
keep |
||||
keepclassmembers |
||||
keepclasseswithmembers |
||||
keepnames |
||||
keepclassmembernames |
||||
keepclasseswithmembernames |
Modificador de opção "manter"
Um modificador de opção de manutenção é usado para controlar o escopo e o comportamento de uma regra de manutenção. É possível adicionar zero ou mais modificadores de opção de retenção à sua regra de retenção.
Os valores possíveis para um modificador de opção de manutenção são descritos na tabela a seguir:
| Valor | Descrição |
|---|---|
allowoptimization |
Permite a otimização dos elementos especificados. No entanto, os elementos especificados não são renomeados nem removidos. |
allowobfucastion |
Permite renomear os elementos especificados. No entanto, os elementos não são removidos nem otimizados de outra forma. |
allowshrinking |
Permite a remoção dos elementos especificados se o R8 não encontrar referências a eles. No entanto, os elementos não são renomeados nem otimizados de outra forma. |
includedescriptorclasses |
Instrui o R8 a manter todas as classes que aparecem nos descritores dos métodos (tipos de parâmetro e tipos de retorno) e campos (tipos de campo) que estão sendo mantidos. |
allowaccessmodification |
Permite que o R8 mude (normalmente amplie) os modificadores de acesso (public, private, protected) de classes, métodos e campos durante o processo de otimização. |
allowrepackage |
Permite que o R8 mova classes para pacotes diferentes, incluindo o pacote padrão (raiz). |
Especificação de classe
É necessário especificar uma classe (incluindo interface, enumeração e classes de anotação) em
cada regra de preservação. Você pode restringir a regra com base em anotações ou especificando uma superclasse ou interface implementada. Todas as classes, incluindo
classes do namespace java.lang, como java.lang.String, precisam ser
especificadas usando o nome Java totalmente qualificado. Para entender os nomes que devem ser usados, inspecione o bytecode usando as ferramentas descritas em Inspecionar nomes Java gerados.
O exemplo a seguir mostra como especificar a classe MaterialButton:
- Correto:
com.google.android.material.button.MaterialButton - Incorreto:
MaterialButton
As especificações de classe também especificam os membros dentro de uma classe que
deve ser mantida. A regra a seguir mantém a classe MaterialButton e todos os membros dela:
-keep class com.google.android.material.button.MaterialButton { *; }
Especificar classes com base em anotações
Para especificar classes com base nas anotações, adicione um símbolo @ como prefixo ao nome Java totalmente qualificado da anotação. Exemplo:
-keep class @com.example.MyAnnotation com.example.MyClass
Se uma regra de manutenção tiver mais de uma anotação, ela vai manter as classes que tiverem todas as anotações listadas. É possível listar várias anotações, mas a regra só será aplicada se a classe tiver todas as anotações listadas. Por exemplo, a regra a seguir mantém todas as classes anotadas por Annotation1 e Annotation2.
-keep class @com.example.Annotation1 @com.example.Annotation2 *
Especificar subclasses e implementações
Para segmentar uma subclasse ou classe que implementa uma interface, use extend e implements, respectivamente.
Por exemplo, se você tiver a classe Bar com a subclasse Foo da seguinte maneira:
class Foo : Bar()
A regra de manutenção a seguir preserva todas as subclasses de Bar. A regra de
preservação não inclui a superclasse Bar.
-keep class * extends Bar
Se você tiver a classe Foo que implementa a interface Bar:
class Foo : Bar
A regra de preservação a seguir preserva todas as classes que implementam Bar. A regra de preservação não inclui a interface Bar.
-keep class * implements Bar
Modificador de acesso
É possível especificar modificadores de acesso, como public, private, static e
final, para tornar as regras de permanência mais precisas.
Por exemplo, a regra a seguir mantém todas as classes public no pacote api e nos subpacotes dele, além de todos os membros públicos e protegidos nessas classes.
-keep public class com.example.api.** { public protected *; }
Também é possível usar modificadores para os membros de uma classe. Por exemplo, a regra a seguir mantém apenas os métodos public static de uma classe Utils:
-keep class com.example.Utils {
public static void *(...);
}
Modificadores específicos do Kotlin
O R8 não é compatível com modificadores específicos do Kotlin, como internal e suspend.
Siga estas diretrizes para manter esses campos.
Para manter uma classe, um método ou um campo
internal, trate-o como público. Por exemplo, considere a seguinte origem Kotlin:package com.example internal class ImportantInternalClass { internal f: Int internal fun m() {} }As classes, os métodos e os campos
internalsãopublicnos arquivos.classproduzidos pelo compilador Kotlin. Portanto, use a palavra-chavepublicconforme mostrado no exemplo a seguir:-keepclassmembers public class com.example.ImportantInternalClass { public int f; public void m(); }Quando um membro
suspendé compilado, corresponda à assinatura compilada na regra de permanência.Por exemplo, se você tiver a função
fetchUserdefinida conforme mostrado no snippet a seguir:suspend fun fetchUser(id: String): UserQuando compilada, a assinatura no bytecode fica assim:
public final Object fetchUser(String id, Continuation<? super User> continuation);Para escrever uma regra de preservação para essa função, é preciso corresponder a essa assinatura compilada ou usar
....Veja um exemplo de uso da assinatura compilada:
-keepclassmembers class com.example.repository.UserRepository { public java.lang.Object fetchUser(java.lang.String, kotlin.coroutines.Continuation); }Um exemplo de uso de
...é:-keepclassmembers class com.example.repository.UserRepository { public java.lang.Object fetchUser(...); }
Especificação de membro
A especificação de classe inclui opcionalmente os membros da classe a serem preservados. Se você especificar um ou mais membros para uma turma, a regra será aplicada apenas a eles.
Também é possível especificar membros com base nas anotações deles. Assim como nas classes, você
prefixa com o nome Java totalmente qualificado da anotação com @. Isso permite
manter apenas os membros de uma classe marcados com anotações
específicas. Por exemplo, para manter métodos e campos anotados com
@com.example.MyAnnotation:
-keep class com.example.MyClass {
@com.example.MyAnnotation <methods>;
@com.example.MyAnnotation <fields>;
}
É possível combinar isso com a correspondência de anotações no nível da classe para criar regras poderosas e segmentadas:
-keep class @com.example.ClassAnnotation * {
@com.example.MethodAnnotation <methods>;
@com.example.FieldAnnotation <fields>;
}
Isso mantém as classes anotadas com @ClassAnnotation e, nessas classes, mantém os métodos anotados por @MethodAnnotation e os campos anotados por @FieldAnnotation.
Considere usar regras de preservação com base em anotações sempre que possível. Essa abordagem fornece
um link explícito entre seu código e as regras de retenção, o que geralmente leva a configurações mais
robustas. A biblioteca de anotações androidx.annotation, por exemplo, usa esse mecanismo.
Exemplos
Por exemplo, para preservar uma classe específica e todos os membros dela, use o seguinte:
-keep class com.myapp.MyClass { *; }
Para preservar apenas a classe e não os membros dela, use o seguinte:
-keep class com.myapp.MyClass
Na maioria das vezes, você vai querer especificar alguns membros. Por exemplo, o
exemplo a seguir mantém o campo público text e o método público
updateText() na classe MyClass.
-keep class com.myapp.MyClass {
public java.lang.String text;
public void updateText(java.lang.String);
}
Para manter todos os campos e métodos públicos, consulte o exemplo a seguir:
-keep public class com.example.api.ApiClient {
public *;
}
Métodos
A sintaxe para especificar um método na especificação de membro de uma regra de retenção é a seguinte:
[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);
Por exemplo, a regra de preservação a seguir mantém um método público chamado setLabel()
que retorna void e usa um String.
-keep class com.example.MyView {
public void setLabel(java.lang.String);
}
Use <methods> como um atalho para corresponder a todos os métodos em uma classe da seguinte forma:
-keep class com.example.MyView {
<methods>;
}
Para saber mais sobre como especificar tipos para tipos de retorno e tipos de parâmetros, consulte Tipos.
Construtores
Para especificar um construtor, use <init>. A sintaxe para especificar um construtor
na especificação de membro de uma regra de retenção é esta:
[<access_modifier>] <init>(parameter_types);
Por exemplo, a regra de retenção a seguir mantém um construtor View personalizado que
usa um Context e um AttributeSet.
-keep class com.example.ui.MyCustomView {
public <init>(android.content.Context, android.util.AttributeSet);
}
Para manter todos os construtores públicos, use o exemplo a seguir como referência:
-keep class com.example.ui.MyCustomView {
public <init>(...);
}
Campos
A sintaxe para especificar um campo na especificação de membro de uma regra de retenção é a seguinte:
[<access_modifier>...] [<type>] <field_name>;
Por exemplo, a regra de preservação a seguir mantém um campo de string particular chamado
userId e um campo de número inteiro estático público chamado STATUS_ACTIVE:
-keep class com.example.models.User {
private java.lang.String userId;
public static int STATUS_ACTIVE;
}
É possível usar <fields> como um atalho para corresponder a todos os campos em uma classe da seguinte maneira:
-keep class com.example.models.User {
<fields>;
}
Funções no nível do pacote
Para referenciar uma função Kotlin definida fora de uma classe (geralmente chamada de função de nível superior), use o nome Java gerado para a classe implicitamente adicionada pelo compilador Kotlin. O nome da classe é o nome do arquivo Kotlin com Kt anexado. Por exemplo, se você tiver um arquivo Kotlin chamado
MyClass.kt definido da seguinte maneira:
package com.example.myapp.utils
// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
return email.contains("@")
}
Para escrever uma regra de permanência para a função isEmailValid, a especificação de classe
precisa segmentar a classe gerada MyClassKt:
-keep class com.example.myapp.utils.MyClassKt {
public static boolean isEmailValid(java.lang.String);
}
Tipos
Esta seção descreve como especificar tipos de retorno, tipos de parâmetros e tipos de campos em especificações de membros de regras de permanência. Não se esqueça de usar os nomes Java gerados para especificar tipos se eles forem diferentes do código-fonte Kotlin.
Tipos primitivos
Para especificar um tipo primitivo, use a palavra-chave Java dele. O R8 reconhece os seguintes tipos primitivos: boolean, byte, short, char, int, long, float, double.
Confira um exemplo de regra com um tipo primitivo:
# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
public void setValues(int, float);
}
Tipos genéricos
Durante a compilação, o compilador Kotlin/Java apaga as informações de tipo genérico. Portanto, ao escrever regras de preservação que envolvem tipos genéricos, é necessário direcionar a representação compilada do código, e não o código-fonte original. Para saber mais sobre como os tipos genéricos são alterados, consulte Exclusão de tipo.
Por exemplo, se você tiver o seguinte código com um tipo genérico sem limites
definido em Box.kt:
package com.myapp.data
class Box<T>(val item: T) {
fun getItem(): T {
return item
}
}
Após a eliminação de tipo, o T é substituído por Object. Para manter o construtor e o método da classe, sua regra precisa usar java.lang.Object em vez do T genérico.
Um exemplo de regra de retenção seria:
# Keep the constructor and methods of the Box class.
-keep class com.myapp.data.Box {
public init(java.lang.Object);
public java.lang.Object getItem();
}
Se você tiver o seguinte código com um tipo genérico limitado em NumberBox.kt:
package com.myapp.data
// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)
Nesse caso, o apagamento de tipo substitui T pelo limite java.lang.Number.
Um exemplo de regra de retenção seria:
-keep class com.myapp.data.NumberBox {
public init(java.lang.Number);
}
Ao usar tipos genéricos específicos do app como uma classe de base, é necessário incluir regras de manutenção também para as classes de base.
Por exemplo, para o seguinte código:
package com.myapp.data
data class UnpackOptions(val useHighPriority: Boolean)
// The generic Box class with UnpackOptions as the bounded type
class Box<T: UnpackOptions>(val item: T) {
}
É possível usar uma regra de preservação com includedescriptorclasses para preservar a classe UnpackOptions e o método de classe Box com uma única regra da seguinte maneira:
-keep,includedescriptorclasses class com.myapp.data.Box {
public <init>(com.myapp.data.UnpackOptions);
}
Para manter uma função específica que processa uma lista de objetos, escreva uma regra que corresponda exatamente à assinatura da função. Como os tipos genéricos são apagados, um parâmetro como List<Product> é visto como java.util.List.
Por exemplo, se você tiver uma classe utilitária com uma função que processa uma lista de objetos Product da seguinte maneira:
package com.myapp.utils
import com.myapp.data.Product
import android.util.Log
class DataProcessor {
// This is the function we want to keep
fun processProducts(products: List<Product>) {
Log.d("DataProcessor", "Processing ${products.size} products.")
// Business logic ...
}
}
// The data class used in the list (from the previous example)
package com.myapp.data
data class Product(val id: String, val name: String)
Você pode usar a seguinte regra de preservação para proteger apenas a função processProducts:
-keep class com.myapp.utils.DataProcessor {
public void processProducts(java.util.List);
}
Tipos de matriz
Para especificar um tipo de matriz, adicione [] ao tipo de componente de cada dimensão da matriz. Isso se aplica a tipos de classe e primitivos.
- Matriz de classe unidimensional:
java.lang.String[] - Matriz primitiva bidimensional:
int[][]
Por exemplo, se você tiver o seguinte código:
package com.example.data
class ImageProcessor {
fun process(): ByteArray {
// process image to return a byte array
}
}
Você pode usar a seguinte regra de retenção:
# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
public byte[] process();
}
Caracteres curinga
A tabela a seguir mostra como usar caracteres curinga para aplicar regras de manutenção a várias classes ou membros que correspondem a um determinado padrão.
| Curinga | Aplica-se a turmas ou participantes | Descrição |
|---|---|---|
| ** | Ambos | Mais usado. Corresponde a qualquer nome de tipo, incluindo qualquer número de separadores de pacote. Isso é útil para corresponder a todas as classes em um pacote e seus subpacotes. |
| * | Ambos | Para especificações de classe, corresponde a qualquer parte de um nome de tipo que não contenha separadores de pacote (.) . Para especificações de membro, corresponde a qualquer nome de método ou campo. Quando usado sozinho, ele também é um alias de **. |
| ? | Ambos | Corresponde a qualquer caractere único em um nome de classe ou membro. |
| *** | Membros | Corresponde a qualquer tipo, incluindo tipos primitivos (como int), tipos de classe (como java.lang.String) e tipos de matriz de qualquer dimensão (como byte[][]). |
| … | Membros | Corresponde a qualquer lista de parâmetros de um método. |
| % | Membros | Corresponde a qualquer tipo primitivo, como "int", "float", "boolean" ou outros. |
Confira alguns exemplos de como usar os caracteres curinga especiais:
Se você tiver vários métodos com o mesmo nome que usam diferentes tipos primitivos como entradas, use
%para escrever uma regra de manutenção que os mantenha todos. Por exemplo, esta classeDataStoretem vários métodossetValue:class DataStore { fun setValue(key: String, value: Int) { ... } fun setValue(key: String, value: Boolean) { ... } fun setValue(key: String, value: Float) { ... } }A regra de preservação a seguir mantém todos os métodos:
-keep class com.example.DataStore { public void setValue(java.lang.String, %); }Se você tiver várias classes com nomes que variam em um caractere, use
?para escrever uma regra de preservação que mantenha todas elas. Por exemplo, se você tiver as seguintes classes:com.example.models.UserV1 {...} com.example.models.UserV2 {...} com.example.models.UserV3 {...}A regra de preservação a seguir mantém todas as classes:
-keep class com.example.models.UserV?Para corresponder às classes
ExampleeAnotherExample(se elas fossem classes de nível raiz), mas nãocom.foo.Example, use a seguinte regra de manutenção:-keep class *ExampleSe você usar * sozinho, ele vai funcionar como um alias para **. Por exemplo, as regras de retenção a seguir são equivalentes:
-keepclasseswithmembers class * { public static void main(java.lang.String[];) } -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
Inspecionar nomes Java gerados
Ao escrever regras de preservação, é necessário especificar classes e outros tipos de referência usando os nomes deles depois que são compilados em bytecode Java. Consulte Especificação de classe e Tipos para exemplos. Para verificar quais são os nomes Java gerados para seu código, use uma das seguintes ferramentas no Android Studio:
- APK Analyzer
- Com o arquivo de origem Kotlin aberto, inspecione o bytecode acessando Ferramentas > Kotlin > Mostrar bytecode Kotlin > Descompilar.