Adicionar regras de manutenção

Em um nível alto, 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 preservação é a seguinte, mas algumas opções de preservação não aceitam o keep_option_modfier opcional.


-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>

Confira um exemplo de regra de preservação que usa keepclassmembers como a opção de preservaçã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 o equivalente -keep,allowshrinking.
keepclasseswithmembernames Impede a renomeação de classes e dos membros especificados, mas apenas se eles existirem no código final. Ele não impede a remoção de 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, uma superclasse ou uma interface implementada como parte de uma regra de preservação. Todas as classes, incluindo as 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 Receber 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 precisam ser mantidos. A regra a seguir mantém a classe MaterialButton e todos os membros dela:

-keep class com.google.android.material.button.MaterialButton { *; }

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 preservaçã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 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 retenção mais precisas.

Por exemplo, a regra a seguir mantém todas as classes public no pacote api e nos subpacotes, além de todos os membros públicos e protegidos nessas classes.

-keep public class com.example.api.** { public protected *; }

Você também pode 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 internal são public nos arquivos .class produzidos pelo compilador Kotlin. Portanto, use a palavra-chave public conforme 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 fetchUser definida conforme mostrado no snippet a seguir:

    suspend fun fetchUser(id: String): User
    

    Quando 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.

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 manutenção é esta:

[<access_modifier>] <init>(parameter_types);

Por exemplo, a regra de preservaçã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 permanência é 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 manutençã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 Eliminação de tipos.

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) {
}

Você pode 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 preservaçã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 permanência que os mantenha. Por exemplo, esta classe DataStore tem vários métodos setValue:

    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 manutenção que as mantenha. 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 Example e AnotherExample (se elas fossem classes de nível raiz), mas não com.foo.Example, use a seguinte regra de permanência:

    -keep class *Example
    
  • Se você usar * sozinho, ele vai funcionar como um alias para **. Por exemplo, as seguintes regras de retenção 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.