В общих чертах, правило сохранения указывает класс (или подкласс, или реализацию), а затем члены этого класса — методы, конструкторы или поля — которые необходимо сохранить.
Общий синтаксис правила сохранения выглядит следующим образом:
-<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 | Сохраняет указанные члены только в том случае, если R8 не удаляет класс, который их содержит. |
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[];) }
Правила условного сохранения
В дополнение к стандартным правилам сохранения, можно использовать условные правила сохранения, которые применяются только при выполнении определенного условия. Условные правила можно указать с помощью флага -if . Правило сохранения, следующее за флагом -if , активно только в том случае, если спецификация класса, указанная в флаге -if , соответствует условию.
Условные правила сохранения особенно полезны при работе с библиотеками или шаблонами кода, использующими рефлексию, где правило сохранения необходимо только в том случае, если определенный класс или член присутствует или соответствует шаблону. Использование условных правил помогает минимизировать размер приложения, предотвращая ненужное сохранение кода.
Общий синтаксис правила условного сохранения выглядит следующим образом:
-if <class_specification_if> <keep_rule>
Если спецификация класса в условии -if содержит подстановочные символы (например * или ** ), то последовательность символов, соответствующих подстановочному символу, захватывается . Вы можете ссылаться на эти захваченные строки в последующем правиле сохранения, используя обратные ссылки: <1> относится к строке, захваченной первым подстановочным символом, <2> относится к строке, захваченной вторым подстановочным символом, и так далее.
Например, компонент Jetpack Navigation генерирует классы NavArgs для типобезопасной передачи аргументов между пунктами назначения. При использовании делегата NavArgsLazy он использует рефлексию для поиска и вызова статического метода fromBundle в сгенерированном классе NavArgs для десериализации аргументов. Если ваше приложение использует NavArgs , вам нужно сохранять метод fromBundle только для классов, реализующих интерфейс NavArgs .
Вы можете использовать правило условного сохранения, чтобы указать, что если какой-либо класс реализует интерфейс androidx.navigation.NavArgs , то R8 должен сохранить метод fromBundle для этого конкретного класса:
# If a class implements NavArgs...
-if public class ** implements androidx.navigation.NavArgs
# ...then keep the fromBundle method of that matched class (<1>).
-keepclassmembers public class <1> {
public static ** fromBundle(android.os.Bundle);
}
В этом примере ** — это первый и единственный подстановочный знак в условии -if . Он соответствует имени класса любого класса, реализующего интерфейс androidx.navigation.NavArgs . Строка, которой соответствует ** (в данном случае, имя класса), захватывается, и вы можете ссылаться на нее с помощью <1> в последующем правиле. Таким образом, правило -keepclassmembers применяется к любому классу, реализующему интерфейс androidx.navigation.NavArgs , который был найден в условии -if . Если R8 не находит классов, реализующих NavArgs , он игнорирует это правило keep.
Ещё один распространённый вариант использования — библиотеки сериализации JSON, такие как Gson. Если ваши классы модели данных используют аннотацию @SerializedName из Gson для какого-либо поля, вы можете использовать условное правило для защиты любого такого класса и его членов, которые Gson необходим для рефлексии:
# If a class has fields annotated with @SerializedName...
-if class ** { @com.google.gson.annotations.SerializedName <fields>; }
# ...then keep that class (<1>), its @SerializedName fields,
# and its constructors for Gson.
-keep class <1> {
@com.google.gson.annotations.SerializedName <fields>;
<init>(...);
}
Обратные ссылки захватывают строки, которые могут быть подстроками имен классов, если подстановочный знак соответствует только части имени. Например, если вы используете -if class com.example.*X* , R8 захватывает подстроку перед X как <1> , а подстроку после X как <2> . Следующее правило использует это для поиска любого имени класса, содержащего X , и сохранения соответствующего класса, где X заменен на Y :
# If a class like com.example.PrefixXPostfix exists...
-if class com.example.*X*
# ...keep com.example.PrefixYPostfix.
-keep class com.example.<1>Y<2>
Правила условного сохранения для рефлексии
Один из распространенных вариантов использования правил условного сохранения — обработка рефлексии, когда доступ к определенным методам или классам осуществляется динамически во время выполнения. Например, если библиотека использует рефлексию для взаимодействия с вашим кодом, вам может потребоваться сохранять определенные члены только в том случае, если вы используете определенную функцию этой библиотеки.
Библиотека Jetpack Navigation использует рефлексию с помощью делегата NavArgsLazy для вызова статического метода fromBundle в сгенерированных классах NavArgs для типобезопасной передачи аргументов. Чтобы гарантировать, что этот метод сохраняется только для реализаций NavArgs , а не для каждого класса, Jetpack Navigation включает следующее условное правило сохранения:
# If a class implements NavArgs...
-if public class ** implements androidx.navigation.NavArgs
# ...then keep the fromBundle method of that matched class (<1>).
-keepclassmembers public class <1> {
public static ** fromBundle(android.os.Bundle);
}
Это правило сохраняет свойство fromBundle только для тех классов, которым оно необходимо, вместо того, чтобы сохранять его во всех классах или требовать от вас вручную указывать, каким классам оно нужно.
Проверьте сгенерированные имена Java.
При написании правил сохранения необходимо указывать классы и другие ссылочные типы, используя их имена после компиляции в байт-код Java (см. разделы «Спецификация классов» и «Типы» для примеров). Чтобы проверить, какие имена Java были сгенерированы для вашего кода, используйте один из следующих инструментов в Android Studio:
- Анализатор APK
- Откройте исходный файл Kotlin и просмотрите байт-код, перейдя в меню Инструменты > Kotlin > Показать байт-код Kotlin > Декомпилировать .