Over the past few months, we've made a number of improvements for Android's Signeasy app. We’ve taken big strides in trying to create a better experience for our users, and after almost a year of effort, we've gone from looking like that (left, June 2015) to this (right, June 2016)!
The revamped app was built around the principles of Material Design (which is an interesting story on its own). Not only does it feel faster, but it also provides an improved signing experience. Like all good things, though, this upgrade came at a cost, with the app bulking up to almost 35 MB. Unacceptable! We could feel our users' pain, and decided it was time to put the app on a much-needed diet. Here are some of the techniques we used.
Apps tend to use a bunch of libraries to keep themselves up and running, and I don’t just mean that 3rd-party library you added for animations. You also need to include SDKs for backward compatibility for any of the Google APIs, or even to make your app material. Every dependency you add may significantly contribute to the APK size, especially if your app has been in production for a long time. A good starting point is to trim any unnecessary libraries from your app.
This is a great tool to see which libraries your app is using, including a count of the number of methods each one has. The larger the number, the more it ultimately contributes to your APK. Remove all the unused libraries, especially the bulky ones.
ProTip: If your app is using Google Play Services, make sure you aren’t including the entire package in your build. From v6.5, you can selectively include the libraries that you need – no more, no less. It’s a neat little trick that helped us cut some serious MBs.
Make sure you’re applying ProGuard to your APK. It’s a very effective tool that removes all unused classes, methods, and fields from the packaged app, including any from the libraries.
If you use AppCompat-v7 or support library-v4 in your app (which you probably do), make sure your ProGuard file doesn’t have any of these lines.
The support libraries are large on their own, and it helps if you allow ProGuard to remove all unused classes.
ProTip: If you’re using AppCompat’s SearchView in your app, adding the above two lines may cause things to break. This is because any of the AppCompat elements that are used in XML as a string will not be recognized by AAPT and the class is stripped off by ProGuard. To avoid this, add this line back to your ProGuard configuration.
(This works with any class that is used in XML as a string.)
Minifying and shrinking
A simple technique is to tell Gradle to shrink resources and remove unused resources for you when you package your app. In your build.gradle file, add the following to the build type you want to affect:
Given the wide array of Android devices on the market, developers usually add assets for 5 different densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi), as well as different assets if you have a tablet-specific UI, like we do. All of these images packaged into a single APK significantly contribute to the bulk.
I once got a great piece of advice from a Google Developer Advocate: identify the devices most of your customers are using, and if they don’t fall in the mdpi-hdpi buckets, remove assets for these two densities. Sounds a little risky at first, but it’s one of the most effective ways of getting rid of a good chunk of those MBs. Sure, it takes slightly longer for the xhdpi asset to be rendered on these devices, but it’s hardly noticeable.
Furthermore, as mentioned earlier, the Signeasy app has some assets specific to the tablet form-factor. Due to their larger dimensions compared to phone assets, these images tend to be larger in size as well. We tried to figure out which density bucket most of our tablet users fell into (this helped a lot) and were surprised to see most tablet devices were of either mdpi or xhdpi density. Removing all other assets for tablets affected the APK size drastically.
Adopt vector drawables ... seriously
As an Android developer, one of the best practices you can adopt is forcing your developers and designers to start using SVGs for image assets to be included in your project as vector drawables. This allows you to replace multiple PNGs with a single vector graphic that maintains its sharpness regardless of the density of the device it is rendered on – aka pure magic. Although it was first available only for Lollipop and above, support library 23.2 includes support for vector drawables all the way back to API 7. Plus, Android Studio 1.4 introduced a nifty little tool that helps you import vector graphics into your project. This will definitely help keep the APK size down.
Here are some techniques that will help you chip away at those smaller unused resources. Although these don’t make a big impact on the APK size, every KB ultimately counts. Plus, it helps keep your code base clean.
This is a very handy tool built right into Android Studio (but oddly hard to find). Go to Analyze > Run inspection by name. In the Inspection dialog, type in unused resources, select your scope (whole project recommended) and run. Lint analyzes all the resources (strings, drawables, dimens, etc.) that aren’t being used anywhere in your code. You can go to each suggestion and delete the resource, or simply ask Lint to delete everything for you. It’s easier to do the latter and then re-add something you accidentally deleted, especially if you’ve never done this before, as Lint will probably end up suggesting a very large number of unused resources.
Pick your languages
An interesting technique that was discussed in Google I/O 2016 was specifying which languages you localize in your Gradle script. This strips away all other string files that could have been added by other libraries in languages you don’t even support. To do this, specify the languages you do support in your app-level build.gradle file:
Splitting your APK
The Gradle system allows you to produce multiple APKs for a build depending on the split criterion. Although this isn’t encouraged by the Android documentation, sometimes the most effective way of trimming the APK is by, er, chopping it up. You can do this in various ways:
This is especially useful if your project includes libraries written in native code (.so files) which support different CPU architectures. You can split your APK per architecture so that a user running an arm ABI doesn’t receive the code for x86, and so on. To do this, add the following to your app level build.gradle file:
Density level split
This approach lets you produce multiple APKs that are split by the density of the device, so that a user with a xhdpi device doesn’t carry assets meant for xxxhdpi. To do this, add the following to your app-level build.gradle file:
For a comprehensive list of more split options, read this.
I also recommend watching this Google I/O 2016 talk and reading this related blog for more ways to reduce your app size.
After painstakingly applying most of the above techniques, the Signeasy Android team has successfully decreased our app size by almost 30%. That being said, we aren’t completely satisfied yet, and plan on trying out even more ways to go easy on our users’ device storage. Remember, every KB counts.
For questions, thoughts, and comments, find me on Twitter @ApoorvaTyagi.