Ajouter des règles de conservation

De manière générale, une règle de conservation spécifie une classe (ou une sous-classe ou une implémentation), puis les membres (méthodes, constructeurs ou champs) de cette classe à conserver.

La syntaxe générale d'une règle de conservation est la suivante. Toutefois, certaines options de conservation n'acceptent pas l'option keep_option_modfier.


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

Voici un exemple de règle de conservation qui utilise keepclassmembers comme option de conservation, allowoptimization comme modificateur et conserve someSpecificMethod() à partir de com.example.MyClass :

-keepclassmembers,allowoptimization class com.example.MyClass {
  void someSpecificMethod();
}

Option "Conserver"

L'option "Conserver" est la première partie de votre règle de conservation. Il spécifie les aspects d'une classe à préserver. Six options de conservation sont disponibles : keep, keepclassmembers, keepclasseswithmembers, keepnames, keepclassmembernames et keepclasseswithmembernames.

Le tableau suivant décrit ces options de conservation :

Option Keep Description
keepclassmembers Conserve uniquement les membres spécifiés si la classe existe après l'optimisation.
keep Conserve les classes et les membres (champs et méthodes) spécifiés, en les empêchant d'être optimisés.

Remarque : keep ne doit généralement être utilisé qu'avec les modificateurs d'option "keep", car keep seul empêche toute optimisation des classes correspondantes.
keepclasseswithmembers Conserve une classe et ses membres spécifiés uniquement si la classe comporte tous les membres de la spécification de classe.
keepclassmembernames Empêche le renommage des membres spécifiés d'une classe, mais n'empêche pas la suppression de la classe ou de ses membres.

Remarque : La signification de cette option est souvent mal comprise. Pensez à utiliser l'option équivalente -keepclassmembers,allowshrinking à la place.
keepnames Empêche le renommage des cours et de leurs membres, mais ne les empêche pas d'être supprimés entièrement s'ils sont considérés comme inutilisés.

Remarque : La signification de cette option est souvent mal comprise. Pensez à utiliser l'option équivalente -keep,allowshrinking à la place.
keepclasseswithmembernames Empêche le renommage des classes et de leurs membres spécifiés, mais uniquement si les membres existent dans le code final. Elle n'empêche pas la suppression de code.

Remarque : La signification de cette option est souvent mal comprise. Pensez à utiliser l'option équivalente -keepclasseswithmembers,allowshrinking à la place.

Choisir la bonne option de conservation

Il est essentiel de choisir la bonne option de conservation pour déterminer l'optimisation appropriée pour votre application. Certaines options de conservation réduisent le code (processus par lequel le code non référencé est supprimé), tandis que d'autres l'obfusquent ou le renomment. Le tableau suivant indique les actions des différentes options de conservation :

Option Keep Classes de réduction Obscurcit les classes Réduire le nombre de membres Masque les membres
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

Modificateur d'option "Conserver"

Un modificateur d'option de conservation permet de contrôler le champ d'application et le comportement d'une règle de conservation. Vous pouvez ajouter zéro ou plusieurs modificateurs d'option de conservation à votre règle de conservation.

Le tableau suivant décrit les valeurs possibles pour un modificateur d'option de conservation :

Valeur Description
allowoptimization Permet d'optimiser les éléments spécifiés. Toutefois, les éléments spécifiés ne sont pas renommés ni supprimés.
allowobfucastion Permet de renommer les éléments spécifiés. Toutefois, les éléments ne sont pas supprimés ni optimisés.
allowshrinking Permet de supprimer les éléments spécifiés si R8 ne trouve aucune référence à ceux-ci. Toutefois, les éléments ne sont pas renommés ni optimisés d'une autre manière.
includedescriptorclasses Indique à R8 de conserver toutes les classes qui apparaissent dans les descripteurs des méthodes (types de paramètres et types renvoyés) et des champs (types de champs) conservés.
allowaccessmodification Permet à R8 de modifier (généralement d'élargir) les modificateurs d'accès (public, private, protected) des classes, des méthodes et des champs pendant le processus d'optimisation.
allowrepackage Permet à R8 de déplacer des classes dans différents packages, y compris le package racine par défaut.

Spécification de la classe

Vous devez spécifier une classe, une superclasse ou une interface implémentée dans une règle de conservation. Toutes les classes, y compris celles de l'espace de noms java.lang comme java.lang.String, doivent être spécifiées à l'aide de leur nom Java complet. Pour comprendre les noms à utiliser, inspectez le bytecode à l'aide des outils décrits dans Obtenir les noms Java générés.

L'exemple suivant montre comment spécifier la classe MaterialButton :

  • Correct : com.google.android.material.button.MaterialButton
  • Incorrect : MaterialButton

Les spécifications de classe indiquent également les membres d'une classe à conserver. La règle suivante conserve la classe MaterialButton et tous ses membres :

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

Sous-classes et implémentations

Pour cibler une sous-classe ou une classe qui implémente une interface, utilisez respectivement extend et implements.

Par exemple, si vous disposez de la classe Bar avec la sous-classe Foo comme suit :

class Foo : Bar()

La règle de conservation suivante préserve toutes les sous-classes de Bar. Notez que la règle de conservation n'inclut pas la superclasse Bar elle-même.

-keep class * extends Bar

Si vous disposez d'une classe Foo qui implémente Bar :

class Foo : Bar

La règle de conservation suivante préserve toutes les classes qui implémentent Bar. Notez que la règle de conservation n'inclut pas l'interface Bar elle-même.

-keep class * implements Bar

Modificateur d'accès

Vous pouvez spécifier des modificateurs d'accès tels que public, private, static et final pour rendre vos règles de conservation plus précises.

Par exemple, la règle suivante conserve toutes les classes public dans le package api et ses sous-packages, ainsi que tous les membres publics et protégés de ces classes.

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

Vous pouvez également utiliser des modificateurs pour les membres d'une classe. Par exemple, la règle suivante ne conserve que les méthodes public static d'une classe Utils :

-keep class com.example.Utils {
    public static void *(...);
}

Modificateurs spécifiques à Kotlin

R8 n'est pas compatible avec les modificateurs spécifiques à Kotlin, tels que internal et suspend. Suivez les consignes ci-dessous pour conserver ces champs.

  • Pour conserver une classe, une méthode ou un champ internal, traitez-le comme public. Prenons par exemple la source Kotlin suivante :

    package com.example
    internal class ImportantInternalClass {
      internal f: Int
      internal fun m() {}
    }
    

    Les classes, méthodes et champs internal sont public dans les fichiers .class produits par le compilateur Kotlin. Vous devez donc utiliser le mot clé public, comme indiqué dans l'exemple suivant :

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • Lorsqu'un membre suspend est compilé, sa signature compilée doit correspondre à celle de la règle Keep.

    Par exemple, si vous avez la fonction fetchUser définie comme indiqué dans l'extrait suivant :

    suspend fun fetchUser(id: String): User
    

    Une fois compilée, sa signature dans le bytecode se présente comme suit :

    public final Object fetchUser(String id, Continuation<? super User> continuation);
    

    Pour écrire une règle de conservation pour cette fonction, vous devez faire correspondre cette signature compilée ou utiliser ....

    Voici un exemple d'utilisation de la signature compilée :

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(java.lang.String,  kotlin.coroutines.Continuation);
    }
    

    Voici un exemple d'utilisation de ... :

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(...);
    }
    

Spécification des membres

La spécification de classe inclut éventuellement les membres de la classe à conserver. Si vous spécifiez un ou plusieurs membres pour un cours, la règle ne s'applique qu'à ces membres.

Par exemple, pour conserver une classe spécifique et tous ses membres, utilisez la commande suivante :

-keep class com.myapp.MyClass { *; }

Pour ne conserver que la classe et non ses membres, utilisez la commande suivante :

-keep class com.myapp.MyClass

La plupart du temps, vous souhaiterez spécifier certains membres. Par exemple, l'exemple suivant conserve le champ public text et la méthode publique updateText() dans la classe MyClass.

-keep class com.myapp.MyClass {
    public java.lang.String text;
    public void updateText(java.lang.String);
}

Pour conserver tous les champs et méthodes publics, consultez l'exemple suivant :

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

Méthodes

La syntaxe permettant de spécifier une méthode dans la spécification de membre pour une règle de conservation est la suivante :

[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);

Par exemple, la règle de conservation suivante conserve une méthode publique appelée setLabel() qui renvoie une valeur nulle et accepte un String.

-keep class com.example.MyView {
    public void setLabel(java.lang.String);
}

Vous pouvez utiliser <methods> comme raccourci pour faire correspondre toutes les méthodes d'une classe comme suit :

-keep class com.example.MyView {
    <methods>;
}

Pour savoir comment spécifier des types pour les types de renvoi et les types de paramètres, consultez Types.

Constructeurs

Pour spécifier un constructeur, utilisez <init>. La syntaxe permettant de spécifier un constructeur dans la spécification de membre pour une règle Keep est la suivante :

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

Par exemple, la règle de conservation suivante conserve un constructeur View personnalisé qui accepte un Context et un AttributeSet.

-keep class com.example.ui.MyCustomView {
    public <init>(android.content.Context, android.util.AttributeSet);
}

Pour conserver tous les constructeurs publics, utilisez l'exemple suivant comme référence :

-keep class com.example.ui.MyCustomView {
    public <init>(...);
}

Champs

La syntaxe permettant de spécifier un champ dans la spécification de membre pour une règle de conservation est la suivante :

[<access_modifier>...] [<type>] <field_name>;

Par exemple, la règle de conservation suivante conserve un champ de chaîne privée appelé userId et un champ d'entier statique public appelé STATUS_ACTIVE :

-keep class com.example.models.User {
    private java.lang.String userId;
    public static int STATUS_ACTIVE;
}

Vous pouvez utiliser <fields> comme raccourci pour faire correspondre tous les champs d'une classe comme suit :

-keep class com.example.models.User {
    <fields>;
}

Fonctions au niveau du package

Pour référencer une fonction Kotlin définie en dehors d'une classe (communément appelée fonction de premier niveau), veillez à utiliser le nom Java généré pour la classe ajoutée implicitement par le compilateur Kotlin. Le nom de la classe correspond au nom du fichier Kotlin auquel est ajouté Kt. Par exemple, si vous avez un fichier Kotlin appelé MyClass.kt défini comme suit :

package com.example.myapp.utils

// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
    return email.contains("@")
}

Pour écrire une règle de conservation pour la fonction isEmailValid, la spécification de classe doit cibler la classe générée MyClassKt :

-keep class com.example.myapp.utils.MyClassKt {
    public static boolean isEmailValid(java.lang.String);
}

Types

Cette section explique comment spécifier les types de retour, les types de paramètres et les types de champs dans les spécifications des membres des règles Keep. N'oubliez pas d'utiliser les noms Java générés pour spécifier les types s'ils sont différents du code source Kotlin.

Types primitifs

Pour spécifier un type primitif, utilisez son mot clé Java. R8 reconnaît les types primitifs suivants : boolean, byte, short, char, int, long, float, double.

Voici un exemple de règle avec un type primitif :

# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
    public void setValues(int, float);
}

Types génériques

Lors de la compilation, le compilateur Kotlin/Java efface les informations sur les types génériques. Par conséquent, lorsque vous écrivez des règles de conservation impliquant des types génériques, vous devez cibler la représentation compilée de votre code, et non le code source d'origine. Pour en savoir plus sur la façon dont les types génériques sont modifiés, consultez Effacement de type.

Par exemple, si vous avez le code suivant avec un type générique illimité défini dans Box.kt :

package com.myapp.data

class Box<T>(val item: T) {
    fun getItem(): T {
        return item
    }
}

Après l'effacement de type, T est remplacé par Object. Pour conserver le constructeur et la méthode de classe, votre règle doit utiliser java.lang.Object à la place de T générique.

Voici un exemple de règle de conservation :

# 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();
}

Si vous disposez du code suivant avec un type générique limité dans NumberBox.kt :

package com.myapp.data

// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)

Dans ce cas, l'effacement de type remplace T par sa limite, java.lang.Number.

Voici un exemple de règle de conservation :

-keep class com.myapp.data.NumberBox {
    public init(java.lang.Number);
}

Lorsque vous utilisez des types génériques spécifiques à l'application comme classe de base, il est nécessaire d'inclure également des règles de conservation pour les classes de base.

Par exemple, pour le code suivant :

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

Vous pouvez utiliser une règle Keep avec includedescriptorclasses pour conserver à la fois la classe UnpackOptions et la méthode de classe Box avec une seule règle, comme suit :

-keep,includedescriptorclasses class com.myapp.data.Box {
    public <init>(com.myapp.data.UnpackOptions);
}

Pour conserver une fonction spécifique qui traite une liste d'objets, vous devez écrire une règle qui correspond précisément à la signature de la fonction. Notez que, comme les types génériques sont effacés, un paramètre tel que List<Product> est considéré comme java.util.List.

Par exemple, si vous disposez d'une classe utilitaire avec une fonction qui traite une liste d'objets Product comme suit :

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)

Vous pouvez utiliser la règle keep suivante pour protéger uniquement la fonction processProducts :

-keep class com.myapp.utils.DataProcessor {
    public void processProducts(java.util.List);
}

Types de tableaux

Spécifiez un type de tableau en ajoutant [] au type de composant pour chaque dimension du tableau. Cela s'applique aux types de classe et aux types primitifs.

  • Tableau de classes unidimensionnel : java.lang.String[]
  • Tableau primitif bidimensionnel : int[][]

Par exemple, si vous disposez du code suivant :

package com.example.data

class ImageProcessor {
  fun process(): ByteArray {
    // process image to return a byte array
  }
}

Vous pouvez utiliser la règle de conservation suivante :

# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
    public byte[] process();
}

Caractères génériques

Le tableau suivant montre comment utiliser des caractères génériques pour appliquer des règles de conservation à plusieurs classes ou membres correspondant à un certain modèle.

Caractère générique S'applique aux cours ou aux membres Description
** Les deux Le plus couramment utilisé. Correspond à n'importe quel nom de type, y compris à n'importe quel nombre de séparateurs de package. Cela est utile pour faire correspondre toutes les classes d'un package et de ses sous-packages.
* Les deux Pour les spécifications de classe, correspond à n'importe quelle partie d'un nom de type qui ne contient pas de séparateurs de package (.)
Pour les spécifications de membre, correspond à n'importe quel nom de méthode ou de champ. Lorsqu'il est utilisé seul, il s'agit également d'un alias pour **.
? Les deux Correspond à n'importe quel caractère d'un nom de cours ou de membre.
*** Membres Correspond à n'importe quel type, y compris les types primitifs (comme int), les types de classe (comme java.lang.String) et les types de tableau de n'importe quelle dimension (comme byte[][]).
... Membres Correspond à n'importe quelle liste de paramètres pour une méthode.
% Membres Correspond à n'importe quel type primitif (tel que "int", "float", "boolean" ou autres).

Voici quelques exemples d'utilisation des caractères génériques spéciaux :

  • Si vous avez plusieurs méthodes portant le même nom qui acceptent différents types primitifs en entrée, vous pouvez utiliser % pour écrire une règle de conservation qui les conserve toutes. Par exemple, cette classe DataStore comporte plusieurs méthodes setValue :

    class DataStore {
        fun setValue(key: String, value: Int) { ... }
        fun setValue(key: String, value: Boolean) { ... }
        fun setValue(key: String, value: Float) { ... }
    }
    

    La règle de conservation suivante conserve toutes les méthodes :

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • Si vous avez plusieurs classes dont les noms ne diffèrent que d'un caractère, utilisez ? pour écrire une règle de conservation qui les conserve toutes. Par exemple, si vous disposez des classes suivantes :

    com.example.models.UserV1 {...}
    com.example.models.UserV2 {...}
    com.example.models.UserV3 {...}
    

    La règle de conservation suivante conserve toutes les classes :

    -keep class com.example.models.UserV?
    
  • Pour faire correspondre les classes Example et AnotherExample (si elles étaient des classes de niveau racine), mais pas com.foo.Example, utilisez la règle de conservation suivante :

    -keep class *Example
    
  • Si vous utilisez * seul, il sert d'alias pour **. Par exemple, les règles de conservation suivantes sont équivalentes :

    -keepclasseswithmembers class * { public static void main(java.lang.String[];) }
    
    -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
    

Inspecter les noms Java générés

Lorsque vous écrivez des règles de conservation, vous devez spécifier les classes et autres types de référence en utilisant leurs noms après leur compilation en bytecode Java (consultez Spécification de classe et Types pour obtenir des exemples). Pour vérifier les noms Java générés pour votre code, utilisez l'un des outils suivants dans Android Studio :

  • Analyseur d'APK
  • Ouvrez le fichier source Kotlin, puis inspectez le bytecode en accédant à Tools > Kotlin > Show Kotlin Bytecode > Decompile (Outils > Kotlin > Afficher le bytecode Kotlin > Décompiler).