The size of your APK has an impact on how fast your app loads, how much memory it uses, and how much power it consumes. Users are less likely to install your app if the size is too large. If they manage to install it, it will probably be the first app they uninstall once their storage begins to run out. Code shrinking and obfuscation are two of the most effective app optimization techniques available. I have previously written about other app optimization strategies here.
Code shrinking and obfuscation, what do they mean?
Code shrinking (or tree-shaking): is the processing of detecting and safely removing unused classes, fields, methods, and attributes from your app and its library dependencies (making it a valuable tool for working around the 64k reference limit). For example, if you use only a few APIs of a library dependency, shrinking can identify library code that your app is not using and remove only that code from your app.
Resource shrinking: is the process of removing unused resources from your packaged app, including unused resources in your app’s library dependencies. It works in conjunction with code shrinking such that once unused code has been removed, any resources no longer referenced can be safely removed.
Obfuscation: is the process of shortening the name of classes and members, which results in reduced Dalvic Executable (DEX) file sizes.
Why is shrinking and obfuscation important
Obfuscation is important for preserving the security of your code against potential hackers who may try to reverse-engineer your APKs. Any Android developer can easily decompile your APK and inspect the resultant code. By obfuscating your code, you make your application harder to reverse-engineer, thus, protecting business secrets and vulnerability discovery.
Secondly, shrinking your code removes all unused code and resources. This process can greatly reduce your app’s size.
How to enable code shrinking for your project
There are two main tools for enabling code shrinking in your project.
ProGuard and DexGuard: ProGuard is a free tool that’s included in Android Studio. DexGuard is the commercial version of ProGuard which offers more advanced shrinking and obfuscation.
R8: R8 is the default compiler that converts your project’s Java bytecode into the DEX format that runs on the Android platform. R8 is enabled by default on Android Gradle plugin 3.4.0 and higher. This tutorial uses the R8 tool, so ensure you are running on Android Gradle plugin 3.4.0 or higher.
Visit the article on Proguard and R8 to find out more information about Progurd and R8. It will teach you a lot, including which option is the best for you.
1. Enable code shrinking
Code shrinking is a compile-time optimization which can increase the build time of your project. For this reason, shrinking, obfuscation, and code optimization are disabled by default when you create new projects. It is therefore better to enable them only in your release builds. To enable code shrinking, obfuscation and optimazation, include the code below in your Module level build.gradle
android {
...
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
...
}
2. R8 configuration files
R8 uses ProGuard rules files to modify its default behaviour and better understand your app’s structure, such as the classes that serve as entry points into your app’s code. Although you can modify some of these rules files, some rules may be generated automatically, or inherited from your app’s library dependencies. Here’s a list of the sources of ProGuard rules files that R8 uses:
- Android Studio: When you create a new project or module using Android Studio, the IDE creates a
proguard-rules.pro
file in the root directory of that module. By default, this file does not apply any rules. - Android Gradle plugin: The Android Gradle plugin generates
proguard-android-optimize.txt
, which includes rules that are useful to most Android projects and enables@Keep*
annotations. - Library dependencies. When you publish an library with its own ProGuard rules file, and you include it as a dependency, R8 applies its rules when compiling your project.
- Android Asset Package Tool 2 (AAPT2). AAPT2 generates keep rules based on references to classes in your app’s manifest, layouts, and other app resources. For example, AAPT2 includes a keep rule for each Activity that you register in your app’s manifest.
- Custom configuration files: You can include additional configurations, and R8 applies them at compile-time.
When you set the minifyEnabled
property to true
, R8 combines rules from all the available sources listed above. This is important to remember when you troubleshoot with R8. Because other compile-time dependencies, such as library dependencies, may introduce hidden changes to the R8 behaviour.
R8 works with your existing ProGuard rules, so you’ll likely not need to take any actions to benefit from R8. For most situations, the default ProGuard rules file (proguard-android- optimize.txt
) is sufficient for R8 to remove only the unused code. However, because it’s a different technology to ProGuard, shrinking and optimization may result in removing code that ProGuard may have not. This can cause bugs. To fix errors and force R8 to keep certain code, add a -keep
line in the ProGuard rules file. For example:
-keep public class MyClass
# Preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable
# hide the original source file name if
# keeping line numbers
-renamesourcefileattribute SourceFile
# JavaScript interface class:
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
That’s it! Sync your project with the gradle files to effect these changes. Build an APK and decompile it using any online decompiling tools. See if you can easily make sense of the resultant files. Perhaps more importantly, compare the size of your new APK with an older one to see the difference.