Implementing R8 optimizations can always be a nightmare for the dev that has to do it. R8 is a compiler that works together with ProGuard to shrink, minify, and obfuscate your code. It can reduce significantly the APK size by removing classes that are not used at all. It checks the hierarchy of classes and once it sees a class is not being attached to anything, it removes it.
The above, of course, is a double-edged sword as in some cases, it can remove classes that you actually need like request/response classes and many others. But in a recent project, we got like 10 megabytes of reduced APK size (down from 30) which is a huge gain.
The problem comes when you want to tell R8 to keep certain classes that you need either deobfuscated or simply kept within your dex archive. So let’s see what the approach could be in such cases.
Keeping Request / Response classes
The rule of thumb is that you need to keep the data classes that you use to communicate with your server. These classes usually have names like api_version, first_name, etc., etc. and you want to match what the backend expects to receive. You can imagine how api_version will be obfuscated to 0qke and then the backend will throw a validation exception that there is no such field.
Depending on the library that you use for serialization/deserialisation, you could easily Google for samples with it by simply using:
- Moshi Proguard
- GSON Proguard
- Kotlinx Serialisation Proguard
And from there on it is all about experimenting and checking the results.
Example with Moshi
Code-generation
Usually, such classes are data classes that could either have a suffix or could be identified by an annotation. Let’s think about Moshi as an example. We prefer to use Moshi-Codegen for these request classes to have adapters that use code-generation instead of reflection as code-generation is way faster than reflection. This means we annotate all of them with:
@JsonClass(generateAdapter = true)
By this way, we can identify all of the request/response classes and tell R8 to keep them as they are so the properties that we send to the backend are not obfuscated so instead of us sending:
api_version: String –> 0qke: String
To generate this, we can simply add the following rules for Moshi:
-keep @com.squareup.moshi.JsonQualifier @interface *
# Enum field names are used by the integrated EnumJsonAdapter.
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
**[] values();
*;
}
# Keep helper method to avoid R8 optimisation that would keep all Kotlin Metadata when unwanted
-keepclassmembers class com.squareup.moshi.internal.Util {
private static java.lang.String getKotlinMetadataClassName();
}
# Keep ToJson/FromJson-annotated methods
-keepclassmembers class * {
@com.squareup.moshi.FromJson <methods>;
@com.squareup.moshi.ToJson <methods>;
}
-keep,allowobfuscation,allowshrinking class com.squareup.moshi.JsonAdapter
The last 2 rules at the end of the above example are the rules that make Moshi keep classes that are code-generated by Moshi which have the FromJson and ToJson annotations. This is what Moshi uses when we apply the JsonClass annotation over our data classes.
Reflection
The other part of the rules make sure we keep enum values and various other classes that relate to the Moshi framework. If you are using Moshi-Sealed and reflection instead of code-generation, you may also want to keep the following runtime classes:
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes EnclosingMethod
-keepattributes InnerClasses
-keep class kotlin.Metadata { *; }
-keepattributes RuntimeVisibleAnnotations
This keeps the kotlin Metadata that is needed for reflection to work. Metadata contains information about properties and classes that is used in reflection and code-generation.
HTTP client configuration
The second thing that needs keeping is anything related to your http client. Depending on what you use, you can also use Google to search for appropriate samples.
Example with Retrofit
Retrofit has a very simple Proguard configuration that can be found in many Github issues:
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
This keeps all the necessary classes especially if you are using Retrofit + coroutines so that your interface methods are kept as they are.
Using @Keep annotations
You can always add a rule for certain classes to be kept by R8 in the proguard.txt file. But there is also an easier way to achieve the same thing by simply adding @Keep annotation above the classes that you want to be kept. In case of a sealed class, you also need to add the @Keep annotation above the sealed class implementations.
Usually, you shouldn’t need a lot of Keep annotations unless you have some special cases not handled by the proguard rules. Then it makes sense to add them. But having them spread throughout the codebase instead of having general proguard rules is not a good idea.
Searching online for libraries proguard configurations
By checking each individual library in your codebase, you can also do a simple Google search to find if it has some specific proguard configuration that you need to apply.
Example with Glide
If you want to find what rules you need to apply for Glide you simply search:
glide proguard rules
And you will get a bunch of results back. Although Glide already has ProGuard rules configured and you shouldn’t need to add anything, you can use the same example with other libraries that you use.
Debugging optimized code
A lot of times you will see many issues until you get the R8 optimization working right. To identify such issues, you can try to debug the final code to identify where the issue lies.
There are 3 things you need to check:
- Is it a resource issue?
- Is it an obfuscation issue?
- Is it an optimization issue?
Is it a resource issue?
Disable resource shrinking by using:
shrinkResources false
And run the app again. This will confirm whether the issue you experience is a resource one.
Is it an obfuscation issue?
Add the following in proguard-rules.pro
-dontobfuscate
to disable obfuscation. Run the app again and this will confirm whether it is an obfuscation issue.
Another way to keep obfuscation running is to use the retrace tool. It can be found in:
sdk/tools/proguard/lib
And a very simple way to use it is this one:
retrace mapping.txt stacktrace.txt
Where stacktrace.txt contains the obfuscated stacktrace that you need to deobfuscate. mapping.txt is the mapping that can be found in:
build/outputs/mapping/<variant>/mapping.txt
Is it an optimization issue?
By simply using:
Build -> Analyze APK
You can easily check the generated bytecode by selecting the APK and then selecting the classes.dex. This will indicate to you whether a certain part of your codebase was unnecessarily removed and that’s why you got the exception that will point you to the correct class that you need to check.