Shrinking Your Code

“Your code” refers to both the stuff that you write yourself and the stuff that comes from the dependencies that you need (including transitive dependencies).

Probably you use all the code that you wrote yourself. Otherwise, why did you write it?

However, it is very likely that you are not using all of the features from the dependencies that you are using. Any unused code is simply taking up space in your APK, and so it would be nice to get rid of it.

Unfortunately, that is a bit tricky.

A Tale of Two Tools

Finding unused code in a Java/Kotlin codebase requires examining all of the code, what it refers to, and from there determining what isn’t referred to. Then, we need to actually remove the unused code from what goes into the APK, while not actually affecting the dependencies themselves.

The original tool for this process was ProGuard. ProGuard has been around for nearly two decades, and the Android build tools integrated it fairly early on to help reduce APK size.

More recently, Google has switched to their own tool, R8. R8 is actually the compiler, and it handles identifying unused code as part of the compilation process.

Enable Code Minification

However, by default, eliminating unused code (“minification”) is disabled. Partly, that is for build speed, as finding unused code is a time-consuming process. However, part of the reason why it is disabled is because sometimes the tools will think something is unused when in reality it is not.

To opt into code minification, we use minifyEnabled true in our module’s build.gradle file, typically only for the release build type:

buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  }

  // other stuff here
}

Here, we have a buildTypes closure, where we configure the release build. In there, we have minifyEnabled true, to override the default and ask that R8 try to remove unused code.

You are welcome to enable minification for debug builds as well, using a similar closure. Minification takes time, and so for larger projects it may become impractical to use minification for the builds that you want to do several times per day.

Test and Adjust

Once you enable minification, you will need to test your app, for whatever build types you elected to use minifyEnabled true. This means both your automated tests and manual tests.

If you get ClassNotFoundException, MethodNotFoundException, or NoSuchFieldError crashes with your minified build, whereas you do not in a regular build, that indicates that minification went too far and removed things that you really are using.

The proguard-rules.pro file that (probably) is in your module’s directory is a place to put rules that will be used by R8 (or ProGuard) to configure the minification process. In particular, -keep rules will tell R8 to keep classes, methods, or fields that it might otherwise try to remove:

-keep class net.sqlcipher.* { *; }
-keep class net.sqlcipher.database.* { *; }

These lines, for example, tell R8 to keep classes, methods, and fields in the net.sqlcipher and net.sqlcipher.database packages. The * in the fully-qualified class name will match all classes in the package, and the { *; } will match all methods and fields. Or, you can provide names of specific classes, etc. that you want to keep.

You will need to identify and create -keep rules like these to get R8 to not remove the things that, when removed, cause the aforementioned crashes.

Remove ABIs

Some of your dependencies might have “native code” (C/C++ libraries) in addition to Java/Kotlin code. These will show up in a lib/ directory inside the APK Analyzer output:

Native Code in APK Analyzer
Native Code in APK Analyzer

This native code could be large — in this case, it is nearly 1MB. Frequently, the native code ships for multiple CPU architectures (or ABIs). In this case, we see four:

The vast majority of Android devices use one of those latter two ABIs, for ARM CPUs (32-bit and 64-bit). Your emulator probably uses one of the x86 ABIs, but for a production release, you may be able to ignore the emulator.

You can eliminate native code for CPU architectures that you do not need by adding an ndk closure to your module’s build.gradle file, with an abiFilters declaration:

android {
  defaultConfig {
    // other stuff goes here

    ndk {
      abiFilters 'arm64-v8a', 'armeabi-v7a'
    }
  }
}

abiFilters is where you can list the ABIs that you want to keep. Others will be removed as part of the packaging process.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.