The Road to Single DEX
See my conference talk here:
Youtube link: https://www.youtube.com/watch?v=ZmI-NZ1akow
SpeakerDeck: https://speakerdeck.com/jaredsburrows/the-road-to-single-dex-gradle-summit-2017
Original Post:
The Yammer for Android app was over the dex limit. This means the Android application was being shipped with more than one .dex file because each .dex file can only hold around 64k methods. I was determined to make our Android application much smaller in order to avoid issues that come along with multidex.
Here are the main limitations on why we would like to avoid using multidex:
- Loading extra dex files may lead to possible ANRs.
- Multidex makes very large memory allocation requests may crash during run time.
- Some libraries may not be able to be used until the multidex build tools are updated to allow you to specify classes that must be included in the primary dex file.
Let’s talk about several strategies on how to reduce your apk and proper ways to avoid shipping with multidex.
Big APK = Big Dex + Resources
Here are some dex-specific optimizations you should care about. We’ll go into more detail about these later:
- Remove dead code — eg. compiled, unused code
- Remove old/unused libraries — eg. left behind from old projects/experiments
- Remove large/non-mobile libraries — eg. Google drive Java library, Guava (too large for trying to stay under the dex limit)
- Remove Gradle scopes — eg. testCompile vs compile
Here are some resource-specific optimizations as well:
- Remove extra and unnecessary resources(res folder)— eg. pngs, strings
- Remove extra and unnecessary assets(assets folder) — eg. raw media, fonts
Before Optimizations — APK Size and Method Count
Here are the debug and release builds’ apk size and method count before applying any of the optimizations strategies.
Here are the plugins used in order to print the apk size and dex count:
- Dex count — https://github.com/KeepSafe/dexcount-gradle-plugin
- APK size — https://github.com/vanniktech/gradle-android-apk-size-plugin
Debug
$ gradlew countDebugDexMethods sizeDebugApkTotal APK Size in debug.apk in bytes: 12386152 (12.3 MB)
Total methods in debug.apk: 113007 (172.44% used)
Total fields in debug.apk: 50547 (77.13% used)
Methods remaining in debug.apk: 0
Fields remaining in debug.apk: 14988
Release
$ gradlew countReleaseDexMethods sizeReleaseApkTotal APK Size in release.apk in bytes: 10764242 (10.7MB)
Total methods in release.apk: 85259 (130.10% used)
Total fields in release.apk: 39887 (60.86% used)
Methods remaining in release.apk: 0
Fields remaining in release.apk: 25648
As you can see, there is current 85,259 methods — which is above the 64k method single dex limit.
Avoid dead code
- Remove any and all unused classes.
- Remove all unused libraries from your build.gradle — sometimes you will be able to prefer one library over another. In our case, we chose HockeyApp over ApplicationInsights because Hockeyapp encompasses all of the functionality that we need from ApplicationInsights.
Remove old and unused libraries
- Since we are using HockeyApp to tell users to upgrade nightlies for dogfooding, we are able to once again us HockeyApp in place of another library — AndroidQuery.
- Similar to Guava, we were able to remove Apache Commons validator and save thousands of methods. It turns out we are only using this library to validate emails addresses. We decided to use Android’s internal Pattern regex instead.
- One major reduction in dex methods came from removing Jackson 1 in favor of Jackson 2. There were two different library modules that were using two different versions of Jackson and Jackson 2 has a different package name than Jackson 1(prevents Proguard from doing it’s job). Making sure to consolidate libraries and using the correct package names is very important. Here we saved 6k+ methods!
Try not to use large libraries
Large/Non-mobile libraries
We were using Guava. This is great library that can be extremely helpful but it is also very big, 15k methods and 2.3MB. We have since refactored this out of our application and sticking with our own implementations or using methods that reside in the android.jar.
Necessary evils
Google provides Google Play Services and the Android Support libraries (backwards compatibility). These are already very big and continue to grow with each release:
- App Compat 24.2.1 — 16.5k methods
- Google Play Services GCM 9.6.1 — 16.7k methods
Note: If your project requires both App Compat and Google Play Services GCM for example, your Android application is already starting with 33.2k (16.5k + 16.7k) methods. This means you are already half way to the dex limit!
Correctly use Gradle configurations
When compiling a large application that may have extra custom libraries and other modules, make sure to only add in code that you want shipped with the app in the “compile” scope.
Before
Notice, in this example, all modules are being compiled together.
dependencies {
compile project(":api-module")
compile project(":common-module")
compile project(":common-test-module")
}
After
Since we only need the “common-test-module” for the “testCompile,” we can move this to the correct configuration in order to reduce our overall apk size.
dependencies {
compile project(":api-module")
compile project(":common-module")
testCompile project(":common-test-module")
}
Make sure to use Proguard
- Optimize and obfuscate existing code with Proguard — set minifyEnabled true.
- Remove extra and unused rules — Too many rules may keep too much code. We had extra rules we could easily remove to make sure Proguard is doing its job.
Trimming the APK size: Resources
The apk contains both an assets folder and a res folder. See a list of everything in apk here.
- Remove unused resources after proguard — set shrinkResources true.
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
- Only keep the configurations you need — eg. resConfigs “en”.
android {
defaultConfig {
resConfigs "en"
}
}
- Remove unused fonts and other files in the assets folder.
After Optimizations— APK Size and Method Count
After applying both dex and apk specific optimizations, here are the results of the debug and release builds.
Debug
$ gradlew countDebugDexMethods sizeDebugApkTotal APK Size in debug.apk in bytes: 9701342 (9.7MB)
Total methods in debug.apk: 94390 (144.03% used)
Total fields in debug.apk: 44529 (67.95% used)
Methods remaining in debug.apk: 0
Fields remaining in debug.apk: 21006
Release
$ gradlew countReleaseDexMethods sizeReleaseApkTotal APK Size in release.apk in bytes: 7427360 (7.4MB)
Total methods in release.apk: 60880 (92.90% used)
Total fields in release.apk: 27254 (41.59% used)
Methods remaining in release.apk: 4655
Fields remaining in release.apk: 38281
As you can see, there are now 60,880 methods, which is below the 64k method single dex limit. This means we can finally ship a production build without multidex!
Conclusions
- After making the optimizations, we have been able to get the method count of our apk down to 60,880 methods!
- Along with the method count reduction, we have also reduced the overall apk size from 10MB+ to 7.4MB!
Helpful Gradle Plugins
Here are the helpful Gradle plugins that I used to help monitor and reduce the overall size of the Yammer for Android app.
Further Reading
Wojtek Kaliciński, Smaller APK:
https://medium.com/google-developers/smallerapk-part-3-removing-unused-resources-1511f9e3f761#.n932nn9xl
Cyril Mottier, Putting your APKs on a Diet:
http://cyrilmottier.com/2014/08/26/putting-your-apks-on-diet/
Count AAR/JAR methods:
http://www.methodscount.com/