Configuring ProGuard, an Easy Step-by-Step Tutorial
I was recently asked to apply ProGuard on DuckDuckGo, an open-source, privacy-enabled browser for Android. This lets me experiment with ProGuard on a big-enough application whose internals I wasn’t too familiar with - which was great. The application should also be big enough to be a challenge. And last, it should use reflection, because issues I would encounter with ProGuard are always about reflection.
The main advantage of ProGuard is that it reduces the application size and optimizes performance. Configuring ProGuard can be initially daunting, but once you’ve mastered the underlying concepts and tools, it’s a straightforward affair.
I’ll focus on two specific tools :
-addconfigurationdebugging,
a runtime tool used to determine the rules you’ll require in your ProGuard configuration.- The new ProGuard Playground online tool, which lets you see the impact of a set of keep rules on the target application. This replaces long build and analyze cycles with immediate visualization.
In this post, I’ll show you how to apply ProGuard to an application step-by-step so you can apply it to your own projects. Let’s dive in!
Step 1: Enable ProGuard in the Android project
Before any ProGuard setup, I need to make sure that everything is running correctly. I download the application sources, set up configuration files according to my development environment, and build the application. The resulting apk size is 13.5MB, and executing the application shows that it’s working correctly on-device. I’ll use this APK to help me configure ProGuard.
To enable ProGuard, I first need to update the build.gradle configuration file. The user’s guide proposes the following configuration:
1. Disable R8 in gradle.properties
file
android.enableR8=false
android.enableR8.libraries=false
2. I substitute the module with the latest ProGuard version (currently 7.0.1) in root build.gradle:
buildscript {
...
configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute module('net.sf.proguard:proguard-gradle')
with module('com.guardsquare:proguard-gradle:7.0.1')
}
}
}
3. I then enable minification (shrinking) and set up the ProGuard configuration files:minifyEnabled true
indicates that the code should be shrunk, whereasshrinkResources true
tells me to remove all unused resources. Theproguard-project.txt
file shall hold my specific ProGuard configuration:
android {
...
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFile getDefaultProguardFile('proguard-android-optimize.txt')
proguardFile 'proguard-project.txt'
}
}
}
4. To make life easier during configuration, I enable verbose output by adding-verbose
to myproguard-project.txt
configuration file.
But when I try to build the application, it throws acan’t find referenced class
error. Let’s look at how to solve that next.
Dealing with can’t find referenced class
ProGuard warnings
My first attempt at building the application resulted in many warning messages like this one:
Warning: okhttp3.internal.platform.BouncyCastlePlatform: can't find
referenced class org.bouncycastle.jsse.BCSSLSocket
The ProGuard documentation (see https://www.guardsquare.com/manual/troubleshooting) provides the reason for this:
"A class in one of your program jars or library jars is referring to a class or interface that is missing from the input. The warning lists both the referencing class(es) and the missing referenced class(es)."
It then documents the reasons why classes can be missing:
- If the missing class is referenced from your own code, you may have forgotten to specify an essential library.
- If the missing class is referenced from a pre-compiled third-party library, and your original code runs fine without it, then the missing dependency doesn't seem to hurt.
- If you don't feel like filtering out the problematic classes, you can try your luck with the
-ignorewarnings
option, or even the-dontwarn
option.
The ProGuard user manual explains why the third solution is commonly necessary in the standard Android build process (see https://www.guardsquare.com/manual/troubleshooting). I could just ignore such warnings, by adding-ignorewarnings
in proguard-rules.pro, but this is not recommended because future warnings will also be caught by this rule. So it is better to add precise-dontwarn
rules in the ProGuard configuration instead.
Let’s look at the preceding warning message:
Warning: okhttp3.internal.platform.BouncyCastlePlatform: can't find
referenced class org.bouncycastle.jsse.BCSSLSocket
It indicates that classorg.bouncycastle.jsse.BCSSLSocket
cannot be found which is used byokhttp3.
By default, OkHttp uses the default platform security provider. Other security providers, such asbouncycastle, Conscrypt
andOpenJSSE
can be optionally used by OkHttp. ProGuard doesn't have this knowledge and will warn about this, so I have to explicitly tell it that these missing classes are fine (see the ProGuard user's manual about the-dontwarn
option).
-dontwarn org.bouncycastle.**
-dontwarn org.conscrypt.**
-dontwarn org.openjsse.javax.net.ssl.**
-dontwarn org.openjsse.net.ssl.**
A similar problem occurs withFlow
: Warning: org.reactivestreams.FlowAdapters: can't find referenced class java.util.concurrent.Flow
Flow
was added in API 30, andFlowAdapters
is a bridge between Reactive Streams API and the Java 9 API. Since thecompileSdkVersion
is 29, this class is not available and we need to inform ProGuard about this:
-dontwarn java.util.concurrent.**
I also add the following line in the configuration file because the application does not rely on such packages:
-dontwarn java.awt.geom.**
-dontwarn kotlin.time.**
When optimizing, ProGuard analyses the complete application, which is why it can detect such missing dependencies, which are in fact not used.
The APK is now built successfully, but will it work on the first try?
Step 2: Run the ProGuard-processed application
My first try at running the application results in ajava.lang.IllegalStateException:
exception when setting module id
java.lang.IllegalStateException: Unable to get current module info in ModuleManager created with
non-module Context
and also ajava.lang.AssertionErrordue
to ajava.lang.NoSuchFieldException:
java.lang.AssertionError: Missing field in com.duckduckgo.app.trackerdetection.c.a
[…]
caused by: java.lang.NoSuchFieldException: BLOCK
I expected such issues because ProGuard is altering the package, and in some cases, when code is accessed through reflection, ProGuard cannot always determine which code is actually used in those cases and might remove important code. I have to configure-keep
directives to keep this code in the resulting APK.
Similarly, ProGuard also renames fields, methods, classes, and packages. When these are accessed through reflection, there are cases where ProGuard cannot determine that they cannot be renamed. Consider for instance that the string used to access a field by reflection is dynamically built. We have then to manually indicate this to ProGuard using-keep
configuration rules to preserve such items (see also ProGuard configuration made easy).
Letting ProGuard help me
Helpfully, ProGuard provides great tools to help me configure the required -keep
rules :
- The option
-addconfigurationdebugging
- The ProGuard Playground
Option -addconfigurationdebugging
The option-addconfigurationdebugging
(see https://www.guardsquare.com/manual/configuration/usage) specifies to instrument the processed code with debugging statements that print out suggestions for missing ProGuard configuration rules. This is very useful to get practical hints at run-time, for cases where the processed code crashed because it still lacked some configuration for reflection.
Setting up this option is straightforward. I merely add the following lines to my configuration file.
# Now, it's time to let ProGuard help methods
# I will not leave this option in the release application
# see https://www.guardsquare.com/en/blog/proguard-configuration-made-easy
-addconfigurationdebugging
I then rebuild the application, install it, and set it up to retrieve the logcat output using the following commands:
adb logcat -c
adb logcat > application.log
I only have to run the application on the device and it works like magic! Whenever a reference is made to a missing class, method, or field during execution, a dump is created with this kind of information:
ProGuard: The class 'androidx.lifecycle.ClassesInfoCache' is calling Class.getDeclaredMethods
ProGuard: on class 'com.duckduckgo.app.global.DuckDuckGoApplication' to retrieve its methods.
ProGuard: You might consider preserving all methods with their original names,
ProGuard: with a setting like:
ProGuard:
ProGuard: -keepclassmembers class com.duckduckgo.app.global.DuckDuckGoApplication {
ProGuard: <methods>;
ProGuard: }
Testing the application with-addconfigurationdebugging
enabled provides a list of-keep
and-keepclassmembers
rules which can be added in the configuration file. Of course, since initially the classes are not kept, the application does not behave like the original one and might crash on multiple occasions. We’ll need to iterate updating our keep rules several times to complete our runtime testing and configuration file
I apply this technique on the DuckDuckGo application which gives me the following list of-keep
rules:
Note that the rules proposed by-addconfigurationdebugging
should not be added blindly in the configuration file because they need to be aggregated, and sometimes might not be correct. The ProGuard Playground is a great tool to help us with this.
ProGuard Playground
The ProGuard Playground is a new tool that provides an easy way to visualize the impact of ProGuard rules on your application. It lets you interactively tweak the keep rules without having to rebuild the application. Without the Playground, this process would be performed by long build and analyze cycles, but the ProGuard playground lets you immediately see the impact of a configuration change on the embedded classes, methods, and fields in the target APK.
When I apply the-keep
rules gathered during the first iteration, the ProGuard Playground shows that some of theapi
packages have an active-keep
rule for the fields and methods, but some don’t. This seems inconsistent to me: it looks like all fields and methods should be kept for classes in anyapi
package, and all constructors and fields should be kept formodel
packages. I replace all these rules with the following ones:
Using the ProGuard playground, I maintain the quality of these new rules by removing specific-keep
rules one at a time and making sure that the same set of classes, methods, and fields are kept.
Further work toward a fully configured application
I build and run the application with this new ProGuard configuration, testing all application functionalities to make sure that debug suggestions have covered all code paths. ProGuard still suggests more-keep
rules to add.
Issue with constructors
The following configuration options keep appearing:
ProGuard: The class 'com.squareup.moshi.ClassFactory' is calling Class.getDeclaredConstructor
ProGuard: on class 'com.duckduckgo.app.autocomplete.api.AutoCompleteServiceRawResult' to retrieve
ProGuard: the constructor with signature (), but the latter could not be found.
ProGuard: It may have been obfuscated or shrunk.
ProGuard: You should consider preserving the constructor, with a setting like:
ProGuard:
ProGuard: -keepclassmembers class com.duckduckgo.app.autocomplete.api.AutoCompleteServiceRawResult {
ProGuard: public <init>();
ProGuard: }
ProGuard:
Since I don’t know the DuckDuckGo application well enough, this error is not clear to me. But the ProGuard playground will efficiently show me the reason for this message.
Again, using the original APK in the playground, I encode the corresponding keep rule,
-keepclassmembers class com.duckduckgo.app.autocomplete.api.AutoCompleteServiceRawResult {
public <init>();
}
and find out that theinit()
method requires a string parameter - but the keep directive does not specify any. I update the -keep
rule by considering any number of parameters:
-keepclassmembers class com.duckduckgo.app.autocomplete.api.AutoCompleteServiceRawResult {
public <init>(...);
}
The Proguard Playground shows that it will fix this issue. To make sure I keep all class constructors, I apply this solution for all constructors in both theapi
andmodel
packages by updating the following rules:
LifeCycle adapters
ProGuard also suggests keeping XXX_LifeCycle adapters:
02-10 14:20:04.733 28890 28890 W ProGuard: The class 'androidx.lifecycle.Lifecycling' is calling Class.forName to retrieve
02-10 14:20:04.733 28890 28890 W ProGuard: the class 'com.duckduckgo.app.global.DuckDuckGoApplication_LifecycleAdapter', but the latter could not be found.
02-10 14:20:04.733 28890 28890 W ProGuard: It may have been obfuscated or shrunk.
02-10 14:20:04.733 28890 28890 W ProGuard: You should consider preserving the class with its original name,
02-10 14:20:04.733 28890 28890 W ProGuard: with a setting like:
02-10 14:20:04.733 28890 28890 W ProGuard:
02-10 14:20:04.733 28890 28890 W ProGuard: -keep class com.duckduckgo.app.global.DuckDuckGoApplication_LifecycleAdapter
02-10 14:20:04.733 28890 28890 W ProGuard:
But adding the proposed keep rule does not help to avoid this, and this is where ProGuard Playground comes to the rescue. When I check the impact of such rules on the system, I’m surprised to see that these rules have no impact: nothing gets flagged as kept, but the classDuckDuckGoApplication_LifecycleAdapter
is also nowhere to be found.
I understand this is part of theandroidx.lifecycle
, which checks for the existence of such classes and handles their absence. Since no such class is used in the application, this warning can be ignored.
New keep rules
During this iteration, ProGuard also indicates that I should add the following-keep
rule:
-keepclassmembers class com.duckduckgo.app.browser.PulseAnimation { <methods>; }
Testing classes
Lastly, ProGuard also proposes the following keep rule:
-keep class kotlinx.coroutines.test.internal.TestMainDispatcherFactory
But since I am not currently executing any unit test, it’s no wonder the class cannot be found. I don’t need to add such a rule in the configuration file.
The resulting application is running correctly, and no more relevant warnings are emitted by-addconfigurationdebugging.
Step 3: Getting production-ready
Remove -addconfigurationdebugging.
As mentioned in the User’s guide, and since I’ve finished setting up my configuration file, I remove the-addconfigurationdebugging
flag from it. This flag should be used for configuration purposes only - it is no longer useful and would add obfuscation information to the processed code while also increasing the application size and slowing it down.
The (final) configuration
The resulting ProGuard configuration is as follows:
Results
The following table summarizes some metrics about the resulting application:
Initial |
Optimized |
Reduction Ratio |
|
APK Size |
13 MB |
8.8 MB |
67% |
- Resources |
4.5 MB |
4.4 MB |
97% |
- Code |
5.7 MB |
1.8 MB |
31% |
- Native libraries |
~1 MB |
~1 MB |
100% |
Download size |
11.6 MB |
7.5 MB |
64% |
Classes count |
13302 |
6690 |
50% |
Methods count |
87162 |
30175 |
34% |
It’s clear that the size of resources (such as pictures, strings, etc.) is not reduced by the process, and still has a big impact on the overall package size. Note that you can use DexGuard instead to remove unused resources (see https://www.guardsquare.com/dexguard). The size of native libraries is not impacted by the operation either. But ProGuard managed to drastically reduce the code size by a factor of 3, and the package size and download size have been reduced by one-third. Good job! :-)
Lessons learned
This post provides a practical example of applying Proguard to a real-life application. Most of the configuration needed during this process is related to creating a coherent set of -keep
rules. To summarize:
- The configuration option
-addconfigurationdebugging
provides specific-keep
rules related to the application, but this set of rules is still rough and needs to be adjusted. - The ProGuard Playground is a great tool to interactively see the impact of the
-keep
rules on the target application without having to go through a long build process. - ProGuard was able to shrink the code size by a factor of 3, which resulted in a one-third smaller package.