App Diets are not a Fad
- English |
- 中文
(This is our second guest post by Mikhail Nakhimovich. By day Mikhail Nakhimovich is an architect of the award winning New York Times Android App and by night he writes about Android and helps startups create performant, delightful apps with his team at Friendly Robot.)
In our last post we explored an architecture/library selection that would lead to nearly perfect startup times. Today I wanted to explore another type of performance optimization: APK size. A smaller APK results in faster resource lookup and faster reflection. Image optimization, an important way to cut down APK size, allows for fewer dropped frames. And last but not least, users generally love apps that take up minimal space and data to download.
This year I was fortunate enough to attend Google’s I/O, where Wojtek Walicinski gave a great talk on how to reduce APK size (“Putting Your App on a Diet”). I’d recommend that you watch the full video, but this article will summarize the main points. We’ll also dive deeper into the main concepts in a Q&A with Boris Farber, a Google Developer Advocate and expert on performance.
Why Does (App) Size Matter?
In a word? Performance. Apps are faster when they have fewer resources to load. I’ve seen 30% speed improvements for layout inflation after removing 20k methods from my app.
Plus, it’s worth noting that while many users in the U.S. aren’t constrained by data limits, this isn’t true for much of the world. It’s common in many countries that users pay per megabyte, which makes reducing APK size a central part of offering a positive user experience.
How to Think about App Size
What do we mean when we talk about the size of an app? There are several different measurement categories:
- Raw APK Size. Interestingly, this is the most talked about metric among developers but the least relevant for users.
- Download Size. How many megabytes need to be downloaded.
- Install Size. How much space an app actually takes up on the device.
- Update Size. The size of the update.
APK Size
When thinking about APK size, you should remember that an APK is just a zip file. An Android APK contains several parts:
- Byte code. This includes classes.dex (your app’s sources) as well as any libraries and library binaries that you’ve imported. These can be Java or Android
- Native code. C-libraries for architecture (.so files)
- Resources. This is basically everything else: strings, colors, images, the Android Manifest, and the META-INF folder. NimbleDroid actually does a great job of showing you the size of the various parts of your APK on upload. Check out Facebook’s APK file profile.
Download Size
Sometimes developers see how much data it takes to download their APK and try to figure out how they can reduce it. You might see Resources.arsc getting pretty big, think that strings are compressible, and compress it. This is actually a bad idea - once the APK is downloaded, the framework needs to uncompress and hold the entirety of the uncompressed resources in memory to read just a single string. So compressing your resources is actually an anti-pattern. If you’re wondering how resource loading normally works, the framework memory maps the file when it isn’t compressed and optimizes how it stores/accesses strings directly from specific memory addresses. Additionally, the Play Store serves a compressed version of your APK to keep bandwidth down. It’s important to know exactly what will happen to an APK before trying to shrink it.
Install Size
An APK contains files that include architectures and different device size resources. Unfortunately, there’s nothing your phone can do to strip these out after installation. Only on Lollipop and Marshmallow devices will ART take your raw APK and compress it to an OAT native file. N will be going back to using a JIT compiler, which means no more optimizing apps on install.
Update Size
When a user wants to download an update, the delta or missing classes/resources are computed. Next, the delta classes/resources are compressed. The device downloads the difference and reconstructs the APK on device. The algorithms that Google is using for diffing are constantly changing and improving (currently it’s BSDiff). Play Store will now show download size rather than APK size. On update users will see the update size.
There were packaging improvements in Studio 2.2 that also help reduce APK size:
- Sort all files in archive by name
- Zero all timestamps
- Zero empty space in archive
- Ability to store uncompressed *.so files
You might be wondering how optimizations affect APK size. Google has recently open sourced APK Patch Size Estimator, created by Julian Toledo. It shows estimates for the new APK size on disk, the new APK Gzipped size (download size for new installs), and the Gzipped BsDiff Patch Size (size of updates).
Here are some recipes for how to keep your APK size to a minimum:
First is an anti-pattern: the Zopfli compression of APKs. Zopfli was developed at Google as an alternative compression. The good news is that it makes your APK smaller. The bad news is that it has tremendous resource consumption during compression and may not work with new compression algorithms for differential updates. As a result, it’s recommended to not Zopfli-compress a whole APK anymore.
Shrinking your Images
And now for the good stuff. We’ll start with image compression. You’ll need to use an external tool to preprocess images. If you use an external tool to compress your images, you’ll need to disable the default compression during the build phase. You can do this by adding the following to your build.gradle file to prevent build tools from processing the images and making them bigger.
aaptOptions {
cruncherEnabled=false
}
Interestingly enough you can Zopfli pngs. They will look the same (lossless) and be just as fast.
If compression isn’t your thing, you can also switch to WebP. It’s 30% smaller than Jpeg, works on Android 4.0+ for non transparent PNGs, and works with transparency and lossless images in 4.2.1+.
As far as Android specific formats, Vector Drawables are amazing for icons. This is a text based format that now has a support library to boot. Another text based drawable format is ShapeDrawables, which has support dating back to Android 1.0. Shape Drawables are great for button backgrounds, borders and gradients.
Shrinking your Code
The best way to shrink code is to use Proguard. You will hate Proguard but will love being under the dex limit. Proguard will strip all methods without an execution path from your APK. Turning on Proguard is trivial. All you have to do is add minifyEnabled true to build.gradle. The tricky part is maintaining a list of rules that specify which classes to keep. These rules are kept in the aapt_rules.txt file. That’s where the Android specific files are stored. You can also specify that a method should be retained by adding @Keep to a method to flag it for keeping by Proguard. Just make sure to keep anything that might be called through reflection.
Besides using tools like Proguard, you can also do investigation into exactly what is going into your apk. More often than not a big dependency can be pulling in quite a few methods/resources. The first thing you can do to analyze dependencies is run a gradle task: ./gradlew app:dependencies. This will let you see a tree of all your dependencies. Alternatively, you can use the excellent open source tool ClassyShark github.com/google/android-classyshark. Classyshark is an APK file explorer that’s great for debugging Proguard. (See the bonus interview with the author at end of article)
Shrinking Your Resources
Another tactic for cutting app size is shrinking resources such as Strings or styles you forgot to remove. If you’re using Proguard, you can add shrinks.Resources true
to build.gradle
to remove any unused resources in your app.
Advanced App Shrinking
The final strategy for cutting APK size is to segment your app into parts based on density, texture (game development), or architecture, so users can download only the code and resources tailored to their needs. You can do this by splitting the APK into many smaller APKs targeted at devices and uploading them all to the Play store. The only caveat is that you will need to generate different version codes for each APK. Otherwise, splitting APKs is trivial:
android {
splits {
density {
enable true
exclude “ldpi”, “tvdpi”, “xxxhdpi”
compatibleScreens ‘small’, ‘normal’, ‘large', ‘xlarge’
}
abi {
enable true
reset()
include ‘x86’, ‘armeabi-v7a’
universalApk true
}
}
}
Hear from an Expert - More on Classy Shark
For a deeper look at app slimming tactics, I got the skinny from Boris Farber, a Google Developer Advocate and the author of Classy Shark.
Q. When an app is slow, what is first thing you look at?
I always start by looking at the dependencies. Usually, the problem isn’t that my code is slow, especially on new devices. The problem more often is that I am using different things that I do not know the cost of. Don’t worry too much about optimizing your code, optimize your decisions. [Note: NimbleDroid shows you problematic dependencies]
Q. Classy Shark was mentioned in Wojtek Walicinski’s I/O talk about APK size. Could you provide a few details on what it is?
Classy Shark is a browser that allows me to look at APKs as a first class citizen. Now something that runs on your user device is something that you can think and reason about. Rather than sharing shell scripts you share something within a tool.
Q. What are some of the use cases that merit considering Classy Shark?
I’d say you should use it as a toothbrush–twice a day. Always keep your finger on what is going on in the APK. If you add a dependency, check Classy Shark. The earlier you find problems the better. If you find them later in the process, it makes change much more painful.
Q. I’ve worked on projects where the libraries we used were not perfect for the use case, which hurt performance. What do you look for when evaluating whether to add a new library to your app?
When I consider a library, the most important criteria is, “does this library solve my problem?” I try to find three to four candidates and then evaluate. This includes looking at the dex count, examining how it plays with other dependencies I am using, and evaluating performance in the app - but the most important thing is that it has to solve my problem at hand.
Q. Do you have advice for developers who work in an organization that doesn’t prioritize performance?
Well, we’ve found that Play store reviews are correlated highly with APK size and performance. That said, things should not be taken too far to other end, so don’t prematurely optimize. Find specific bottlenecks/problems and then figure out if they are worth optimizing. Some companies optimize throughput, others need graphic processing. Your solution needs to fit the specific problem as does your proposal.