Przykłady i przypadki użycia reguł

Poniższe przykłady są oparte na typowych scenariuszach, w których używasz R8 do optymalizacji, ale potrzebujesz zaawansowanych wskazówek dotyczących tworzenia reguł przechowywania.

Odbicie

Ogólnie rzecz biorąc, nie zalecamy używania odbicia, aby uzyskać optymalną wydajność. W niektórych przypadkach może to być jednak nieuniknione. Poniższe przykłady zawierają wskazówki dotyczące reguł przechowywania w typowych scenariuszach, w których używane jest odbicie.

Odbicie z klasami wczytywanymi według nazwy

Biblioteki często wczytują klasy dynamicznie, używając nazwy klasy jako String. R8 nie może jednak wykryć klas wczytywanych w ten sposób i może usunąć klasy, które uzna za nieużywane.

Rozważmy na przykład sytuację, w której masz bibliotekę i aplikację, która z niej korzysta. Kod pokazuje moduł wczytywania biblioteki, który tworzy instancję interfejsu StartupTask zaimplementowanego przez aplikację.

Kod biblioteki wygląda tak:

// The interface for a task that runs once.
interface StartupTask {
    fun run()
}

// The library object that loads and executes the task.
object TaskRunner {
    fun execute(className: String) {
        // R8 won't retain classes specified by this string value at runtime
        val taskClass = Class.forName(className)
        val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
        task.run()
    }
}

Aplikacja korzystająca z biblioteki ma ten kod:

// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
    override fun run() {
        // This log will never appear if the class is removed by R8.
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is told to run the app's task by its name.
    TaskRunner.execute("com.example.app.PreCacheTask")
}

W tym przypadku biblioteka powinna zawierać plik reguł przechowywania dla konsumentów z tymi regułami:

-keep class * implements com.example.library.StartupTask {
    <init>();
}

Bez tej reguły R8 usunie PreCacheTask z aplikacji, ponieważ aplikacja nie używa tej klasy bezpośrednio, co spowoduje przerwanie integracji. Reguła znajduje klasy, które implementują interfejs StartupTask biblioteki, i zachowuje je wraz z konstruktorem bez argumentów, co umożliwia bibliotece utworzenie instancji i wykonanie PreCacheTask.

Odbicie z ::class.java

Biblioteki mogą wczytywać klasy, przekazując bezpośrednio obiekt Class. Jest to bardziej niezawodna metoda niż wczytywanie klas według nazwy. Tworzy to silne odniesienie do klasy, które R8 może wykryć. Chociaż zapobiega to usunięciu klasy przez R8, nadal musisz użyć reguły przechowywania, aby zadeklarować, że klasa jest tworzona za pomocą odbicia, i chronić elementy, do których uzyskuje się dostęp za pomocą odbicia, takie jak konstruktor.

Rozważmy na przykład sytuację, w której masz bibliotekę i an aplikację, która z niej korzysta. Moduł wczytywania biblioteki tworzy instancję StartupTask interfejsu, przekazując bezpośrednio odniesienie do klasy.

Kod biblioteki wygląda tak:

// The interface for a task that runs once.
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

Aplikacja korzystająca z biblioteki ma ten kod:

// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}

W tym przypadku biblioteka powinna zawierać plik reguł przechowywania dla konsumentów z tymi regułami:

# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
    <init>();
}

Te reguły zostały zaprojektowane tak, aby doskonale współpracować z tym typem odbicia, co pozwala na maksymalną optymalizację przy jednoczesnym zapewnieniu prawidłowego działania kodu. Reguły umożliwiają R8 zaciemnienie nazwy klasy i zmniejszenie lub usunięcie implementacji klasy StartupTask, jeśli aplikacja nigdy jej nie używa. W przypadku każdej implementacji, takiej jak PrecacheTask używana w przykładzie, zachowują one jednak domyślny konstruktor (<init>()), który musi wywołać biblioteka.

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: ta reguła dotyczy każdej klasy, która implementuje interfejs StartupTask.
    • -keep class * implements com.example.library.StartupTask: Ta reguła zachowuje każdą klasę (*), która implementuje Twój interfejs.
    • ,allowobfuscation: ta reguła informuje R8, że pomimo zachowania klasy może ją zmienić lub zaciemnić. Jest to bezpieczne, ponieważ biblioteka nie polega na nazwie klasy. Bezpośrednio pobiera obiekt Class.
    • ,allowshrinking: ten modyfikator informuje R8, że może usunąć klasę, jeśli nie jest używana. Pomaga to R8 bezpiecznie usunąć implementację StartupTask, która nigdy nie jest przekazywana do TaskRunner.execute(). Krótko mówiąc, ta reguła oznacza, że jeśli aplikacja używa klasy, która implementuje StartupTask, R8 zachowuje tę klasę. R8 może zmienić nazwę klasy, aby zmniejszyć jej rozmiar, i usunąć ją, jeśli aplikacja jej nie używa.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: Ta reguła dotyczy konkretnych elementów klas zidentyfikowanych w pierwszej regule – w tym przypadku konstruktora.
    • -keepclassmembers class * implements com.example.library.StartupTask: ta reguła zachowuje konkretne elementy (metody, pola) klasy, która implementuje interfejs StartupTask, ale tylko wtedy, gdy sama implementowana klasa jest zachowywana.
    • { <init>(); }: to selektor elementów. <init> to specjalna wewnętrzna nazwa konstruktora w kodzie bajtowym Javy. Ta część dotyczy konkretnie domyślnego konstruktora bez argumentów.
    • Ta reguła jest krytyczna, ponieważ Twój kod wywołuje getDeclaredConstructor().newInstance() bez argumentów, co powoduje wywołanie domyślnego konstruktora za pomocą odbicia. Bez tej reguły R8 widzi, że żaden kod nie wywołuje bezpośrednio new PreCacheTask(), zakłada, że konstruktor jest nieużywany, i usuwa go. Powoduje to awarię aplikacji w czasie działania z InstantiationException.

Odbicie na podstawie adnotacji metody

Biblioteki często definiują adnotacje, których deweloperzy używają do oznaczania metod lub pól. Biblioteka używa następnie odbicia, aby znaleźć te oznaczone elementy w czasie działania. Na przykład adnotacja @OnLifecycleEvent służy do znajdowania wymaganych metod w czasie działania.

Rozważmy na przykład sytuację, w której masz bibliotekę i aplikację, która z niej korzysta. Przykład pokazuje magistralę zdarzeń, która znajduje i wywołuje metody oznaczone adnotacją @OnEvent.

Kod biblioteki wygląda tak:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent

class EventBus {
    fun dispatch(listener: Any) {
        // Find all methods annotated with @OnEvent and invoke them
        listener::class.java.declaredMethods.forEach { method ->
            if (method.isAnnotationPresent(OnEvent::class.java)) {
                try {
                    method.invoke(listener)
                } catch (e: Exception) { /* ... */ }
            }
        }
    }
}

Aplikacja korzystająca z biblioteki ma ten kod:

class MyEventListener {
    @OnEvent
    fun onSomethingHappened() {
        // This method will be removed by R8 without a keep rule
        Log.d(TAG, "Event received!")
    }
}

fun onCreate() {
    // Instantiate the listener and the event bus
    val listener = MyEventListener()
    val eventBus = EventBus()

    // Dispatch the listener to the event bus
    eventBus.dispatch(listener)
}

Biblioteka powinna zawierać plik reguł przechowywania dla konsumentów, który automatycznie zachowuje wszystkie metody używające jej adnotacji:

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: ta reguła zachowuje adnotacje, które mają być odczytywane w czasie działania.
  • -keep @interface com.example.library.OnEvent: Ta reguła zachowuje samą klasę adnotacji OnEvent.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: Ta reguła zachowuje klasę i konkretne elementy tylko wtedy, gdy klasa jest używana i zawiera te elementy.
    • -keepclassmembers: ta reguła zachowuje klasę i konkretne elementy tylko wtedy, gdy klasa jest używana i zawiera te elementy.
    • class *: reguła ma zastosowanie do każdych zajęć.
    • @com.example.library.OnEvent <methods>;: ta reguła zachowuje każdą klasę , która ma co najmniej 1 metodę (<methods>) oznaczoną adnotacją @com.example.library.OnEvent, a także same oznaczone metody.

Odczucia na podstawie adnotacji zajęć

Biblioteki mogą używać odbicia do skanowania klas, które mają określoną adnotację. W tym przypadku klasa modułu wykonawczego zadań znajduje wszystkie klasy oznaczone adnotacją ReflectiveExecutor za pomocą odbicia i wykonuje metodę execute.

Rozważmy na przykład sytuację, w której masz bibliotekę i aplikację, która z niej korzysta.

Biblioteka ma ten kod:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor

class TaskRunner {
    fun process(task: Any) {
        val taskClass = task::class.java
        if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
            val methodToCall = taskClass.getMethod("execute")
            methodToCall.invoke(task)
        }
    }
}

Aplikacja korzystająca z biblioteki ma ten kod:

// In consumer app

@ReflectiveExecutor
class ImportantBackgroundTask {
    fun execute() {
        // This class will be removed by R8 without a keep rule
        Log.e("ImportantBackgroundTask", "Executing the important background task...")
    }
}

// Usage of ImportantBackgroundTask

fun onCreate(){
    val task = ImportantBackgroundTask()
    val runner = TaskRunner()
    runner.process(task)
}

Ponieważ biblioteka używa odbicia do uzyskiwania dostępu do konkretnych klas, powinna zawierać plik reguł przechowywania dla konsumentów z tymi regułami:

# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations

# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor

# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
   public void execute();
}

Ta konfiguracja jest bardzo wydajna, ponieważ dokładnie informuje R8, co ma zachować.

Odbicie do obsługi opcjonalnych zależności

Typowym przypadkiem użycia odbicia jest tworzenie miękkiej zależności między biblioteką podstawową a opcjonalną biblioteką dodatków. Biblioteka podstawowa może sprawdzić, czy dodatek jest uwzględniony w aplikacji, a jeśli tak, może włączyć dodatkowe funkcje. Umożliwia to dostarczanie modułów dodatków bez wymuszania bezpośredniej zależności biblioteki podstawowej od nich.

Biblioteka podstawowa używa odbicia (Class.forName), aby wyszukać konkretną klasę według nazwy. Jeśli klasa zostanie znaleziona, funkcja jest włączana. Jeśli nie, funkcja działa prawidłowo.

Rozważmy na przykład ten kod, w którym podstawowy AnalyticsManager sprawdza opcjonalną klasę VideoEventTracker, aby włączyć statystyki wideo.

Biblioteka podstawowa ma ten kod:

object AnalyticsManager {
    private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"

    fun initialize() {
        try {
            // Attempt to load the optional module's class using reflection
            Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
            Log.d(TAG, "Video tracking enabled.")
        } catch (e: ClassNotFoundException) {
            Log.d(TAG,"Video tracking module not found. Skipping.")
        } catch (e: Exception) {
            Log.e(TAG, e.printStackTrace())
        }
    }
}

Opcjonalna biblioteka wideo ma ten kod:

package com.example.analytics.video

class VideoEventTracker {
    // This constructor must be kept for the reflection call to succeed.
    init { /* ... */ }
}

Deweloper opcjonalnej biblioteki jest odpowiedzialny za udostępnienie niezbędnej reguły przechowywania dla konsumentów. Ta reguła przechowywania zapewnia, że każda aplikacja korzystająca z opcjonalnej biblioteki zachowuje kod, który musi znaleźć biblioteka podstawowa.

# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

Bez tej reguły R8 prawdopodobnie usunie VideoEventTracker z opcjonalnej biblioteki, ponieważ nic w tym module nie używa go bezpośrednio. Reguła przechowywania zachowuje klasę i jej konstruktor, co umożliwia bibliotece podstawowej utworzenie jej instancji.

Odbicie do uzyskiwania dostępu do elementów prywatnych

Używanie odbicia do uzyskiwania dostępu do kodu prywatnego lub chronionego, który nie jest częścią publicznego interfejsu API biblioteki, może powodować poważne problemy. Taki kod może ulec zmianie bez powiadomienia, co może prowadzić do nieoczekiwanego działania lub awarii aplikacji.

Jeśli polegasz na odbiciu w przypadku interfejsów API innych niż publiczne, możesz napotkać te problemy:

  • Zablokowane aktualizacje: zmiany w kodzie prywatnym lub chronionym mogą uniemożliwić aktualizację do nowszych wersji biblioteki.
  • Utracone korzyści: możesz stracić dostęp do nowych funkcji, ważnych poprawek awarii lub niezbędnych aktualizacji zabezpieczeń.

Optymalizacje R8 i odbicie

Jeśli musisz użyć odbicia w kodzie prywatnym lub chronionym biblioteki, zwróć szczególną uwagę na optymalizacje R8. Jeśli nie ma bezpośrednich odniesień do tych elementów, R8 może założyć, że są one nieużywane, a następnie je usunąć lub zmienić ich nazwę. Może to prowadzić do awarii w czasie działania, często z mylącymi komunikatami o błędach, takimi jak NoSuchMethodException lub NoSuchFieldException.

Rozważmy na przykład sytuację, w której pokazujemy, jak można uzyskać dostęp do pola prywatnego z klasy biblioteki.

Biblioteka, której nie jesteś właścicielem, ma ten kod:

class LibraryClass {
    private val secretMessage = "R8 will remove me"
}

Twoja aplikacja ma ten kod:

fun accessSecretMessage(instance: LibraryClass) {
    // Use Java reflection from Kotlin to access the private field
    val secretField = instance::class.java.getDeclaredField("secretMessage")
    secretField.isAccessible = true
    // This will crash at runtime with R8 enabled
    val message = secretField.get(instance) as String
}

Dodaj w aplikacji regułę -keep, aby uniemożliwić R8 usunięcie pola prywatnego:

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: ta reguła zachowuje konkretne elementy klasy tylko wtedy, gdy sama klasa jest zachowywana.
  • class com.example.LibraryClass: ta reguła dotyczy dokładnej klasy zawierającej pole.
  • private java.lang.String secretMessage;: ta reguła identyfikuje konkretne pole prywatne według nazwy i typu.

Java Native Interface (JNI)

Optymalizacje R8 mogą powodować problemy podczas pracy z wywołaniami zwrotnymi z kodu natywnego (C/C++) do Javy lub Kotlina. Chociaż odwrotna sytuacja też jest możliwa – wywołania w dół z Javy lub Kotlina do kodu natywnego mogą powodować problemy – domyślny plik proguard-android-optimize.txt zawiera tę regułę, aby zapewnić działanie wywołań w dół. Ta reguła chroni przed przycinaniem metod natywnych.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

Interakcja z kodem natywnym za pomocą Java Native Interface (JNI)

Gdy aplikacja używa JNI do wykonywania wywołań zwrotnych z kodu natywnego (C/C++) do Javy lub Kotlina, R8 nie widzi, które metody są wywoływane z kodu natywnego. Jeśli w aplikacji nie ma bezpośrednich odniesień do tych metod, R8 błędnie zakłada, że są one nieużywane, i je usuwa, co powoduje awarię aplikacji.

Poniższy przykład pokazuje klasę Kotlin z metodą, która ma być wywoływana z biblioteki natywnej. Biblioteka natywna tworzy instancję typu aplikacji i przekazuje dane z kodu natywnego do kodu Kotlin.

package com.example.models

// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
    /**
     *   This method is called from the native side.
     *   R8 will remove it if it's not kept.
     */
    fun onNativeEvent(data: NativeData) {
        Log.d(TAG, "Received event from native code: $data")
    }
    // Use 'external' to declare a native method
    external fun startNativeProcess()

    companion object {
        init {
            // Load the native library
            System.loadLibrary("my-native-lib")
        }
    }
}

W tym przypadku musisz poinformować R8, aby nie optymalizował typu aplikacji. Jeśli metody wywoływane z kodu natywnego używają własnych klas w swoich sygnaturach jako parametrów lub typów zwracanych, musisz też sprawdzić, czy te klasy nie zostały zmienione.

Dodaj do aplikacji te reguły przechowywania:

-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
    public void onNativeEvent(com.example.model.NativeData);
}

-keep class NativeData{
        <init>(java.lang.Integer, java.lang.String);
}

Te reguły przechowywania uniemożliwiają R8 usunięcie lub zmianę nazwy metody onNativeEvent oraz – co najważniejsze – jej typu parametru.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: ta reguła zachowuje konkretne elementy klasy tylko wtedy, gdy klasa jest najpierw tworzona w kodzie Kotlin lub Java. Informuje R8, że aplikacja używa tej klasy i że powinna zachować jej konkretne elementy.
    • -keepclassmembers: ta reguła zachowuje konkretne elementy klasy tylko wtedy, gdy klasa jest najpierw tworzona w kodzie Kotlin lub Java. Informuje R8, że aplikacja używa tej klasy i że powinna zachować jej konkretne elementy.
    • class com.example.JniBridge: ta reguła dotyczy dokładnej klasy zawierającej pole.
    • includedescriptorclasses: ten modyfikator zachowuje też wszystkie klasy znalezione w sygnaturze lub deskryptorze metody. W tym przypadku uniemożliwia R8 zmianę nazwy lub usunięcie klasy com.example.models.NativeData, która jest używana jako parametr. Jeśli nazwa NativeData zostanie zmieniona (np. na a.a), sygnatura metody nie będzie już zgodna z oczekiwaniami kodu natywnego, co spowoduje awarię.
    • public void onNativeEvent(com.example.models.NativeData);: ta reguła określa dokładną sygnaturę Java metody, którą należy zachować.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: chociaż includedescriptorclasses zapewnia zachowanie samej klasy NativeData, wszystkie elementy (pola lub metody) w NativeData , do których uzyskuje się dostęp bezpośrednio z kodu natywnego JNI, wymagają własnych reguł przechowywania.
    • -keep class NativeData: ta reguła dotyczy klasy o nazwie NativeData a blok określa, które elementy w klasie NativeData mają być zachowane.
    • <init>(java.lang.Integer, java.lang.String): to sygnatura konstruktora. Jednoznacznie identyfikuje konstruktor, który przyjmuje 2 parametry: pierwszy to Integer, a drugi to String.

Pośrednie wywołania platformy

Przesyłanie danych za pomocą implementacji Parcelable

Platforma Android używa odbicia do tworzenia instancji obiektów Parcelable. W nowoczesnym programowaniu w Kotlinie należy używać wtyczki kotlin-parcelize, która automatycznie generuje niezbędną implementację Parcelable, w tym pole CREATOR i metody wymagane przez platformę.

Rozważmy na przykład sytuację, w której wtyczka kotlin-parcelize służy do tworzenia klasy Parcelable:

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
    val name: String,
    val age: Int
) : Parcelable

W tym przypadku nie ma zalecanej reguły przechowywania. Wtyczka Gradle kotlin-parcelize automatycznie generuje wymagane reguły przechowywania dla klas oznaczonych adnotacją @Parcelize. Zajmuje się złożonością, zapewniając, że wygenerowane CREATOR i konstruktory są zachowywane na potrzeby wywołań odbicia platformy Android.

Jeśli ręcznie napiszesz klasę Parcelable w Kotlinie bez użycia @Parcelize, musisz zachować pole CREATOR i konstruktor, który akceptuje Parcel. Jeśli o tym zapomnisz, aplikacja ulegnie awarii, gdy system spróbuje zdeserializować Twój obiekt. Używanie @Parcelize to standardowa i bezpieczniejsza praktyka.

Podczas korzystania z wtyczki kotlin-parcelize pamiętaj o tych kwestiach:

  • Wtyczka automatycznie tworzy pola CREATOR podczas kompilacji.
  • Plik proguard-android-optimize.txt zawiera niezbędne reguły keep, aby zachować te pola i zapewnić prawidłowe działanie.
  • Deweloperzy aplikacji muszą sprawdzić, czy wszystkie wymagane reguły keep są obecne, zwłaszcza w przypadku implementacji niestandardowych lub zależności od innych firm.

Biblioteki, które używają odbicia lub przekształceń kodu bajtowego, uzyskują dostęp do kodu dynamicznie w czasie działania. Jeśli R8 usunie lub zmieni nazwy klas, pól lub metod, do których uzyskuje się dostęp w ten sposób, aplikacja może ulec awarii.

Popularne biblioteki innych firm (takie jak Gson, Retrofit i Kotlinx Serialization) automatycznie dołączają jednak własne reguły przechowywania dla konsumentów R8. Jeśli używasz najnowszych wersji tych bibliotek, nie musisz dodawać ręcznie reguł przechowywania do projektu.

Gson

Gson to biblioteka serializacji i deserializacji JSON, która w dużym stopniu opiera się na odbiciu. Jeśli używasz trybu pełnego do optymalizacji aplikacji, usuwa on sygnatury typów ogólnych, domyślne konstruktory i pola bez adnotacji, chyba że wyraźnie poinstruujesz go inaczej.

Aby zapewnić prawidłowe działanie Gson, dodaj konkretne reguły, które zachowają pola nietrwałe w klasach modelu danych i zachowają hierarchię TypeToken:

# Preserve generic type information required for deserialization
-keepattributes Signature

# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
    !transient <fields>;
}

# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken

Pola oznaczone modyfikatorem transient są ignorowane przez Gson podczas serializacji i deserializacji, dlatego reguła przechowywania dotyczy konkretnie pól nietrwałych (!transient).

Retrofit

Retrofit to biblioteka sieciowa, która sprawdza metody interfejsu usługi oznaczone adnotacjami HTTP (takimi jak @GET lub @POST) za pomocą odbicia, aby tworzyć żądania sieciowe i konwertować odpowiedzi.

Retrofit dynamicznie generuje implementacje interfejsów API w czasie działania za pomocą Proxy.newProxyInstance(). Ponieważ R8 nie widzi żadnej klasy, która statycznie implementuje te interfejsy, może usunąć metody lub ich ogólne typy zwracane.

Dołączone reguły przechowywania

Retrofit opiera się na odbiciu w czasie działania, aby sprawdzać parametry ogólne, adnotacje metod i adnotacje parametrów. Bez odpowiedniej konfiguracji tryb pełny R8 może całkowicie usunąć sygnatury ogólne z typów zwracanych, kontynuacji Kotlin i klas odpowiedzi, a nawet zastąpić wartości interfejsu wartością null, ponieważ interfejsy Retrofit są dynamicznie tworzone za pomocą proxy.

Od wersji 2.10.0 biblioteka Retrofit automatycznie dołącza oficjalne reguły przechowywania wymagane do zachowania domyślnych adnotacji, parametrów metody usługi i niezbędnych metadanych klasy. Więcej informacji znajdziesz w artykule Reguły używane przez Retrofit.

Zachowywanie ogólnych typów zwracanych

Retrofit sprawdza sygnaturę ogólną zwracanego typu (np. Observable<Data>), aby prawidłowo zdeserializować odpowiedź sieciową. Jeśli R8 usunie sygnaturę ogólną, Retrofit zastąpi utworzony obiekt wartością null.

Aby uniemożliwić trybowi pełnemu R8 usuwanie sygnatury ogólnej typów zwracanych, użyj tej reguły warunkowej:

# Preserve generic type information for Call/Observable return types
-keepattributes Signature

# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
    @retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

Należy też zachować rzeczywistą klasę modelu danych, która jest zwracana (np. Data w Observable<Data>), ponieważ zostanie ona utworzona za pomocą odbicia przez konwerter (np. Gson).

Współprogramy

Gdy używasz współprogramów Kotlin, kompilator Kotlin przekształca funkcje suspend, dodając parametr Continuation do skompilowanej sygnatury metody.

Gdy biblioteki takie jak Retrofit odczytują sygnaturę ogólną funkcji suspend za pomocą odbicia, polegają na tym parametrze Continuation. W trybie pełnym atrybut Signature jest zachowywany tylko w przypadku klas, które są jawnie zachowywane. Ponieważ Continuation jest parametrem syntetycznym, R8 domyślnie usuwa jego sygnaturę, co powoduje przerwanie odbicia.

Aby zapobiec usuwaniu sygnatur i zapewnić zgodność w czasie działania w trybie pełnym, uwzględnij tę regułę:

# Keep the signature attribute globally
-keepattributes Signature

# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation