保持ルールのユースケースと例

次の例は、R8 を使用して最適化を行う一般的なシナリオに基づいています。ただし、keep ルールを作成するには高度なガイダンスが必要です。

振り返り

一般に、最適なパフォーマンスを得るために、リフレクションの使用はおすすめしません。ただし、特定のシナリオでは、回避できない場合があります。次の例は、リフレクションを使用する一般的なシナリオでのキープルールのガイダンスを示しています。

リフレクションが独自のアプリまたはライブラリ コード内にある場合は、保持ルールを手動で記述する代わりに、keep アノテーションを使用できます。Gradle プラグインは、ライブラリのコンシューマー keep ルールなど、ソースコード内のアノテーションから keep ルールを生成します。

名前で読み込まれたクラスを使用したリフレクション

ライブラリは、クラス名を String として使用して、クラスを動的に読み込むことがよくあります。ただし、R8 はこの方法で読み込まれたクラスを検出できないため、未使用と見なしたクラスを削除する可能性があります。

たとえば、ライブラリと、そのライブラリを使用するアプリがある次のシナリオを考えてみましょう。このコードは、アプリによって実装された StartupTask インターフェースをインスタンス化するライブラリ ローダーを示しています。

ライブラリ コードは次のとおりです。

// 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()
    }
}

ライブラリを使用するアプリには、次のコードがあります。

// 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")
}

このシナリオでは、ライブラリに、次の保持ルールを含むコンシューマー保持ルール ファイルを含める必要があります。

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

このルールがないと、アプリがクラスを直接使用していないため、R8 はアプリから PreCacheTask を削除し、統合が中断されます。このルールは、ライブラリの StartupTask インターフェースを実装するクラスを見つけて、引数なしのコンストラクタとともに保持します。これにより、ライブラリは PreCacheTask を正常にインスタンス化して実行できます。

::class.java を使用したリフレクション

ライブラリは、アプリが Class オブジェクトを直接渡すことでクラスを読み込むことができます。これは、名前でクラスを読み込むよりも堅牢な方法です。これにより、R8 が検出できるクラスへの強い参照が作成されます。ただし、この方法では R8 によるクラスの削除は防止できますが、クラスがリフレクションによってインスタンス化されることを宣言し、コンストラクタなど、リフレクションによってアクセスされるメンバーを保護するには、keep ルールを使用する必要があります。

たとえば、ライブラリと、そのライブラリを使用するアプリがあるというシナリオを考えてみましょう。ライブラリ ローダは、クラス参照を直接渡すことで StartupTask インターフェースをインスタンス化します。

ライブラリ コードは次のとおりです。

// 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()
    }
}

ライブラリを使用するアプリには、次のコードがあります。

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

このシナリオでは、ライブラリに次の保持ルールを含むコンシューマー保持ルール ファイルを含める必要があります。

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

これらのルールは、このタイプのリフレクションで完全に動作するように設計されており、コードが正しく動作することを保証しながら、最大限の最適化を実現します。このルールにより、アプリで クラスが使用されない場合、R8 はクラス名を難読化し、StartupTask クラスの 実装を縮小または削除できます。ただし、この例で使用されている PrecacheTask などの実装では、ライブラリが呼び出す必要があるデフォルトのコンストラクタ(<init>())が保持されます。

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: このルールは、StartupTask インターフェースを実装するクラスを対象とします。
    • -keep class * implements com.example.library.StartupTask: インターフェースを実装するクラス(*)を保持します。
    • ,allowobfuscation: このオプションは、 クラスを保持するものの、名前の変更(難読化)は可能であることを R8 に指示します。ライブラリはクラスの名前に依存せず、Class オブジェクトを直接取得するため、これは安全です。
    • ,allowshrinking: この修飾子は、クラスが未使用の場合に削除できることを R8 に指示します。これにより、R8 は TaskRunner.execute() に渡されない StartupTask の実装を安全に削除できます。つまり、このルールは、StartupTask を実装するクラスをアプリが使用する場合、R8 はそのクラスを保持することを意味します。R8 は、クラスの名前を変更してサイズを削減したり、アプリで使用されていない場合はクラスを削除したりできます。
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: このルールは、最初のルールで特定されたクラスの特定のメンバー(この場合はコンストラクタ)をターゲットにします。
    • -keepclassmembers class * implements com.example.library.StartupTask: StartupTask インターフェースを実装するクラスの特定のメンバー(メソッド、フィールド)を保持します。ただし、実装されたクラス自体が保持されている場合に限ります。
    • { <init>(); }: これはメンバー セレクタです。<init> は、Java バイトコードのコンストラクタの特別な内部名です。この部分は、デフォルトの引数なしコンストラクタを特に対象としています。
    • このルールは重要です。コードは引数なしで getDeclaredConstructor().newInstance() を呼び出し、デフォルトのコンストラクタをリフレクションで呼び出すためです。このルールがないと、R8 は new PreCacheTask() を直接呼び出すコードがないと判断し、コンストラクタが未使用であると想定して削除します。これにより、アプリは実行時に InstantiationException でクラッシュします。

メソッド アノテーションに基づくリフレクション

ライブラリでは、デベロッパーがメソッドやフィールドにタグ付けするために使用するアノテーションが定義されることがよくあります。ライブラリは、リフレクションを使用して、実行時にこれらのアノテーション付きメンバーを検索します。たとえば、@OnLifecycleEvent アノテーションは、実行時に必要なメソッドを見つけるために使用されます。

たとえば、ライブラリと、そのライブラリを使用するアプリがある次のシナリオを考えてみましょう。この例では、@OnEvent でアノテーションが付けられたメソッドを検索して呼び出すイベントバスを示します。

ライブラリ コードは次のとおりです。

@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) { /* ... */ }
            }
        }
    }
}

ライブラリを使用するアプリには、次のコードがあります。

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

ライブラリには、アノテーションを使用するメソッドを自動的に保持するコンシューマー保持ルールファイルを含める必要があります。

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: このルールは、実行時に読み取られるアノテーションを保持します。
  • -keep @interface com.example.library.OnEvent: このルールは、OnEvent アノテーション クラス自体を保持します。
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: このルールは、クラスが使用されており、クラスに特定のメンバーが含まれている場合にのみ、クラスと特定のメンバーを保持します。
    • -keepclassmembers: このルールは、クラスが使用されており、クラスに特定のメンバーが含まれている場合にのみ、クラスと特定のメンバーを保持します。
    • class *: ルールはすべてのクラスに適用されます。
    • @com.example.library.OnEvent <methods>;: @com.example.library.OnEvent アノテーションが付いた 1 つ以上のメソッド(<methods>)を持つクラスを保持し、アノテーションが付いたメソッド自体も保持します。

クラス アノテーションに基づくリフレクション

ライブラリはリフレクションを使用して、特定のアノテーションを持つクラスをスキャンできます。この場合、タスクランナー クラスは、リフレクションを使用して ReflectiveExecutor アノテーションが付けられたすべてのクラスを検索し、execute メソッドを実行します。

たとえば、ライブラリと、そのライブラリを使用するアプリがあるシナリオを考えてみましょう。

ライブラリには次のコードがあります。

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

ライブラリを使用するアプリには、次のコードがあります。

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

このライブラリは、リフレクションを使用して特定のクラスを取得するため、次の keep ルールを含むコンシューマー keep ルール ファイルを含める必要があります。

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

この構成は、R8 に保持するものを正確に伝えるため、非常に効率的です。

オプションの依存関係をサポートするリフレクション

リフレクションの一般的なユースケースは、コア ライブラリとオプションのアドオン ライブラリの間にソフトな依存関係を作成することです。コア ライブラリは、アドオンがアプリに含まれているかどうかを確認し、含まれている場合は追加機能を有効にできます。これにより、コア ライブラリにアドオン モジュールへの直接的な依存関係を強制することなく、アドオン モジュールを配布できます。

コア ライブラリは、リフレクション(Class.forName)を使用して、名前で特定のクラスを検索します。クラスが見つかった場合、この機能は有効になります。そうでない場合は、正常に失敗します。

たとえば、次のコードでは、コア AnalyticsManager がオプションの VideoEventTracker クラスをチェックして、動画分析を有効にしています。

コア ライブラリには次のコードが含まれています。

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

オプションの動画ライブラリには次のコードがあります。

package com.example.analytics.video

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

オプション ライブラリのデベロッパーは、必要なコンシューマー保持ルールを提供する責任があります。この keep ルールにより、オプション ライブラリを使用するアプリは、コアライブラリが検索する必要があるコードを保持します。

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

このルールがないと、R8 はオプション ライブラリから VideoEventTracker を削除する可能性があります。これは、そのモジュール内の何も VideoEventTracker を直接使用していないためです。keep ルールはクラスとそのコンストラクタを保持し、コア ライブラリが正常にインスタンス化できるようにします。

プライベート メンバーにアクセスするためのリフレクション

リフレクションを使用して、ライブラリの公開 API の一部ではないプライベート コードまたは保護されたコードにアクセスすると、重大な問題が発生する可能性があります。このようなコードは予告なく変更される可能性があり、アプリケーションで予期しない動作やクラッシュが発生する可能性があります。

非公開 API にリフレクションを使用すると、次の問題が発生する可能性があります。

  • ブロックされた更新: 非公開コードまたは保護されたコードを変更すると、ライブラリを上位バージョンに更新できなくなる可能性があります。
  • メリットを享受できない: 新機能、重要なクラッシュ修正、重要なセキュリティ アップデートを利用できない可能性があります。

R8 の最適化とリフレクション

ライブラリのプライベート コードまたは保護されたコードにリフレクションする必要がある場合は、R8 の最適化に十分注意してください。これらのメンバーへの直接参照がない場合、R8 はそれらが未使用であると想定し、削除または名前変更する可能性があります。これにより、ランタイム クラッシュが発生し、NoSuchMethodException や NoSuchFieldException などの誤解を招くエラー メッセージが表示されることがあります。

たとえば、ライブラリ クラスからプライベート フィールドにアクセスする方法を示す次のシナリオを考えてみましょう。

所有していないライブラリに次のコードがあります。

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

アプリに次のコードが含まれている。

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
}

R8 がプライベート フィールドを削除しないように、アプリに -keep ルールを追加します。

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: クラス自体が保持されている場合にのみ、クラスの特定のメンバーを保持します。
  • class com.example.LibraryClass: フィールドを含む正確なクラスをターゲットにします。
  • private java.lang.String secretMessage;: 名前と型で特定のプライベート フィールドを識別します。

Java Native Interface(JNI)

R8 の最適化は、ネイティブ(C/C++ コード)から Java または Kotlin へのアップコールを処理する際に問題が発生する可能性があります。逆の場合(Java または Kotlin からネイティブ コードへのダウンコール)も問題が発生する可能性がありますが、デフォルトのファイル proguard-android-optimize.txt には、ダウンコールを機能させるための次のルールが含まれています。このルールは、ネイティブ メソッドがトリミングされるのを防ぎます。

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

Java Native Interface(JNI)を介したネイティブ コードとのインタラクション

アプリが JNI を使用してネイティブ(C/C++)コードから Java または Kotlin へのアップコールを行う場合、R8 はネイティブ コードから呼び出されるメソッドを認識できません。アプリでこれらのメソッドが直接参照されていない場合、R8 はこれらのメソッドが使用されていないと誤って判断して削除し、アプリがクラッシュします。

次の例は、ネイティブ ライブラリから呼び出すことを目的としたメソッドを含む Kotlin クラスを示しています。ネイティブ ライブラリは、アプリケーション タイプをインスタンス化し、ネイティブ コードから 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")
        }
    }
}

この場合、アプリケーション タイプが最適化されないように R8 に指示する必要があります。また、ネイティブ コードから呼び出されるメソッドが、シグネチャ内で独自のクラスをパラメータまたは戻り値の型として使用している場合は、それらのクラスの名前が変更されていないことも確認する必要があります。

アプリに次の keep ルールを追加します。

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

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

これらの keep ルールにより、R8 が onNativeEvent メソッドとそのパラメータ タイプ(重要)を削除または名前変更することを防ぎます。

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: クラスが Kotlin または Java コードで最初にインスタンス化された場合にのみ、クラスの特定のメンバーを保持します。これは、アプリがクラスを使用していること、およびクラスの特定のメンバーを保持する必要があることを R8 に伝えます。
    • -keepclassmembers: クラスが Kotlin または Java コードで最初にインスタンス化された場合にのみ、クラスの特定のメンバーを保持します。これは、アプリがクラスを使用していること、およびクラスの特定のメンバーを保持する必要があることを R8 に伝えます。
    • class com.example.JniBridge: フィールドを含む正確なクラスをターゲットにします。
    • includedescriptorclasses: この修飾子は、メソッドのシグネチャまたは記述子で見つかったクラスも保持します。この場合、R8 はパラメータとして使用される com.example.models.NativeData クラスの名前変更や削除を行いません。NativeData の名前が変更された場合(たとえば a.a に変更された場合)、メソッド シグネチャがネイティブ コードの想定と一致しなくなり、クラッシュが発生します。
    • public void onNativeEvent(com.example.models.NativeData);: 保持するメソッドの正確な Java シグネチャを指定します。
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: includedescriptorclasses は NativeData クラス自体が保持されるようにしますが、ネイティブ JNI コードから直接アクセスされる NativeData 内のメンバー(フィールドまたはメソッド)には、独自の保持ルールが必要です。
    • -keep class NativeData: NativeData という名前のクラスを対象とし、ブロックは NativeData クラス内のどのメンバーを保持するかを指定します。
    • <init>(java.lang.Integer, java.lang.String): コンストラクタのシグネチャです。これは、2 つのパラメータ(1 つ目は Integer、2 つ目は String)を受け取るコンストラクタを一意に識別します。

また、ネイティブ コードが呼び出すメソッドに、keep ルールを記述する代わりに、Jetpack Keep の @UnconditionallyKeep アノテーションを付けることもできます。アノテーションは NativeData などのメソッドのシグネチャ内のクラスを保持しないため、これらのクラスは別途保持する必要があります。

間接プラットフォーム呼び出し

Parcelable の実装を使用してデータを転送する

Android フレームワークは、リフレクションを使用して Parcelable オブジェクトのインスタンスを作成します。最新の Kotlin 開発では、kotlin-parcelize プラグインを使用する必要があります。このプラグインは、フレームワークが必要とする CREATOR フィールドやメソッドなど、必要な Parcelable 実装を自動的に生成します。

たとえば、kotlin-parcelize プラグインを使用して 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

このシナリオでは、推奨される保持ルールはありません。kotlin-parcelize Gradle プラグインは、@Parcelize でアノテーションを付けたクラスに必要な保持ルールを自動的に生成します。複雑な処理が自動的に行われ、生成された CREATOR とコンストラクタが Android フレームワークのリフレクション呼び出し用に保持されます。

@Parcelize を使用せずに Kotlin で Parcelable クラスを手動で作成する場合は、CREATOR フィールドと Parcel を受け入れるコンストラクタを保持する必要があります。これを忘れると、システムがオブジェクトの逆シリアル化を試みたときにアプリがクラッシュします。@Parcelize を使用するのが標準的で安全な方法です。

kotlin-parcelize プラグインを使用する場合は、次の点に注意してください。

  • このプラグインは、コンパイル時に CREATOR フィールドを自動的に作成します。
  • proguard-android-optimize.txt ファイルには、これらのフィールドを保持して適切な機能を実現するために必要な keep ルールが含まれています。
  • アプリ デベロッパーは、必要な keep ルールがすべて存在することを確認する必要があります。特に、カスタム実装やサードパーティの依存関係については、この確認が重要です。

リフレクションまたはバイトコード変換を使用するライブラリは、実行時にコードに動的にアクセスします。R8 がこのようにアクセスされるクラス、フィールド、メソッドを削除または名前変更すると、アプリがクラッシュする可能性があります。

ただし、一般的なサードパーティ ライブラリ(Gson、Retrofit、Kotlinx Serialization など)は、独自の R8 コンシューマー保持ルールを自動的にバンドルします。これらのライブラリの最新バージョンを使用している場合は、プロジェクトに手動で保持ルールを追加する必要はありません。

Gson

Gson は、リフレクションに大きく依存する JSON シリアル化 / デシリアル化ライブラリです。フルモードを使用してアプリを最適化すると、明示的に指示しない限り、汎用型シグネチャ、デフォルト コンストラクタ、アノテーションのないフィールドが削除されます。

Gson が正しく機能するように、データモデル クラスに非一時フィールドを保持し、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

transient 修飾子でマークされたフィールドは、シリアル化と逆シリアル化の際に Gson によって無視されます。そのため、保持ルールは非一時的なフィールド(!transient)を対象としています。

Retrofit

Retrofit は、HTTP アノテーション(@GET や @POST など)でアノテーションが付けられたサービス インターフェース メソッドをリフレクションを使用して検査し、ネットワーク リクエストを構築してレスポンスを変換するネットワーキング ライブラリです。

Retrofit は、Proxy.newProxyInstance() を使用して、実行時に API インターフェースの実装を動的に生成します。R8 は、これらのインターフェースを静的に実装するクラスを認識しないため、メソッドまたはその汎用戻り値の型を削除する可能性があります。

バンドルされた保持ルール

Retrofit は、ランタイム リフレクションを使用して、ジェネリック パラメータ、メソッド アノテーション、パラメータ アノテーションを検査します。適切な構成がない場合、R8 フルモードでは、戻り値の型、Kotlin 継続、レスポンス クラスから汎用シグネチャが徹底的に削除されたり、Retrofit インターフェースがプロキシで動的にインスタンス化されるため、インターフェース値が null に置き換えられたりする可能性があります。

Retrofit 2.10.0 以降、ライブラリは、アノテーションのデフォルト、サービス メソッド パラメータ、必要なクラス メタデータを保持するために必要な公式の保持ルールを自動的にバンドルします。詳しくは、Retrofit で使用されるルールをご覧ください。

汎用的な戻り値の型を保持する

Retrofit は、戻り値の型の汎用シグネチャ(Observable<Data> など)を検査して、ネットワーク レスポンスを正しく逆シリアル化します。R8 が汎用シグネチャを削除すると、Retrofit はインスタンス化されたオブジェクトを null に置き換えます。

R8 フルモードで戻り値の型の汎用シグネチャが削除されないようにするには、次の条件付きルールを使用します。

# 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>

返される実際のデータモデル クラス(Observable<Data> の Data など)も、コンバータ(Gson など)によってリフレクションを使用して構築されるため、保持する必要があります。

コルーチン

Kotlin コルーチンを使用すると、Kotlin コンパイラは、コンパイルされたメソッド シグネチャに Continuation パラメータを追加して、suspend 関数を変換します。

Retrofit などのライブラリが suspend 関数のジェネリック シグネチャをリフレクションで読み取る場合、その Continuation パラメータに依存します。フルモードを使用する場合、Signature 属性は明示的に保持されるクラスに対してのみ保持されます。Continuation は合成パラメータであるため、R8 はデフォルトでそのシグネチャを削除し、リフレクションを中断します。

署名の削除を防ぎ、フルモードでのランタイム互換性を確保するには、次のルールを追加します。

# Keep the signature attribute globally
-keepattributes Signature

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