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 MyWorker
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.