The Genius app started out just over 1 year ago with File > New Project. Somewhere along the way, our baby APK ballooned into an almost unrecognizable 22MB. But we were too busy adding new features, and no one event spurred us into action … until we encountered that gift that keeps on giving, the dex method limit.
Careful configuration of Proguard got us back under the dex limit, and it made our app smaller, too. But not significantly smaller. A less lazy engineer might have dug into the problem, but instead I just wondered why as I got on the plane to Google I/O... where the Android Studio team announced a tool called the APK Analyzer.
A week later I landed in NYC, downloaded the Android Studio 2.2 preview, and tried the analyzer out. (You can find it in Build > Analyze APK). Here’s what we saw:
Most of our app's size was third party libraries—even after Proguard! Let's look closer...
70% of our app’s size was native code from a single 3rd party library*. We had done all the “right things” -- made sure we didn’t ship unused resources, used vectors over PNGs in almost every case, even preferred StringDefs over enums — and none of them made an effective difference. We were doing a great job of optimizing the wrong thing.
Why was the library so huge?
The answer is that different Android devices have different CPU architectures. When native libraries are built for Android, they’re compiled as separate versions for each architecture. So the library giving us trouble was built as a “fat binary” with multiple architectures—e.g. mips, ARM, and x86 -- all packaged together.
By default, gradle builds a single APK, no matter what libraries you include. This means your APK contains the full library, which itself contains multiple copies of the same native code (just optimized for different architectures).
This single APK is uploaded to the Play Store, and downloaded to your device. At that point, the package manager on your phone is finally smart enough to install only the code for its particular architecture—but that’s no relief to your cellular data plan. To make it worse, users who are short on storage only see the original download size of your APK and may well decide not to bother.
Together, not the same
After a pointer from Romain Guy, we followed the steps in this blog post from Realm to reduce the size of our app's native code. (We've also spoken to the Third-Party-Who-Must-Not-Be-Named, but the fundamental problem here isn't their fault.) We streamlined our code to reference as little of the library as possible, deleted all the native files we could, and then used apk splits to generate one APK per architecture.
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips'
universalApk false
}
}
The difference was staggering. For some architectures, our current release (1.11) was as little as 4.3MB. That’s a huge difference if you are updating over the cellular network.
Why doesn’t everyone do this?!
Using multiple APKs adds complexity to your updates, and it can affect app restore. Uploading multiple APKs to the Play Store means you need separate version numbers for each. If you upgrade to a new phone with a different architecture, app restore won’t work because the version number of Genius installed before no longer matches the device. (For security reasons, the Play Store will only update you to the exact build you had before.)*
The official Android documentation recommends that you only consider multiple APKs once your app reaches 100MB or larger. But to us (and our non-U.S. users, where unlimited cellular data plans are rare) that seems extremely high. Users on the go might wait to download our app until they have WiFi, by which point they've forgotten they wanted the lyrics to Panda.
There's also little risk involved—we can always re-publish a single APK if we change our minds. One of the great things about being a small team is being able to experiment.
For us, we decided the benefits outweighed the drawbacks. We hope our users enjoy their new, smaller downloads and look forward to future updates!
* The other 8% was Realm, but well worth it ... more on that later!
** Thanks for a tip from Xavier Ducrohet. To be clearer, I updated the sample code to match our actual build.gradle file (and the architectures in our screenshot). :)