Code shrinking is an approach that allows us to generate smaller APKs by removing unused code or refactoring existing code, resulting in a smaller footprint. In addition to shrinking, obfuscating is another tactic that allows us to guard our Android apps against reverse engineering.
Using both of these strategies will ensure that your app is faster to download and more difficult to modify by others.
In this post, we’ll cover:
In the early versions of Android, code shrinking and optimization were delegated to a tool called Proguard. However, since Android Gradle Plugin (AGP) v 3.4.0, Android has used the R8 compiler.
While both tools help with code compaction, R8 has richer functionality than code shrinking. For starters, R8 has limited support for Kotlin, whereas Proguard was built for Java toolchains. R8 achieves better inlining and outlining (extracting common code into a function) than Proguard, whereas the latter is better at propagating constant arguments.
Speaking of the actual code compaction process, R8 performs better by achieving 10 percent compaction, as opposed to 8.5 percent for Proguard.
The R8 compiler does various things to reduce the size of your final APK. Some of these include:
Once all of the above steps are completed, R8 converts the bytecode into dexcode by a process called dexing. Earlier, this was a part of D8 compiler, but has now been integrated into the R8 compiler.
Now that we know a bit about the R8 compiler, let’s see how code shrinking actually works.
In Android, we can configure code shrinking by setting the minifyEnabled
flag as true
in your build.gradle
file. Optionally, you may also enable shrinkResources
to remove unneeded resources.
buildTypes{ release{ minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt '), 'proguard-rules.pro' } }
Code shrinking starts by examining what are called entry points. Entry points are declared in a config file and made available via the proguardFiles
parameter in the build.gradle
.
Once R8 has a set of entry points, it begins searching for all classes and entities that are reachable from these entry points. It proceeds to build a list of such tokens. Any token that isn’t reachable is stripped from the final output.
This process is generally not foolproof because:
Many of these entry points are defined in the proguard-android-optimize.txt
file made available via the AGP plugin. Here’s a partial snapshot of what it looks like:
Let’s go over what the two rules above mean:
View
, thus retaining the View
classes as wellView
parameter, namely click listeners used in the XML, which are looked up reflectivelyNext, let’s get to know the schema that powers R8.
Though we’ll be discussing these as Proguard rules, they are the same rules that configure R8 as well. Let’s take deeper dive into how to write them.
A typical R8 or Proguard rule consists of three sections:
keep
ensures we retain the target that matches the rulekeepclass
ensures we retain the class that matches the rulekeepclasswithmembers
retains classes whose members match the rulekeepclassmembers
to retain only the members of a classclass
, enum
or interface
?
: Matches a single character in a name. So, for a rule likekeep class T???Provider
, we must ensure we retain both the TaskProvider
and TrapProvider
classes*
: Matches any part of a name excluding the package separator. This ensures a rule like keep class com.demo.*Provider extends ActionProvider
matches com.demo.TaskProvider
, but doesn’t match com.demo.internal.StorageProvider
**
: Matches any part of a name including the package separator. In the above example, it would even match the StorageProvider
class<n>
: Allows us to match dynamic elements within our rule. For example, if we wish to have classes that fit the following template:
class TaskProvider { fun getTaskKey(): String } class StorageProvider { fun getStorageKey(): String }
we can write:
-keepclasseswithmembers class *Provider { public java.lang.String get<1>Key(); }
This is because our first wildcard matcher, *
, matches Task
and Storage
, which we can reuse to define the dynamic parts of our function’s name.
The R8 or Proguard rules shipped via AGP are generally sufficient, however, a need may arise to write your own rules. While writing R8 rules, we should strive to avoid including more than what’s needed in our keep rules to ensure that we can compress most of our code. Also, all classes we specify need to be fully qualified, i.e., they must include the package name.
Typically, enums used in XML files are the culprits stripped away by R8. But we can define our own rule to keep them, as follows:
-keep enum com.demo.main.MediaType{ *; }
Note: The
{*;}
in the braces implies that we intend to preserve all members of the class/enum.
Another rule is to preserve class constructors; here’s how you’d do it using the keyword init
:
-keep public class * extends android.view.View { public <init>(android.content.Context); }
Occasionally, there may be entities included in your app that are reflectively looked up via their fully qualified name within jars. You’d want to preserve only the names and prevent R8 from obfuscating or renaming the class. You can retain names by using the keepnames
qualifier:
-keepnames class com.ext.library.ServiceProvider
Another way to retain classes is to annotate them with the @Keep
annotation. These classes are retained via the androidx.annotation
library Proguard rule. However, you can only use this on source code you control; additionally, this is a more generic solution and will result in the inclusion of members that aren’t used.
Resource shrinking is typically done after code shrinking, but instead of using Proguard rules, we can specify resource retention using a keep.xml
in our res/raw
folder. We generally do not need this unless we are looking for resources via Resources.getIdentifier()
.
In such cases, the resource shrinker behaves conservatively. Below is an example:
val name = String.format("ic_%1d", angle + 1) val res = resources.getIdentifier(name, "drawable", packageName)
The shrinker uses pattern matching and retains all assets, starting with ic_
. We can also retain some assets explicitly in our keep.xml
, as follows:
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/ic_sport*, @drawable/ic_banner_option, @layout/item_header" tools:discard="@drawable/wip" />
Note: The
discard
option ensures thewip
is removed from the final build if unused.
Occasionally while using R8, you’ll end up with a missing resource error in the form of ClassNotFoundException
or FieldNotFoundException
. However, since the trace is obfuscated, we’ll need to use a tool called retrace.
Retrace is usually present on the following path: Android/sdk/tools/proguard/bin
. You may optionally use the GUI-based route by using the proguardgui.sh command, as shown below:
Once you’ve figured out which class or member is causing this issue, you can easily fix this by including a specific keep rule for it:
-keep class com.demo.activities.MainActivity
R8 generally strips meta properties like line numbers and source file names. We can retain this information by using keepattributes
, as shown in the below rule:
-keepattributes SourceFile, LineNumberTable
You can find the complete list of attributes here.
Occasionally, you may see that a member that was supposed to be removed from the final APK has not actually been removed. We can figure out why by using whyareyoukeeping
:
-whyareyoukeeping class com.android.AndroidApplication
This will print the below output:
com.android.AndroidApplication |- is referenced in keep rule: | /Users/anvith/Development/Android/project-demo/app/build/intermediates/aapt_proguard_file/release/aapt_rules.txt:3:1
Another useful tool while debugging is to list all the unused classes. This can be done using printusage
, as follows:
-printusage
A quick note about R8 rules: The most broad rules take precedence. So, if
libraryA
ships with a rule to include one method of a class, and `libraryB` is shipped with a rule to include all members,libraryB
’s rule takes precedence.
Lastly, if you wish to see classes matched by your rules, you may use the following command to observe the matched results:
-printseeds
We can instruct R8 to be more aggressive by letting it run in non-compat mode and declaring the following property in the gradle.properties
file:
android.enableR8.fullMode=true
This flag results in some of the more rigorous optimizations, like:
Signature
, Annotations
, etc.) are only retained for matching classes, even if we specify the generic keepattributes
for all entitiesSimilar to the code shrinking option, there is an aggressive resource shrink mode that can be added to the keep.xml
:
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />
In this article, we learned about R8 and how to configure rules for it. In the process, we also covered various debugging options to address the perils of aggressive shrinking.
I hope you’ve found the information in this article useful for addressing your code shrinking concerns and are ready to leverage the R8 toolchain!
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]