Добавить правила сохранения

В общих чертах, правило сохранения указывает класс (или подкласс, или реализацию), а затем члены этого класса — методы, конструкторы или поля — которые необходимо сохранить.

Общий синтаксис правила сохранения выглядит следующим образом:


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

Ниже приведён пример правила сохранения, в котором в качестве параметра сохранения используется keepclassmembers , в качестве модификатора allowoptimization , и сохраняется someSpecificMethod() из com.example.MyClass :

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

Сохранить вариант

Параметр keep — это первая часть вашего правила keep. Он определяет, какие аспекты класса следует сохранить. Существует шесть различных параметров keep, а именно: keep , keepclassmembers , keepclasseswithmembers , keepnames , keepclassmembernames , keepclasseswithmembernames .

В следующей таблице описаны эти варианты сохранения:

Сохранить вариант Описание
keepclassmembers Сохраняет указанные члены только в том случае, если класс существует после оптимизации .
keep Сохраняет указанные классы и указанные члены (поля и методы), предотвращая их оптимизацию.

Примечание : keep следует использовать, как правило, только с модификаторами keep, поскольку сам по себе keep предотвращает любые оптимизации в соответствующих классах.
keepclasseswithmembers Сохраняет класс и указанные в нем члены только в том случае, если класс содержит все члены, указанные в спецификации класса.
keepclassmembernames Предотвращает переименование указанных членов класса, но не препятствует удалению класса или его членов.

Примечание: значение этого параметра часто понимается неправильно; вместо него рекомендуется использовать эквивалентный параметр -keepclassmembers,allowshrinking .
keepnames Предотвращает переименование классов и их членов, но не препятствует их полному удалению, если они будут признаны неиспользуемыми.

Примечание: значение этого параметра часто понимается неправильно; рекомендуется использовать эквивалентный параметр -keep,allowshrinking .
keepclasseswithmembernames Предотвращает переименование классов и указанных в них членов, но только если эти члены существуют в итоговом коде. Не предотвращает удаление кода.

Примечание: значение этого параметра часто понимается неправильно; вместо него рекомендуется использовать эквивалентный параметр -keepclasseswithmembers,allowshrinking .

Выберите правильный вариант сохранения.

Выбор правильного параметра сохранения кода имеет решающее значение для определения оптимальной оптимизации вашего приложения. Некоторые параметры сохранения кода уменьшают размер кода, удаляя неиспользуемый код, в то время как другие обфусцируют или переименовывают код. В следующей таблице указаны действия различных параметров сохранения кода:

Сохранить вариант Уроки психотерапии Затуманивает классы Члены Shrinks Затуманивает членов
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

Модификатор сохранения опции

Модификатор параметра keep используется для управления областью действия и поведением правила keep. Вы можете добавить 0 или более модификаторов параметра keep к своему правилу keep.

Возможные значения модификатора параметра «сохранить» описаны в следующей таблице:

Ценить Описание
allowoptimization Позволяет оптимизировать указанные элементы. Однако указанные элементы не переименовываются и не удаляются.
allowobfucastion Позволяет переименовывать указанные элементы. Однако элементы не могут быть удалены или оптимизированы иным образом.
allowshrinking Позволяет удалять указанные элементы, если R8 не находит на них ссылок. Однако элементы не переименовываются и не оптимизируются каким-либо иным образом.
includedescriptorclasses Указывает R8 сохранять все классы, указанные в дескрипторах сохраняемых методов (типы параметров и возвращаемые типы) и полей (типы полей).
allowaccessmodification Позволяет R8 изменять (как правило, расширять) модификаторы доступа ( public , private , protected ) для классов, методов и полей в процессе оптимизации.
allowrepackage Позволяет R8 перемещать классы в разные пакеты, включая пакет по умолчанию (корневой пакет).

Спецификация класса

В каждом правиле сохранения необходимо указать класс (включая интерфейсы, перечисления и классы аннотаций). При желании можно ограничить правило на основе аннотаций, указав суперкласс или реализованный интерфейс, либо указав модификатор доступа для класса. Все классы, включая классы из пространства имен java.lang , такие как java.lang.String , должны быть указаны с использованием их полного Java-имени. Чтобы понять, какие имена следует использовать, просмотрите байт-код с помощью инструментов, описанных в разделе «Проверка сгенерированных Java-имен» .

В следующем примере показано, как следует указывать класс MaterialButton :

  • Правильно: com.google.android.material.button.MaterialButton
  • Неверно: MaterialButton

Спецификации классов также определяют, какие члены класса следует сохранять. Например, следующее правило сохраняет класс MyClass и метод someSpecificMethod() :

-keep class com.example.MyClass {
  void someSpecificMethod();
}

Указывайте классы на основе аннотаций.

Для указания классов на основе их аннотаций, перед полным Java-именем аннотации добавьте символ @ . Например:

-keep class @com.example.MyAnnotation com.example.MyClass

Если правило сохранения содержит более одной аннотации, оно сохраняет классы, имеющие все перечисленные аннотации. Вы можете перечислить несколько аннотаций, но правило применяется только в том случае, если класс имеет все перечисленные аннотации. Например, следующее правило сохраняет все классы, аннотированные как Annotation1 , так и Annotation2 .

-keep class @com.example.Annotation1 @com.example.Annotation2 *

Укажите подклассы и реализации.

Для выбора подкласса или класса, реализующего интерфейс, используйте методы extend и implements соответственно.

Например, если у вас есть класс Bar с подклассом Foo следующего вида:

class Foo : Bar()

Следующее правило сохранения сохраняет все подклассы класса Bar . Обратите внимание, что правило сохранения не включает сам суперкласс Bar .

-keep class * extends Bar

Если у вас есть класс Foo , реализующий интерфейс Bar :

class Foo : Bar

Следующее правило сохранения сохраняет все классы, реализующие Bar . Обратите внимание, что правило сохранения не включает сам интерфейс Bar .

-keep class * implements Bar

Указывайте классы на основе модификаторов доступа.

Для повышения точности правил сохранения можно указать модификаторы доступа, такие как public , private , static и final .

Например, следующее правило сохраняет все public классы внутри пакета api и его подпакетов, а также все публичные и защищенные члены в этих классах.

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

Также можно использовать модификаторы для членов класса. Например, следующее правило сохраняет только public static методы класса Utils :

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

Модификаторы, специфичные для Kotlin

R8 не поддерживает специфичные для Kotlin модификаторы, такие как internal и suspend . Используйте следующие рекомендации, чтобы сохранить такие поля.

  • Чтобы сохранить internal класс, метод или поле, рассматривайте их как публичные. Например, рассмотрим следующий исходный код на Kotlin:

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

    internal классы, методы и поля являются public в файлах .class создаваемых компилятором Kotlin, поэтому необходимо использовать ключевое слово public как показано в следующем примере:

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • При компиляции suspend элемента необходимо сопоставить его скомпилированную сигнатуру с правилом сохранения.

    Например, если у вас определена функция fetchUser , как показано в следующем фрагменте кода:

    suspend fun fetchUser(id: String): User
    

    После компиляции его сигнатура в байт-коде выглядит следующим образом:

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

    Чтобы написать правило сохранения для этой функции, необходимо сопоставить её с этой скомпилированной сигнатурой или использовать ... .

    Пример использования скомпилированной подписи выглядит следующим образом:

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

    Пример использования ... выглядит следующим образом:

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

Спецификация элемента

В спецификации класса могут быть указаны, при необходимости, члены класса, которые необходимо сохранить. Если для класса указан один или несколько членов, правило не применяется к другим членам.

Укажите участников на основе аннотаций.

Вы можете указывать члены класса на основе их аннотаций. Аналогично классам, перед полным именем аннотации в Java добавляется префикс @ . Это позволяет сохранять в классе только те члены, которые помечены определенными аннотациями. Например, чтобы сохранить методы и поля, аннотированные @com.example.MyAnnotation :

-keep class com.example.MyClass {
  @com.example.MyAnnotation <methods>;
  @com.example.MyAnnotation <fields>;
}

Это можно комбинировать с сопоставлением аннотаций на уровне классов для создания мощных, целенаправленных правил:

-keep class @com.example.ClassAnnotation * {
  @com.example.MethodAnnotation <methods>;
  @com.example.FieldAnnotation <fields>;
}

Это позволяет сохранять классы, аннотированные @ClassAnnotation , а методы этих классов — аннотированными @MethodAnnotation , а поля — аннотированными @FieldAnnotation .

По возможности рекомендуется использовать правила сохранения на основе аннотаций. Такой подход обеспечивает явную связь между вашим кодом и правилами сохранения и часто приводит к более надежным конфигурациям. Например, библиотека аннотаций androidx.annotation использует этот механизм.

Методы

Синтаксис для указания метода в спецификации члена правила сохранения выглядит следующим образом:

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

Например, следующее правило сохранения сохраняет открытый метод setLabel() , который возвращает void и принимает String .

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

Для поиска всех методов в классе можно использовать директиву <methods> следующим образом:

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

Чтобы узнать больше о том, как указывать типы возвращаемых значений и типов параметров, см. раздел «Типы» .

Строители

Для указания конструктора используйте <init> . Синтаксис указания конструктора в спецификации члена для правила сохранения выглядит следующим образом:

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

Например, следующее правило сохранения сохраняет пользовательский конструктор View , который принимает Context и объект AttributeSet .

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

Чтобы сохранить все конструкторы открытыми, используйте следующий пример в качестве образца:

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

Поля

Синтаксис для указания поля в спецификации элемента для правила сохранения выглядит следующим образом:

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

Например, следующее правило сохранения хранит закрытое строковое поле с именем userId и открытое статическое целочисленное поле с именем STATUS_ACTIVE :

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

Для сопоставления всех полей класса можно использовать <fields> в качестве сокращения:

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

Типы

В этом разделе описывается, как указывать типы возвращаемых значений, типы параметров и типы полей в спецификациях членов правила keep. Помните, что для указания типов следует использовать сгенерированные имена Java , если они отличаются от исходного кода Kotlin.

Примитивные типы

Для указания примитивного типа используйте его ключевое слово Java. R8 распознает следующие примитивные типы: boolean , byte , short , char , int , long , float , double .

Пример правила с примитивным типом выглядит следующим образом:

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

Общие типы

В процессе компиляции компилятор Kotlin/Java удаляет информацию о обобщенных типах, поэтому при написании правил сохранения, касающихся обобщенных типов, необходимо ориентироваться на скомпилированное представление кода , а не на исходный код. Подробнее об изменении обобщенных типов см. в разделе «Удаление типов» .

Например, если у вас есть следующий код с неограниченным обобщенным типом, определенным в Box.kt :

package com.myapp.data

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

После стирания типов T заменяется на Object . Чтобы сохранить конструктор и метод класса, ваше правило должно использовать java.lang.Object вместо обобщенного T

Пример правила сохранения может выглядеть следующим образом:

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

Если у вас есть следующий код с ограниченным обобщенным типом в NumberBox.kt :

package com.myapp.data

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

В этом случае стирание типов заменяет T его границей, java.lang.Number .

Пример правила сохранения может выглядеть следующим образом:

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

При использовании специфичных для приложения обобщенных типов в качестве базового класса необходимо также включить правила сохранения для базовых классов.

Например, для следующего кода:

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

С помощью правила сохранения, includedescriptorclasses можно сохранить класс UnpackOptions и метод класса Box одним правилом следующим образом:

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

Чтобы сохранить конкретную функцию, обрабатывающую список объектов, необходимо написать правило, точно соответствующее сигнатуре функции. Обратите внимание, что поскольку обобщенные типы удаляются, параметр типа List<Product> рассматривается как java.util.List .

Например, если у вас есть вспомогательный класс с функцией, которая обрабатывает список объектов Product следующим образом:

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)

Для защиты только функции processProducts можно использовать следующее правило keep:

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

Типы массивов

Тип массива указывается путем добавления квадратных скобок [] к типу компонента для каждого измерения массива. Это относится как к типам классов, так и к примитивным типам.

  • Одномерный массив классов: java.lang.String[]
  • Двумерный примитивный массив: int[][]

Например, если у вас есть следующий код:

package com.example.data

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

Вы можете использовать следующее правило сохранения:

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

Примеры

Например, чтобы сохранить определенный класс и все его члены, используйте следующее:

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

Чтобы сохранить только сам класс и его конструктор по умолчанию, но не другие его члены, используйте следующий код:

-keep class com.myapp.MyClass

Рекомендуется всегда указывать определенные члены класса. Например, в следующем примере открытое поле text и открытый метод updateText() находятся в классе MyClass .

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

Чтобы сохранить все поля и методы общедоступными, см. следующий пример:

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

Опуская спецификацию элементов

Если не указывать параметры члена класса, R8 сохранит конструктор класса по умолчанию.

Например, если вы напишете ` -keep class com.example.MyClass или -keep class com.example.MyClass {} , R8 обработает их так, как если бы вы написали следующее:

-keep class com.example.MyClass{
  void <init>();
}

Функции на уровне пакета

Чтобы сослаться на функцию Kotlin, определенную вне класса (обычно называемую функциями верхнего уровня), убедитесь, что вы используете сгенерированное Java-имя класса, неявно добавленное компилятором Kotlin. Имя класса — это имя файла Kotlin с добавлением Kt . Например, если у вас есть файл Kotlin с именем MyClass.kt , определенный следующим образом:

package com.example.myapp.utils

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

Для написания правила сохранения значения для функции isEmailValid , спецификация класса должна быть ориентирована на сгенерированный класс MyClassKt :

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

Подстановочные карты

В следующей таблице показано, как использовать подстановочные знаки для применения правил сохранения к нескольким классам или членам, соответствующим определенному шаблону.

Wildcard Применяется к классам или участникам. Описание
** Оба Наиболее часто используемый параметр. Соответствует любому имени типа, включая любое количество разделителей пакетов. Это полезно для сопоставления всех классов внутри пакета и его подпакетов.
* Оба В спецификациях классов соответствует любой части имени типа, не содержащей разделителей пакетов ( . ).
В спецификациях элементов соответствует любому имени метода или поля. При использовании отдельно, это также псевдоним для ** .
? Оба Сопоставляет любой отдельный символ в имени класса или элемента.
*** Члены Соответствует любому типу, включая примитивные типы (например int ), типы классов (например, java.lang.String ) и типы массивов любой размерности (например, byte[][] ).
... Члены Соответствует любому списку параметров для метода.
% Члены Соответствует любому примитивному типу (например, `int`, `float`, `boolean` и другим).

Вот несколько примеров использования специальных символов-заменителей:

  • Если у вас есть несколько методов с одинаковым именем, которые принимают в качестве входных данных разные примитивные типы, вы можете использовать % для написания правила сохранения, которое сохранит их все. Например, этот класс DataStore имеет несколько методов setValue :

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

    Следующее правило сохранения сохраняет все методы:

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • Если у вас есть несколько классов с именами, отличающимися всего одним символом, используйте знак вопроса ? для написания правила сохранения, которое сохранит все эти имена. Например, если у вас есть следующие классы:

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

    Следующее правило сохранения сохраняет все классы:

    -keep class com.example.models.UserV?
    
  • Чтобы сопоставить классы Example и AnotherExample (если бы они были классами корневого уровня), но не com.foo.Example , используйте следующее правило keep:

    -keep class *Example
    
  • Если использовать только *, он будет выступать в качестве псевдонима для **. Например, следующие правила сохранения эквивалентны:

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

Проверьте сгенерированные имена Java.

При написании правил сохранения необходимо указывать классы и другие ссылочные типы, используя их имена после компиляции в байт-код Java (см. разделы «Спецификация классов» и «Типы» для примеров). Чтобы проверить, какие имена Java были сгенерированы для вашего кода, используйте один из следующих инструментов в Android Studio:

  • Анализатор APK
  • Откройте исходный файл Kotlin и просмотрите байт-код, перейдя в меню Инструменты > Kotlin > Показать байт-код Kotlin > Декомпилировать .