The following examples are based on common scenarios where you use R8 for optimization, but need advanced guidance to draft keep rules.
Reflection
In general, for optimum performance, it's not recommended to use reflection. However, in certain scenarios, it might be unavoidable. The following examples provide guidance for keep rules in common scenarios that use reflection.
Reflection with classes loaded by name
Libraries often load classes dynamically by using the class name as a String
. However, R8 cannot detect classes that are loaded in this manner, and might remove the classes it considers unused.
For example, consider the following scenario where you have a library and an app that uses the library- the code demonstrates a library loader that instantiates a StartupTask
interface implemented by an app.
The library code is as follows:
// 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 that uses the library has the following code:
// 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") }
In this scenario, your library should include a consumer keep rules file with the following keep rules:
-keep class * implements com.example.library.StartupTask { <init>(); }
Without this rule, R8 removes PreCacheTask
from the app because the app doesn't use the class directly, breaking the integration. The rule finds the classes that implement your library's StartupTask
interface and preserves them, along with their no-argument constructor, allowing the library to successfully instantiate and execute PreCacheTask
.
Reflection with ::class.java
Libraries can load classes by having the app pass the Class
object directly, which is a more robust method than loading classes by name. This creates a strong reference to the class that R8 can detect. However, while this prevents R8 from removing the class, you still need to use a keep rule to declare that the class is instantiated reflectively and to protect the members that are accessed reflectively, like the constructor.
For example, consider the following scenario in which you have a library and an app that uses the library- the library loader instantiates a StartupTask
interface by passing the class reference directly.
The library code is as follows:
// 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 that uses the library has the following code:
// 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) }
In this scenario, your library should include a consumer keep rules file with the following keep rules:
# 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>(); }
These rules are designed to work perfectly with this type of reflection, allowing for maximum optimization while making sure that the code works correctly. The rules let R8 obfuscate the class name and shrink, or remove, the implementation of the StartupTask
class if the app never uses it. However, for any implementation, such as the PrecacheTask
that is used in the example, they preserve the default constructor (<init>()
) that your library needs to call.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
: This rule targets any class that implements yourStartupTask
interface.-keep class * implements com.example.library.StartupTask
: This preserves any class (*
) that implements your interface.,allowobfuscation
: This instructs R8 that despite keeping the class, it can rename, or obfuscate, it. This is safe because your library doesn't rely on the class's name; it gets theClass
object directly.,allowshrinking
: This modifier instructs R8 that it can remove the class if it's unused. This helps R8 to safely delete an implementation ofStartupTask
that is never passed toTaskRunner.execute()
. In short, this rule implies the following: If an app uses a class that implementsStartupTask
, R8 keeps the class. R8 can rename the class to reduce its size and can delete it if the app doesn't use it.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: This rule targets specific members of the classes that were identified in the first rule—in this case, the constructor.-keepclassmembers class * implements com.example.library.StartupTask
: This preserves specific members (methods, fields) of the class that implementsStartupTask
interface, but only if the implemented class itself is being kept.{ <init>(); }
: This is the member selector.<init>
is the special internal name for a constructor in Java bytecode. This part specifically targets the default, no-argument constructor.- This rule is critical because your code calls
getDeclaredConstructor().newInstance()
without any arguments, which reflectively invokes the default constructor. Without this rule, R8 sees that no code directly callsnew PreCacheTask()
, assumes that the constructor is unused, and removes it. This causes your app to crash at runtime with anInstantiationException
.
Reflection based on method annotation
Libraries often define annotations that developers use to tag methods or fields. The library then uses reflection to find these annotated members at runtime. For example, the @OnLifecycleEvent
annotation is used to find the required methods at runtime.
For example, consider the following scenario in which you have a library and an app that uses the library- the example demonstrates an event bus that finds and invokes methods annotated with @OnEvent
.
The library code is as follows:
@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) { /* ... */ } } } } }
The app that uses the library has the following code:
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) }
The library should include a consumer keep rules file that automatically preserves any methods using its annotations:
-keepattributes RuntimeVisibleAnnotations -keep @interface com.example.library.OnEvent; -keepclassmembers class * { @com.example.library.OnEvent <methods>; }
-keepattributes RuntimeVisibleAnnotations
: This rule preserves annotations that are meant to be read at runtime.-keep @interface com.example.library.OnEvent
: This rule preserves theOnEvent
annotation class itself.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: This rule preserves a class and specific members only if the class is being used and the class contains those members.-keepclassmembers
: This rule preserves a class and specific members only if the class is being used and the class contains those members.class *
: The rule applies to any class.@com.example.library.OnEvent <methods>;
: This preserves any class that has one or more methods (<methods>
) annotated with@com.example.library.OnEvent
, and to also preserve the annotated methods themselves.
Reflection based on class annotations
Libraries can use reflection to scan for classes that have a specific annotation. In this case, the task runner class finds all the classes annotated with ReflectiveExecutor
using reflection and executes the execute
method.
For example, consider the following scenario where you have a library and an app that uses the library.
The library has the following code:
@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) } } }
The app that uses the library has the following code:
// 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) }
Because the library reflectively uses reflection to get specific classes, the library should include a consumer keep rules file with the following keep rules:
# 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(); }
This configuration is highly efficient because it tells R8 exactly what to preserve.
Reflection to support optional dependencies
A common use case for reflection is to create a soft dependency between a core library and an optional add-on library. The core library can check if the add-on is included in the app and, if it is, can enable extra features. This lets you ship add-on modules without forcing the core library to have a direct dependency on them.
The core library uses reflection (Class.forName
) to look for a specific class by its name. If the class is found, the feature is enabled. If not, it fails gracefully.
For example, consider the following code where a core AnalyticsManager
checks for an optional VideoEventTracker
class to enable video analytics.
The core library has the following code:
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()) } } }
The optional video library has the following code:
package com.example.analytics.video class VideoEventTracker { // This constructor must be kept for the reflection call to succeed. init { /* ... */ } }
The developer of the optional library is responsible for providing the necessary consumer keep rule. This keep rule makes sure that any app using the optional library preserves the code the core library needs to find.
# In the video library's consumer keep rules file -keep class com.example.analytics.video.VideoEventTracker { <init>(); }
Without this rule, R8 likely removes VideoEventTracker
from the optional library since nothing in that module directly uses it. The keep rule preserves the class and its constructor, letting the core library successfully instantiate it.
Reflection to access private members
Using reflection to access private or protected code that is not part of a library's public API can introduce significant problems. Such code is subject to change without notice, which can lead to unexpected behavior or crashes in your application.
When you rely on reflection for non-public APIs, you might encounter the following issues:
- Blocked updates: Changes in the private or protected code can prevent you from updating to higher library versions.
- Missed benefits: You might miss out on new functionality, important crash fixes, or essential security updates.
R8 optimizations and reflection
If you must reflect into a library's private or protected code, pay close attention to R8's optimizations. If there are no direct references to these members, R8 might assume they are unused and subsequently remove or rename them. This can lead to runtime crashes, often with misleading error messages such as NoSuchMethodException
or NoSuchFieldException
.
For example, consider the following scenario that demonstrates how you might access a private field from a library class.
A library that you don't own has the following code:
class LibraryClass { private val secretMessage = "R8 will remove me" }
Your app has the following code:
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 }
Add a -keep
rule in your app to prevent R8 from removing the private field:
-keepclassmembers class com.example.LibraryClass { private java.lang.String secretMessage; }
-keepclassmembers
: This preserves specific members of a class only if the class itself is retained.class com.example.LibraryClass
: This targets the exact class containing the field.private java.lang.String secretMessage;
: This identifies the specific private field by its name and type.
Java Native Interface (JNI)
R8's optimizations can have issues when working with upcalls from native (C/C++ code) to Java or Kotlin. While the reverse is also true—downcalls from Java or Kotlin to native code can have issues—the default file proguard-android-optimize.txt
includes the following rule to keep the downcalls working. This rule guards against native methods being trimmed.
-keepclasseswithmembernames,includedescriptorclasses class * { native <methods>; }
Interaction with native code through the Java Native Interface (JNI)
When your app uses JNI to make upcalls from native (C/C++) code to Java or Kotlin, R8 can't see which methods are called from your native code. If there are no direct references to these methods in your app, R8 incorrectly assumes that these methods are unused and removes them, causing your app to crash.
The following example shows a Kotlin class with a method intended to be called from a native library. The native library instantiates an application type and passes data from native code to the Kotlin code.
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") } } }
In this case, you must inform R8 to prevent the application type from being optimized. Additionally, if methods called from native code use your own classes in their signatures as parameters or return types, you must also verify that those classes are not renamed.
Add the following keep rules to your app:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge { public void onNativeEvent(com.example.model.NativeData); } -keep class NativeData{ <init>(java.lang.Integer, java.lang.String); }
These keep rules prevent R8 from removing or renaming the onNativeEvent
method and—critically—its parameter type.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}
: This preserves specific members of a class only if the class is instantiated in Kotlin or Java code first—it tells R8 that the app is using the class and that it should preserve specific members of the class.-keepclassmembers
: This preserves specific members of a class only if the class is instantiated in Kotlin or Java code first—it tells R8 that the app is using the class and that it should preserve specific members of the class.class com.example.JniBridge
: This targets the exact class containing the field.includedescriptorclasses
: This modifier also preserves any classes found in the method's signature, or descriptor. In this case, it prevents R8 from renaming or removing thecom.example.models.NativeData
class, which is used as a parameter. IfNativeData
were renamed (for example, toa.a
), the method signature would no longer match what the native code expects, causing a crash.public void onNativeEvent(com.example.models.NativeData);
: This specifies the exact Java signature of the method to preserve.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
: Whileincludedescriptorclasses
makes sure that theNativeData
class itself is preserved, any members (fields or methods) withinNativeData
that are accessed directly from your native JNI code need their own keep rules.-keep class NativeData
: This targets the class namedNativeData
and the block specifies which members inside theNativeData
class to keep.<init>(java.lang.Integer, java.lang.String)
: This is the constructor's signature. It uniquely identifies the constructor that takes two parameters: the first is anInteger
and the second is aString
.
Indirect platform calls
Transfer data with an implementation of Parcelable
The Android framework uses reflection to create instances of your Parcelable
objects. In modern Kotlin development, you should use the kotlin-parcelize
plugin, which automatically generates the necessary Parcelable
implementation, including the CREATOR
field and methods that the framework needs.
For example, consider the following example where the kotlin-parcelize
plugin is used to create a Parcelable
class:
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
In this scenario, there isn't a recommended keep rule. The kotlin-parcelize
Gradle plugin automatically generates the required keep rules for the classes you annotate with @Parcelize
. It handles the complexity for you, making sure that the generated CREATOR
and constructors are preserved for the Android framework's reflection calls.
If you write a Parcelable
class manually in Kotlin without using @Parcelize
, you are responsible for keeping the CREATOR
field and the constructor that accepts a Parcel
. Forgetting to do so causes your app to crash when the system tries to deserialize your object. Using @Parcelize
is the standard, safer practice.
When using the kotlin-parcelize
plugin, be aware of the following:
- The plugin automatically creates
CREATOR
fields during compilation. - The
proguard-android-optimize.txt
file contains the necessarykeep
rules to retain these fields for proper functionality. - App developers must verify that all required
keep
rules are present, especially for any custom implementations or third-party dependencies.