В общих чертах, правило сохранения указывает класс (или подкласс, или реализацию), а затем члены этого класса — методы, конструкторы или поля — которые необходимо сохранить.
Общий синтаксис правила сохранения выглядит следующим образом:
-<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 > Декомпилировать .