Mastodon

Android's Built-in ProGuard Rules: The Missing Guide

Android's build tools come with a few ProGuard facets to squeeze the most juice out of your release builds. In this post we'll cover what they all are, what's inside 'em, and what you can do with this knowledge!

Android's Built-in ProGuard Rules: The Missing Guide

Note: This post is a little long and intended to be an information dump. Feel free to skip sections if they're not relevant to you!

The default configs

A lot of projects declare one of either proguard-android.txt or proguard-android-optimize.txt and use them sort of like this:

android {
  buildTypes {
    release {
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
    }
  }
}

getDefaultProguardFile() is a simple helper method that fetches them out of build/intermediates/proguard-files. The Android Gradle Plugin (AGP) puts them there.

What's in these files? Let's go through them section-by-section.

First up: proguard-android.txt

-dontoptimize

As you'd expect, this tells the shrinker to not optimize!

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Preserve some attributes that may be required for reflection.
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod

Exactly what it looks like! Bear in mind that * is a wildcard, so there's actually multiple different attributes with Annotation in the name that this is matching on.

-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService

These are miscellaneous Play Services things, I suspect they predate AARs that could package consumer ProGuard rules.

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
    native <methods>;
}

This keeps native methods' names for linking at runtime. keepclasseswithmembernames is somewhat nuanced - it keeps both the class and native methods but allows for their removal during shrinking if neither are used. Only applicable in obfuscation.

Edit: It's worth noting that this rule was incomplete and a fixed version will be included AGP 4.1 canary 4. You can manually update this yourself  in your own config though.

# Keep setters in Views so that animations can still work.
-keepclassmembers public class * extends android.view.View {
    void set*(***);
    *** get*();
}

This is for some animation APIs that could fall back to reflection to invoke their setter/getters, like ObjectAnimator.ofInt(view, "scaleX"). You can avoid needing this by just using the relevant properties instead (e.g. View.SCALE_X).

Here we have different rule - keepclassmembers. This keeps the members only, allowing for the class (and its members) to be removed in shrinking if the class isn't used. Enabling that property saved us ~400 dex methods in the Slack app.

# We want to keep methods in Activity that could be used in the XML attribute onClick.
-keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
}

Here we have a relic of very old Android days. Remember how you can specify android:onClick in an xml view and it could invoke a corresponding method in the Activity it runs in? This is what makes that work with optimization (otherwise it could look unused) and obfuscation (since it has to look it up by name!).

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

This keeps the magic values() and valueOf() methods for enums. While this rule is quite greedy (all enums are opted in), I think it's probably worth it due to the ubiquitous and often surprising use of these methods in code.

-keepclassmembers class * implements android.os.Parcelable {
    public static final ** CREATOR;
}

This is for Parcelable types, whose CREATOR fields are looked up at runtime via reflection.

-keepclassmembers class **.R$* {
    public static <fields>;
}

This is for the resources table (aka R.class).  This is so Resources can look them up by name. The $ is to cover inner classes (layout, id, etc).

According to Jake Wharton on Twitter, this might just be something for resource shrinking and actually removable.

# Preserve annotated Javascript interface methods.
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

This is for users of @JavascriptInterface, as the method names are looked up reflectively.

# The support libraries contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**

The comment here explains it. I think the AndroidX versions at least could be moved into the androidx libraries since jars and aars can package their own rules now.

# This class is deprecated, but remains for backward compatibility.
-dontwarn android.util.FloatMath

These methods were deprecated, but still exist in the runtime. Presumably this is here for older libraries that possibly still reference them. See https://developer.android.com/reference/android/util/FloatMath

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @androidx.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @androidx.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

-keepclasseswithmembers class * {
    @androidx.annotation.Keep <init>(...);
}

This is a large block, but they're all for the @Keep annotation. AndroidX's annotations artifact packages these rules in its own jar too. These two do sort of two things. One is to keep the @Keep annotation itself (because if you run multiple passes of optimizations, ProGuard may remove them in the first pass and think they're not present in a subsequent one!). The second is that these are defined individually for each member type (constructors, methods, fields). This may look overly verbose, but it's because of keepclasswithmembers. If they were all defined in one, then it would only keep them if the class had all members like that.

# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-dontnote org.apache.http.**
-dontnote android.net.http.**

These are related to the bootclasspath tricks that AGP does to put the legacy apache http library on the classpath, just so Proguard doesn't complain about them.

Optimize: proguard-android-optimize.txt

The only differences between proguard-android-optimize.txt and the one we just covered are that dontoptimize is removed and the following block is added:

-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification

Let's break this down.

-allowaccessmodification is the option to allow ProGuard to change access modification, which can help it in other optimizations. Classic example: consider a standard POJO with private fields and public getters. If allowed to change access, ProGuard can change the getter calls to access the field directly.

-optimizationpasses indicates how many passes it takes, with the idea that more passes means squeezing more juice. They can have a compounding effect too, where one pass can optimize something in a way that enables subsequent passes to build on.

-optimizations is a comma-separated value of optimizations to not run. They can be thought of as increasingly specific categories. code/simplification/arithmetic is just that optimization. field/* is all field optimizations.

It's important to note that optimizations and optimizationpasses are only applicable in ProGuard. R8 is single-pass and doesn't allow fine-grained control over which optimizations it runs (details).

The three optimizations here are as follows:

At Slack - we were fine with just disabling field optimizations, and even that was just temporary until we could investigate further (we ended up moving to R8 instead).

A closer look at optimization passes

In nontrivial projects, ProGuard can take a significant amount of time. I think most Android developers are used to this because it's built in and how the optimized config has always worked. But as we see above - R8 runs in a single pass. Does it have a secret sauce that ProGuard doesn't? Is ProGuard squeezing more juice out of this process if we run it with 5 passes?

As always, the answer is it depends. But, let me tell you my observations: the first pass with ProGuard gives you 90-95% of the benefit in terms of APK size and method counts.

There, I said it. Is that 5-10% worth the extra passes? In a nontrivial project, I don't think so. In the case of Slack, we lowered our release build time by nearly 10 minutes when switching to 1 pass. Spread over a few dozen developers, that's a lot of time back in their pockets waiting for CI.

Another observation - this single-pass change from many ProGuard -> R8 migrations also likely explains why R8 is conventionally receiving rave reviews in the community for its speed. It's more likely that it's just the single pass change that's making up the time difference. The R8 minify task in Gradle ran ~30 seconds faster than its single pass ProGuard predecessor, but would have looked like it was 10.5 minutes faster if we hadn't made ProGuard single pass first!

Embedded rules

Another consideration is that your project can merge in rules published with external libraries you consume. In Android library projects, this works via consumerProguardRules() in AGP. In plain Java/Kotlin projects, this can be done via embedding ProGuard rules in the resulting .jar's resources directory.

AAR example: Picasso rules and Gradle config

Jar example: OkHttp

Tooling rules

AGP can also generate dynamic rules on the fly and include them in your configuration under the hood! A good example of this is the previously mentioned minimal rules for aapt. Another example is custom rules for instrumentation/UI tests, where it will inject custom rules for things like Jacoco (if coverage is enabled). See ProguardConfigurableTask.kt in AGP's sources if you want to dig more.

For XML views, their constructors are all kept when seen in resources. This is rather heavy-weight, and you can now opt out of this with more minimal keep rules based via aapt2. More details in Jake Wharton's blogpost here: https://jakewharton.com/increased-accuracy-of-aapt2-keep-rules/, or you can read the source directly here: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/tools/aapt2/java/ProguardRules.cpp.

R8 vs Proguard Compatibility

R8 and ProGuard have slightly different supported behaviors. Aside from the aforementioned optimization rules that have no effect in R8, there are also rules that are unique to R8 that ProGuard will error on due to not recognizing them. One such example is -identifiernamestring, which is used by Dagger. More and more Android libraries have started to expect R8 usage in consumers, so it's important to be mindful of this if you use ProGuard.

There are other behavior changes that are out of the scope of this post, but some to be aware of and check out include:

Some further reading on behavior/feature differences can be found here:

Latest and greatest

Did you know you can force a newer version of ProGuard or R8? AGP bumps the versions periodically, but both projects race ahead and you don't necessarily need to wait for AGP to catch up to use their improvements.

Here's how you can configure a newer ProGuard version. At the time of writing, 6.2.2 is the latest stable release and 6.3.0beta1 is available for those who want to live on the edge.

buildscript {
  dependencies {
    // Make sure to declare this before AGP!
    classpath "net.sf.proguard:proguard-base:6.2.2"
  }
}

For R8, you have to add their releases repository as well. At the time of writing, 2.0.37 is the latest version available, and that's what we're using at Slack.

buildscript {
  repositories {
    maven { url = "https://storage.googleapis.com/r8-releases/raw" }
  }
  dependencies {
    // Make sure to declare this before AGP!
    classpath "com.android.tools:r8:2.0.37"
  }
}

R8 also has a separate repo if you want to target a specific git sha from their project (just use the sha1 as the version): http://storage.googleapis.com/r8-releases/raw/master.

Final notes

To close this out - most of the rules in the built-in configs are there to help you! At the end of the day, the tools team on Android wants to make it work for most folks out of the box and avoid common issues. However, some of them can be overly conservative and some aren't relevant anymore.

My suggestions?


Special thanks to Ian Lake for proofreading this and Jake Wharton for a bunch of added context on certain bits.