ProGuard is one of the tools you probably avoid at all cost. It’s hard to configure ProGuard properly and it can backfire when misused. On the other hand it can greatly improve both runtime performance and build times, which outweighs the problems with initial setup. In this post I’ll show when it’s beneficial to use ProGuard and how to use it without breaking your app.

Why use ProGuard?

Let’s start with the simplest “Hello, world!” program possible, with no dependencies. It builds almost instantly and its footprint is minimal:

dependencies {
}
$ ./gradlew assembleDebug
Total time: 1.908 secs

$ du -h ./app/build/outputs/apk/app-debug.apk
24K ./app-debug.apk

Now, let’s add the Google Play Services dependency and see what happens:

dependencies {
  compile 'com.google.android.gms:play-services:6.5.87'
}
$ ./gradlew assembleDebug
Total time: 3.014 secs

$ du -h ./app/build/outputs/apk/app-debug.apk
1.8M ./app-debug.apk

Our app grew by almost 2 megabytes, and we didn’t add a single line of code! But hey, it’s 2015, connectivity is cheap and widely available, so who cares about the size of the application? Now, let’s add a few more libraries:

dependencies {
  compile 'com.google.android.gms:play-services:6.5.87'
  compile 'com.android.support:…'

  compile 'com.google.guava:guava:18.0'
  compile 'com.google.code.gson:gson:2.3.1'
  compile 'commons-io:commons-io:2.4'
  compile 'joda-time:joda-time:2.7'

  compile 'io.reactivex:…'

  compile 'com.squareup.…'
  compile 'android-arsenal.…'
}
$ ./gradlew assembleDebug

:app:dexDebug FAILED


UNEXPECTED TOP-LEVEL EXCEPTION:
 DexIndexOverflowException method ID not in [0, 0xffff]

Oops, we just hit the infamous 65k method limit. In case you don’t know what happened: a long time ago someone decided that 216 unique methods should be enough for everyone and used 2 bytes for method identifiers in the dex file format. As a consequence, if you work on a non-trivial app, you have to modify your build configuration to create multiple dex files:

dependencies {

  compile 'com.android.support:multidex:1.0.0'
}

buildTypes {

  debug {

    multiDexEnabled true

  }

}

@Override

protected void attachBaseContext(Context c) {
  
super.attachBaseContext(c);


  MultiDex.install(this);

}
$ ./gradlew assembleDebug

Total time: 48.784 secs


$ du -h ./app/build/outputs/apk/app-debug.apk

7.5M ./app-debug.apk

$ dexcount ./app/build/outputs/apk/app-debug.apk

Total method count: 81986

Since Android 5.0 all dex files are ahead-of-time compiled into oat files during app installation, so there is no runtime overhead of multidexing. On older Android versions the supplementary dex files have to be loaded on application startup, and the larger the dex files are, the longer it takes to process them. On Nexus 5 the dex loading takes 250ms for an app with 66k methods, ~1000ms for 81k methods and a staggering 4000ms for 130k methods. This solution clearly doesn’t scale.

ProGuard to the rescue

You probably don’t need 100% of the functionality provided by each library dependency, but the unused code paths are still compiled and packaged with your application. ProGuard can help you with that by analyzing the bytecode and pruning the dead code.

I’ll use the futuresimple/dex-method-counts utility to list the method counts within each java package included in the final apk file. Let’s start with our “Hello, world!” program:

dependencies {
}
$ dexcount ./app/build/outputs/apk/app-debug.apk

<root>: 15

  android.app: 2
  java.lang: 2
  org.chalup.proguardtechtalk: 11

Enabling ProGuard with the standard configuration for Android applications removes a few methods:

buildTypes {
  all {
    minifyEnabled true 
    proguardFiles = [ getDefaultProguardFile( ‘proguard-android.txt' ), 'proguard-rules.pro' ]
  }
}
$ dexcount ./app/build/outputs/apk/app-debug.apk

<root>: 5

  android.app: 2
  org.chalup.proguardtechtalk: 3

If we just add Google Play Services as a dependency, we might expect to get the same output, but it turns out that’s not the case:

dependencies {
  compile 'com.google.android.gms:play-services:6.5.87'
}
$ dexcount ./app/build/outputs/apk/app-debug.apk

android: 1874

com.google.android.gms: 6453

java: 381

org.chalup.proguardtechtalk: 3

org.json: 13



Total method count: 8726

The default ProGuard configuration supplied in the Android SDK ($ANDROID_HOME/tools/proguard/proguard-android.txt) is too lenient: it keeps all Parcelable objects instead of only the ones we use. It’s safe to adjust the configuration file in the following way:

--keep class * implements android.os.Parcelable {
-  public static final android.os.Parcelable$Creator *;

+-keepnames class * implements android.os.Parcelable {
+  public static final ** CREATOR;

Which yields the expected results:

$ dexcount ./app/build/outputs/apk/app-debug.apk

<root>: 5

  android.app: 2
  org.chalup.proguardtechtalk: 3

On top of that we can instruct the build system to remove unused resources to cut the size of the apk with the shrinkResources flag in your gradle.build. In some cases it won’t remove 100% of unused assets (see this bug report for an explanation), but it’s still good idea to enable it.

ProGuard, y u no prune!

Let’s add the appcompat library to the mix:

dependencies {
  compile 'com.google.android.gms:play-services:6.5.87'
  compile 'com.android.support:appcompat-v7:21.0.3'
}
$ dexcount ./app/build/outputs/apk/app-debug.apk

android: 3173

  graphics: 53

  support: 2584

    v4: 652

    v7: 1932

      internal: 1298

      widget: 621

  view: 201

  widget: 162

Why did ProGuard keep these classes? It turns out aapt automatically generates a ProGuard configuration to keep all classes referenced from AndroidManifest.xml or any layout file. This ensures you can safely use the UI libraries with ProGuard enabled, but also limits the amount of code that can be pruned by ProGuard.

ProGuard, y u no keep!

Many Android libraries use Java annotations with code generation under the hood. The generated code is usually accessed via reflection, which means that ProGuard static analysis would not mark it as used and happily delete it from packaged bytecode. For example this code using the Butterknife library would crash with a NullPointerException on accessing mTextView:

public class MainActivity extends Activity {


  @InjectView(R.id.hello_text)

  TextView mTextView;



  @Override

  protected void onCreate(Bundle state) {

    super.onCreate(state);

    setContentView(R.layout.activity_main);



    ButterKnife.inject(this);
    mTextView.setText(R.string.hello_world);

  }

}

You have to explicitly tell ProGuard to keep the annotated and generated code:

-dontwarn butterknife.internal.**

-keep class **$$ViewInjector {
  *; 
}

-keepnames class * { 
  @butterknife.InjectView *;
}

The rule of thumb is: if the library API is using annotations, it probably requires ProGuard configuration, which unfortunately is not an easy task. The library authors can expose the proper ProGuard rules for their users using a consumerProguardFiles property in their build file, but very few do this at the moment. The next stop is checking some known lists of ProGuard snippets. If this fails, you have to cook up the rules yourself, or use your google-fu to search dark corners of the Internet for the correct incantations.

Recap

Let’s see how the ProGuard configuration works for our app with a gazillion dependencies:

$ ./gradlew assembleDebug

Total time: 16.096 secs


$ du -h ./app/build/outputs/apk/app-debug.apk

3.5M ./app-debug.apk

$ dexcount ./app/build/outputs/apk/app-debug.apk

Total method count: 6636

It builds three times faster, we halved the apk size, and we’re well below the 65k method limit, which means we don’t have to worry about the runtime overhead of multidex. Not a bad result for a few lines of configuration. Keep calm and ProGuard!

Posted by

Jerzy Chałupski

Share this article

  • Mehdi Chouag

    Nice, post :)

    I just have one question. Where did you get the “dexcount” command ? Did you make it yourself or get it in a Gitub ?

  • Mallikarjun P

    Hi Jerzy. I am getting below error
    Error:Execution failed for task ‘:app:shrinkReleaseMultiDexComponents’.
    > java.io.IOException: The output jar [E:androidStudioProjectsnameappbuildintermediatesmulti-dexreleasecomponentClasses.jar] must be specified after an input jar, or it will be empty.
    Please tel me how to solve this