Как добавить правила хранения

На высоком уровне правило сохранения указывает класс (или подкласс, или реализацию), а затем элементы – методы, конструкторы или поля – в этом классе, которые нужно сохранить.

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


-<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, keepclassmembers, keepclasseswithmembers, keepnames, keepclassmembernames и keepclasseswithmembernames.

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

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

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

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

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

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

Как выбрать подходящий вариант хранения

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

Вариант сохранения Уменьшает классы Обфускация классов Уменьшение участников Скрывает участников
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

Сохранить модификатор варианта

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

Возможные значения модификатора параметра keep описаны в таблице ниже.

Значение Описание
allowoptimization Позволяет оптимизировать указанные элементы. Однако указанные элементы не будут переименованы или удалены.
allowobfuscation Позволяет переименовывать указанные элементы. Однако элементы не удаляются и не оптимизируются.
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, его сигнатура скомпилированного байт-кода должна соответствовать правилу сохранения.

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

    package com.example.repository
    
    import com.example.model.User
    
    class UserRepository {
        suspend fun fetchUser(id: String): User {
            // Implementation details...
        }
    }
    

    Когда компилятор Kotlin компилирует этот класс в байт-код, функции suspend преобразуются в стиль передачи продолжения (CPS). Модификатор suspend удаляется, тип возвращаемого значения меняется на java.lang.Object, а в сигнатуру метода добавляется параметр kotlin.coroutines.Continuation для управления асинхронным конечным автоматом.

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

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

    Чтобы написать правило сохранения для этой функции, нельзя использовать синтаксис Kotlin suspend. Вместо этого необходимо сопоставить сигнатуру скомпилированного байт-кода (с указанием на kotlin.coroutines.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.

Для отражения в собственном коде можно также использовать Jetpack Keep. Вместо того чтобы писать правила сохранения, которые нацелены на аннотации, вы аннотируете код, а плагин Gradle генерирует правила сохранения за вас.

Методы

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

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

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

-keep class com.example.model.UserData {
    public java.lang.String getUserId();
}

Вы можете использовать <methods> как сокращение, чтобы сопоставить все методы в классе, как показано ниже:

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

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

Конструкторы

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

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

Например, следующее правило keep сохраняет конструктор для хранилища состояния пользовательского интерфейса, которое принимает экземпляр репозитория.

-keep class com.example.ui.state.UserViewModel {
    public <init>(com.example.repository.UserDataRepository);
}

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

-keep class com.example.ui.state.UserViewModel {
    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>;
}

Типы

В этом разделе описывается, как указывать типы возвращаемых значений, параметров и полей в спецификациях элементов правил сохранения. Если сгенерированные названия типов на языке 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 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>();
}

Исключить шаблоны названий участников

Начиная с версии плагина Android Gradle (AGP) 9.2.0, вы можете отрицать шаблоны имен элементов в правилах сохранения. Это позволяет указать участников, которых нужно оставить или исключить, на основе шаблонов. Чтобы исключить шаблон, добавьте к шаблону имени участника восклицательный знак (!).

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

Пример

Чтобы сохранить все общедоступные методы в com.example.MyClass, кроме тех, которые заканчиваются на "ForTesting", используйте следующее правило:

-keepclassmembers class com.example.MyClass {
    public *** !*ForTesting(...);
}

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

Чтобы сослаться на функцию 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);
}

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

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

Подстановочный знак Применяется к курсам или участникам Описание
** Оба варианта Используется чаще всего. Соответствует любому названию типа, включая любое количество разделителей пакетов. Это полезно, если вы хотите найти все классы в пакете и его подпакетах.
* Оба варианта Для спецификаций классов соответствует любой части имени типа, которая не содержит разделителей пакетов (.).
Для спецификаций элементов соответствует любому имени метода или поля. Если используется отдельно, то также является псевдонимом для **.
? Оба варианта Соответствует любому отдельному символу в названии класса или элемента.
*** Участники Соответствует любому типу, включая примитивные типы (например, 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 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, он игнорирует это правило.

Ещё один распространенный вариант использования – библиотеки сериализации JSON, такие как Gson. Если в классах модели данных используется аннотация Gson @SerializedName для какого-либо поля, вы можете использовать условное правило, чтобы защитить любой такой класс и его элементы, необходимые 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 Analyzer
  • Откройте исходный файл Kotlin и проверьте байт-код, выбрав Tools (Инструменты) > Kotlin > Show Kotlin Bytecode (Показать байт-код Kotlin) > Decompile (Декомпилировать).