次の例は、R8 を最適化に使用する一般的なシナリオに基づいていますが、キープルールの作成には高度なガイダンスが必要です。
気分
一般に、最適なパフォーマンスを得るために、リフレクションの使用はおすすめしません。ただし、特定のシナリオでは、避けられない場合があります。次の例は、リフレクションを使用する一般的なシナリオでのキープルールのガイダンスを示しています。
名前で読み込まれたクラスによるリフレクション
ライブラリは、クラス名を 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 がクラスを削除することはなくなりますが、クラスがリフレクションによってインスタンス化されることを宣言し、コンストラクタなど、リフレクションによってアクセスされるメンバーを保護するには、キープルールを使用する必要があります。
たとえば、ライブラリと、そのライブラリを使用するアプリがあるシナリオを考えてみましょう。ライブラリ ローダーは、クラス参照を直接渡すことで 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>();
}
これらのルールは、このタイプのリフレクションと完全に連携するように設計されており、コードが正しく動作することを保証しながら、最大限の最適化を実現します。このルールにより、アプリで 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)
}
ライブラリには、アノテーションを使用するメソッドを自動的に保持するコンシューマー保持ルールファイルを含める必要があります。
-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>;
: 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)
}
ライブラリはリフレクションを使用して特定のクラスを取得するため、ライブラリには次の保持ルールを含むコンシューマー保持ルール ファイルを含める必要があります。
# 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 { /* ... */ }
}
オプション ライブラリのデベロッパーは、必要なコンシューマー保持ルールを提供する責任があります。この保持ルールにより、オプション ライブラリを使用するアプリは、コア ライブラリが検索する必要があるコードを確実に保持します。
# 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
}
アプリに -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 に通知する必要があります。また、ネイティブ コードから呼び出されるメソッドが、シグネチャ内で独自のクラスをパラメータまたは戻り値の型として使用している場合は、それらのクラスの名前が変更されていないことも確認する必要があります。
アプリに次の保持ルールを追加します。
-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
)を受け取るコンストラクタを一意に識別します。
間接プラットフォーム呼び出し
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
ルールがすべて存在することを確認する必要があります。特に、カスタム実装やサードパーティの依存関係については確認が必要です。