May 11, 2021

    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 :

    1. -addconfigurationdebugging,a runtime tool used to determine the rules you’ll require in your ProGuard configuration. 
    2.   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 trueindicates that the code should be shrunk, whereasshrinkResources truetells 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-verboseto myproguard-project.txtconfiguration file.

    But when I try to build the application, it throws acan’t find referenced classerror. 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: 

    1. If the missing class is referenced from your own code, you may have forgotten to specify an essential library.
    2. 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. 
    3. If you don't feel like filtering out the problematic classes, you can try your luck with the-ignorewarningsoption, or even the-dontwarnoption. 

    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-ignorewarningsin 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 andOpenJSSEcan 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-dontwarnoption).

    -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

    Flowwas added in API 30, andFlowAdaptersis a bridge between Reactive Streams API and the Java 9 API. Since thecompileSdkVersionis 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.AssertionErrordueto 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-keepdirectives 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-keepconfiguration 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 -keeprules : 

    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-addconfigurationdebuggingenabled provides a list of-keepand-keepclassmembersrules 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-keeprules: 

    Note that the rules proposed by-addconfigurationdebuggingshould 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-keeprules gathered during the first iteration, the ProGuard Playground shows that some of theapipackages have an active-keeprule 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 anyapipackage, and all constructors and fields should be kept formodelpackages. I replace all these rules with the following ones: 

    Using the ProGuard playground, I maintain the quality of these new rules by removing specific-keeprules 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-keeprules 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 andmodelpackages 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_LifecycleAdapteris 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-keeprule:

    -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-addconfigurationdebuggingprovides specific-keeprules 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-keeprules 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.

    Laurent Ferier - DexGuard Team Lead

    Instantly visualise the impact of your ProGuard or R8 rules using the ProGuard Playground

    Try it Now >

    Other posts you might be interested in