次の例は、R8 を使用して最適化を行う一般的なシナリオに基づいていますが、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 ルールを含むコンシューマー keep ルール ファイルを含める必要があります。
-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)
}
このシナリオでは、ライブラリに次の keep ルールを含むコンシューマー keep ルール ファイルを含める必要があります。
# 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>();
}
これらのルールは、このタイプのリフレクションで完全に機能するように設計されており、コードが正しく動作することを保証しながら、最大限の最適化を実現します。これらのルールにより、アプリが StartupTask クラスを使用しない場合、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)
}
ライブラリには、アノテーションを使用するメソッドを自動的に保持するコンシューマー keep ルール ファイルを含める必要があります。
-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>)を持つクラスが保持され、アノテーション付き メソッド自体も保持されます。@com.example.library.OnEvent
クラス アノテーションに基づくリフレクション
ライブラリはリフレクションを使用して、特定の
アノテーションを持つクラスをスキャンできます。この場合、タスク ランナー クラスはリフレクションを使用して 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 ルールを提供する必要があります。この keep ルールにより、オプションのライブラリを使用するアプリは、コアライブラリが検索する必要があるコードを保持します。
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
このルールがないと、そのモジュール内の何も直接使用しないため、R8 はオプションのライブラリから 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
}
アプリに -keep ルールを追加して、R8 が非公開フィールドを削除しないようにします。
-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: この修飾子は、メソッドのシグネチャまたは記述子で見つかったクラスも保持します。この場合、パラメータとして使用されるcom.example.models.NativeDataクラスの名前変更や削除が R8 によって行われるのを防ぎます。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 ルールが必要です。-keep class NativeData: これはNativeDataという名前のクラスを対象とし、ブロックはNativeDataクラス内で 保持するメンバーを指定します。<init>(java.lang.Integer, java.lang.String): これは コンストラクタのシグネチャです。2 つのパラメータを受け取るコンストラクタを一意に識別します。1 つ目はInteger、2 つ目はStringです。
間接的なプラットフォーム呼び出し
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
このシナリオでは、推奨される keep ルールはありません。kotlin-parcelize Gradle プラグインは、@Parcelize アノテーション付きのクラスに必要な keep ルールを自動的に生成します。複雑な処理はプラグインが行い、生成された CREATOR とコンストラクタが Android フレームワークのリフレクション呼び出し用に保持されるようにします。
Kotlin で Parcelable クラスを @Parcelize を使用せずに手動で作成する場合は、CREATOR フィールドと Parcel を受け取るコンストラクタを保持する必要があります。これを忘れると、システムがオブジェクトのシリアル化を解除しようとしたときにアプリがクラッシュします。@Parcelize を使用するのが標準的で安全な方法です。
kotlin-parcelize プラグインを使用する場合は、次の点に注意してください。
- このプラグインは、コンパイル時に
CREATORフィールドを自動的に作成します。 proguard-android-optimize.txtファイルには、これらのフィールドを保持して適切に機能させるために必要なkeepルールが含まれています。- アプリ デベロッパーは、必要な
keepルールがすべて存在することを確認する必要があります。特に、カスタム実装やサードパーティの依存関係については注意が必要です。
人気のライブラリ
リフレクションまたはバイトコード変換を使用するライブラリは、実行時にコードに動的にアクセスします。R8 がこの方法でアクセスされるクラス、フィールド、メソッドを削除または名前変更すると、アプリがクラッシュする可能性があります。
ただし、Gson、Retrofit、Kotlinx Serialization などの一般的なサードパーティ ライブラリには、独自の R8 コンシューマー keep ルールが自動的にバンドルされます。これらのライブラリの最新バージョンを使用している場合は、プロジェクトに手動で keep ルールを追加する必要はありません。
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 によって無視されます。そのため、keep ルールは非一時的なフィールド(!transient)を対象としています。
Retrofit
Retrofit は、リフレクションを使用して HTTP アノテーション(@GET や @POST など)でアノテーションが付けられたサービス インターフェース メソッドを検査し、ネットワーク リクエストを構築してレスポンスを変換するネットワーク ライブラリです。
Retrofit は、Proxy.newProxyInstance() を使用して、実行時に API インターフェースの実装を動的に生成します。R8 はこれらのインターフェースを静的に実装するクラスを認識しないため、メソッドまたはその汎用的な戻り値の型を削除する可能性があります。
バンドルされた keep ルール
Retrofit は、実行時のリフレクションに依存して、汎用パラメータ、メソッド アノテーション、パラメータ アノテーションを検査します。適切な構成がない場合、R8 の完全モードでは、戻り値の型、Kotlin 継続、レスポンス クラスから汎用シグネチャが完全に削除される可能性があります。また、Retrofit インターフェースはプロキシで動的にインスタンス化されるため、インターフェース値が null に置き換えられることもあります。
Retrofit 2.10.0 以降では、アノテーションのデフォルト、サービス メソッド パラメータ、必要なクラス メタデータを保持するために必要な公式の keep ルールがライブラリに自動的にバンドルされます。詳しくは、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>
返される実際のデータモデル クラス(Data の
Observable<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