Aggiungi regole di conservazione

A livello generale, una regola di conservazione specifica una classe (o una sottoclasse o un'implementazione) e poi i membri (metodi, costruttori o campi) all'interno di quella classe da conservare.

La sintassi generale per una regola di conservazione è la seguente, tuttavia alcune opzioni di conservazione non accettano keep_option_modfier facoltativo.


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

Di seguito è riportato un esempio di regola di conservazione che utilizza keepclassmembers come opzione di conservazione, allowoptimization come modificatore e conserva someSpecificMethod() da com.example.MyClass:

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

Opzione Mantieni

L'opzione di conservazione è la prima parte della regola di conservazione. Specifica gli aspetti di una classe da conservare. Esistono sei diverse opzioni di conservazione, ovvero keep, keepclassmembers, keepclasseswithmembers, keepnames, keepclassmembernames, keepclasseswithmembernames.

La tabella seguente descrive queste opzioni di conservazione:

Opzione Keep Descrizione
keepclassmembers Conserva solo i membri specificati se la classe esiste dopo l'ottimizzazione.
keep Conserva le classi specificate e i membri specificati (campi e metodi), impedendone l'ottimizzazione.

Nota: keep in genere deve essere utilizzato solo con i modificatori dell'opzione di conservazione perché keep da solo impedisce qualsiasi tipo di ottimizzazione delle classi corrispondenti.
keepclasseswithmembers Conserva un corso e i relativi membri specificati solo se il corso ha tutti i membri della specifica del corso.
keepclassmembernames Impedisce la ridenominazione dei membri del corso specificati, ma non impedisce la rimozione del corso o dei suoi membri.

Nota:il significato di questa opzione viene spesso frainteso. Ti consigliamo di utilizzare l'opzione equivalente -keepclassmembers,allowshrinking.
keepnames Impedisce la ridenominazione dei corsi e dei relativi membri, ma non la loro rimozione completa se vengono considerati inutilizzati.

Nota:il significato di questa opzione viene spesso frainteso. Ti consigliamo di utilizzare l'opzione equivalente -keep,allowshrinking.
keepclasseswithmembernames Impedisce la ridenominazione delle classi e dei relativi membri specificati, ma solo se i membri esistono nel codice finale. Non impedisce la rimozione del codice.

Nota:il significato di questa opzione viene spesso frainteso. Ti consigliamo di utilizzare l'opzione equivalente -keepclasseswithmembers,allowshrinking.

Scegliere l'opzione di conservazione giusta

La scelta dell'opzione di conservazione corretta è fondamentale per determinare l'ottimizzazione giusta per la tua app. Alcune opzioni di conservazione riducono il codice, un processo mediante il quale viene rimosso il codice non referenziato, mentre altre offuscano o ridenominano il codice. La tabella seguente indica le azioni delle varie opzioni di conservazione:

Opzione Keep Riduce le classi Offusca le classi Riduce i membri Offusca i membri
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

Modificatore dell'opzione Mantieni

Un modificatore dell'opzione di conservazione viene utilizzato per controllare l'ambito e il comportamento di una regola di conservazione. Puoi aggiungere zero o più modificatori dell'opzione di conservazione alla regola di conservazione.

I possibili valori per un modificatore dell'opzione di conservazione sono descritti nella tabella seguente:

Valore Descrizione
allowoptimization Consente l'ottimizzazione degli elementi specificati. Tuttavia, gli elementi specificati non vengono rinominati o rimossi.
allowobfucastion Consente di rinominare gli elementi specificati. Tuttavia, gli elementi non vengono rimossi o ottimizzati in altro modo.
allowshrinking Consente la rimozione degli elementi specificati se R8 non trova riferimenti. Tuttavia, gli elementi non vengono rinominati o ottimizzati in altro modo.
includedescriptorclasses Indica a R8 di conservare tutte le classi che compaiono nei descrittori dei metodi (tipi di parametri e tipi restituiti) e dei campi (tipi di campi) che vengono conservati.
allowaccessmodification Consente a R8 di modificare (in genere ampliare) i modificatori di accesso (public, private, protected) di classi, metodi e campi durante il processo di ottimizzazione.
allowrepackage Consente a R8 di spostare le classi in pacchetti diversi, incluso il pacchetto predefinito (root).

Specifica della classe

Devi specificare una classe, una superclasse o un'interfaccia implementata come parte di una regola keep. Tutte le classi, incluse quelle dello spazio dei nomi java.lang come java.lang.String, devono essere specificate utilizzando il nome Java completo. Per comprendere i nomi da utilizzare, ispeziona il bytecode utilizzando gli strumenti descritti in Recuperare i nomi Java generati.

L'esempio seguente mostra come specificare la classe MaterialButton:

  • Corretto: com.google.android.material.button.MaterialButton
  • Errato: MaterialButton

Le specifiche della classe specificano anche i membri all'interno di una classe che devono essere mantenuti. La seguente regola mantiene la classe MaterialButton e tutti i relativi membri:

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

Sottoclassi e implementazioni

Per scegliere come target una sottoclasse o una classe che implementa un'interfaccia, utilizza rispettivamente extend e implements.

Ad esempio, se hai la classe Bar con la sottoclasse Foo come segue:

class Foo : Bar()

La seguente regola di conservazione conserva tutte le sottoclassi di Bar. Tieni presente che la regola di conservazione non include la superclasse Bar stessa.

-keep class * extends Bar

Se hai la classe Foo che implementa Bar:

class Foo : Bar

La seguente regola di conservazione conserva tutte le classi che implementano Bar. Tieni presente che la regola di conservazione non include l'interfaccia Bar stessa.

-keep class * implements Bar

Modificatore di accesso

Puoi specificare modificatori di accesso come public, private, static e final per rendere più precise le regole di conservazione.

Ad esempio, la seguente regola mantiene tutte le classi public all'interno del pacchetto api e dei relativi pacchetti secondari, nonché tutti i membri pubblici e protetti di queste classi.

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

Puoi anche utilizzare modificatori per i membri all'interno di una classe. Ad esempio, la seguente regola mantiene solo i metodi public static di una classe Utils:

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

Modificatori specifici di Kotlin

R8 non supporta modificatori specifici di Kotlin come internal e suspend. Segui le seguenti linee guida per conservare questi campi.

  • Per mantenere una classe, un metodo o un campo internal, consideralo pubblico. Ad esempio, considera il seguente codice sorgente Kotlin:

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

    Le classi, i metodi e i campi internal sono public nei file .class prodotti dal compilatore Kotlin, quindi devi utilizzare la parola chiave public come mostrato nell'esempio seguente:

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • Quando viene compilato un membro suspend, la relativa firma compilata viene abbinata alla regola di conservazione.

    Ad esempio, se hai definito la funzione fetchUser come mostrato nello snippet seguente:

    suspend fun fetchUser(id: String): User
    

    Una volta compilata, la sua firma nel bytecode ha il seguente aspetto:

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

    Per scrivere una regola di conservazione per questa funzione, devi corrispondere a questa firma compilata o utilizzare ....

    Di seguito è riportato un esempio di utilizzo della firma compilata:

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

    Ecco un esempio di utilizzo di ...:

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

Specifica del membro

La specifica della classe include facoltativamente i membri della classe da conservare. Se specifichi uno o più membri per un corso, la regola si applica solo a questi membri.

Ad esempio, per conservare una classe specifica e tutti i suoi membri, utilizza il seguente comando:

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

Per conservare solo il corso e non i suoi membri, utilizza quanto segue:

-keep class com.myapp.MyClass

La maggior parte delle volte, vorrai specificare alcuni membri. Ad esempio, il seguente esempio mantiene il campo pubblico text e il metodo pubblico updateText() all'interno della classe MyClass.

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

Per conservare tutti i campi pubblici e i metodi pubblici, vedi l'esempio seguente:

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

Metodi

La sintassi per specificare un metodo nella specifica del membro per una regola di conservazione è la seguente:

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

Ad esempio, la seguente regola di conservazione mantiene un metodo pubblico chiamato setLabel() che restituisce void e accetta un String.

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

Puoi utilizzare <methods> come scorciatoia per abbinare tutti i metodi in una classe come segue:

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

Per scoprire di più su come specificare i tipi per i tipi restituiti e i tipi di parametri, vedi Tipi.

Costruttori

Per specificare un costruttore, utilizza <init>. La sintassi per specificare un costruttore nella specifica del membro per una regola di conservazione è la seguente:

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

Ad esempio, la seguente regola di conservazione mantiene un costruttore View personalizzato che accetta un Context e un AttributeSet.

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

Per conservare tutti i costruttori pubblici, utilizza il seguente esempio come riferimento:

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

Campi

La sintassi per specificare un campo nella specifica del membro per una regola di conservazione è la seguente:

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

Ad esempio, la seguente regola di conservazione mantiene un campo stringa privato denominato userId e un campo intero statico pubblico denominato STATUS_ACTIVE:

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

Puoi utilizzare <fields> come scorciatoia per abbinare tutti i campi di una classe come segue:

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

Funzioni a livello di pacchetto

Per fare riferimento a una funzione Kotlin definita al di fuori di una classe (comunemente chiamate funzioni di primo livello), assicurati di utilizzare il nome Java generato per la classe aggiunta implicitamente dal compilatore Kotlin. Il nome della classe è il nome file Kotlin con Kt aggiunto. Ad esempio, se hai un file Kotlin denominato MyClass.kt definito come segue:

package com.example.myapp.utils

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

Per scrivere una regola di conservazione per la funzione isEmailValid, la specifica della classe deve avere come target la classe generata MyClassKt:

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

Tipi

Questa sezione descrive come specificare i tipi restituiti, i tipi di parametri e i tipi di campi nelle specifiche dei membri delle regole di conservazione. Ricorda di utilizzare i nomi Java generati per specificare i tipi se sono diversi dal codice sorgente Kotlin.

Tipi primitivi

Per specificare un tipo primitivo, utilizza la parola chiave Java corrispondente. R8 riconosce i seguenti tipi primitivi: boolean, byte, short, char, int, long, float, double.

Di seguito è riportata una regola di esempio con un tipo primitivo:

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

Tipi generici

Durante la compilazione, il compilatore Kotlin/Java cancella le informazioni sul tipo generico, quindi quando scrivi regole di conservazione che coinvolgono tipi generici, devi scegliere come target la rappresentazione compilata del codice e non il codice sorgente originale. Per scoprire di più su come vengono modificati i tipi generici, consulta Cancellazione del tipo.

Ad esempio, se hai il seguente codice con un tipo generico senza limiti definito in Box.kt:

package com.myapp.data

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

Dopo l'eliminazione del tipo, T viene sostituito da Object. Per mantenere il costruttore e il metodo della classe, la regola deve utilizzare java.lang.Object al posto di T generico.

Una regola di conservazione di esempio potrebbe essere la seguente:

# 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 hai il seguente codice con un tipo generico limitato in NumberBox.kt:

package com.myapp.data

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

In questo caso, la cancellazione del tipo sostituisce T con il relativo limite, java.lang.Number.

Una regola di conservazione di esempio potrebbe essere la seguente:

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

Quando utilizzi tipi generici specifici dell'app come classe base, è necessario includere anche le regole di conservazione per le classi base.

Ad esempio, per il seguente codice:

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

Puoi utilizzare una regola di conservazione con includedescriptorclasses per conservare sia la classe UnpackOptions sia il metodo della classe Box con una sola regola, come segue:

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

Per conservare una funzione specifica che elabora un elenco di oggetti, devi scrivere una regola che corrisponda esattamente alla firma della funzione. Tieni presente che poiché i tipi generici vengono cancellati, un parametro come List<Product> viene visualizzato come java.util.List.

Ad esempio, se hai una classe di utilità con una funzione che elabora un elenco di oggetti Product nel seguente modo:

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)

Puoi utilizzare la seguente regola di conservazione per proteggere solo la funzione processProducts:

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

Tipi di array

Specifica un tipo di array aggiungendo [] al tipo di componente per ogni dimensione dell'array. Questo vale sia per i tipi di classe sia per i tipi primitivi.

  • Array di classi unidimensionale: java.lang.String[]
  • Array primitivo bidimensionale: int[][]

Ad esempio, se hai il seguente codice:

package com.example.data

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

Potresti utilizzare la seguente regola di conservazione:

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

Caratteri jolly

La seguente tabella mostra come utilizzare i caratteri jolly per applicare le regole di conservazione a più classi o membri che corrispondono a un determinato pattern.

Jolly Si applica a corsi o membri Descrizione
** Entrambi Utilizzato più di frequente. Corrisponde a qualsiasi nome di tipo, incluso qualsiasi numero di separatori di pacchetti. Questa opzione è utile per trovare corrispondenze per tutte le classi all'interno di un pacchetto e dei relativi pacchetti secondari.
* Entrambi Per le specifiche della classe, corrisponde a qualsiasi parte di un nome di tipo che non contiene separatori di pacchetti (.)
Per le specifiche dei membri, corrisponde a qualsiasi nome di metodo o campo. Se utilizzato da solo, è anche un alias di **.
? Entrambi Corrisponde a qualsiasi singolo carattere nel nome di una classe o di un membro.
*** Abbonati Corrisponde a qualsiasi tipo, inclusi i tipi primitivi (come int), i tipi di classe (come java.lang.String) e i tipi di array di qualsiasi dimensione (come byte[][]).
Abbonati Corrisponde a qualsiasi elenco di parametri per un metodo.
% Abbonati Corrisponde a qualsiasi tipo primitivo (ad esempio `int`, `float`, `boolean` o altri).

Ecco alcuni esempi di come utilizzare i caratteri jolly speciali:

  • Se hai più metodi con lo stesso nome che accettano diversi tipi primitivi come input, puoi utilizzare % per scrivere una regola di conservazione che li conservi tutti. Ad esempio, questa classe DataStore ha più metodi setValue:

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

    La seguente regola di conservazione mantiene tutti i metodi:

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • Se hai più corsi con nomi che variano di un carattere, utilizza ? per scrivere una regola di conservazione che li conservi tutti. Ad esempio, se hai le seguenti classi:

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

    La seguente regola di conservazione mantiene tutte le classi:

    -keep class com.example.models.UserV?
    
  • Per corrispondere alle classi Example e AnotherExample (se fossero classi di livello principale), ma non a com.foo.Example, utilizza la seguente regola di conservazione:

    -keep class *Example
    
  • Se utilizzi * da solo, funge da alias per **. Ad esempio, le seguenti regole di conservazione sono equivalenti:

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

Ispeziona i nomi Java generati

Quando scrivi regole di conservazione, devi specificare classi e altri tipi di riferimento utilizzando i loro nomi dopo la compilazione in bytecode Java (vedi Specifica della classe e Tipi per esempi). Per controllare quali sono i nomi Java generati per il tuo codice, utilizza uno dei seguenti strumenti in Android Studio:

  • Strumento di analisi APK
  • Con il file sorgente Kotlin aperto, esamina il bytecode andando su Strumenti > Kotlin > Mostra bytecode Kotlin > Decompila.