Decreasing Android app size using split APKs

Jacob Muchow
QuarkWorks, Inc.
Published in
6 min readJul 13, 2017

I work as a team lead on Android projects at a mobile app development company named Quarkworks. We work on a variety of products in the mobile sphere in Silicon Valley as consultants. One of our clients is hello Network, an up-and-coming social network focused on building connections between people with similar passions and interests.

For an app like hello, which has a large following in nations where bandwidth is at a premium, the download size of your app can make all the difference in converting users. Whatever we can do to slim the app down is a must.

When an APK is downloaded by a user, unused code and data is also downloaded. This isn’t ideal, but most Android developers are doing it this way since it’s the default. Fortunately, the Google Developer API is robust and allows for multiple APK support if you want to target specific devices with slimmed down APKs.

If you’re like me and use Realm Database, or any other module that has processor-native code, it is possible to generate multiple APKs that specifically target each supported processor architecture (ABI). This can also be done by screen size, API level and some more niche characteristics. If you are including many assets split by DPI in resources, you may want to consider doing this.

You can inspect the contents of an APK in Android Studio by opening it.

Modules such as Realm include code chunks on a per-ABI basis which are all included in your APK. Here you can see that Realm is occupying 4.9 MB (27.1%) of the total APK. What if we only included the .so file that would be used by the end-user? Our app download size might stand to go down by ~4 MB. We can make it happen with just a few changes to our Gradle file.

build.gradle

splits {
abi {
enable gradle.startParameter.taskNames.contains("assembleRelease")
reset()
include 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
universalApk false
}
}
buildTypes {
...
}

This code chunk is adapted from the recommended one on realm.io. It tells Gradle to generate multiple APKs by ABI type during a release build (their example does it for all builds. I didn’t want to split for debug or our Crashlytics Beta build variant). Also note that universalApk is false by default, so it doesn’t need to be included, but I am going to make a point about that later.

If you read the Google documentation about multiple APK support, you will learn that each APK you upload to the Developer API / Console will need a different version code and each new APK must have a greater code than a previous fitting APK. This means that for multiple APK support, you will need to have different version codes for each APK even though they will be active at the same time. Here is my solution (adapted from Google this time):

build.gradle

android {
...
}
// Map for the version code that gives each ABI a value.
ext.abiCodes = ['armeabi-v7a':'1', 'arm64-v8a':'2', 'mips':'3', 'x86':'4', 'x86_64':'5']
import com.android.build.OutputFile// For each APK output variant, override version code of outputs based on ABI codes
// ex) 'mips' -> 3xxx
// ex) 'x86' -> 4xxx
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def baseVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (baseVersionCode != null) {
output.versionCodeOverride = Integer.valueOf(baseVersionCode + variant.versionCode)
}
}
}

This code will override the default version code for each of the generated APKs and apply a prefix number to it. For example, Hello is on build 257, so the version code for the mips-targeted APK will be 3257. The next build will be 3258. This works with the Developer API’s rules about version numbers because the rules are specific to the set of characteristics that an individual APK fits. In other words, version codes for APKs that support different devices will not be considered against each other. For more information, read the multiple APK support documentation.

This is Google’s suggestion for overriding the version code by ABI:

“…override versionCode with a combination of ext.abiCodes * 1000 + variant.versionCode”.

I think this method may not work permanently because they are adding numbers together rather than truly prepending. This could lead to situations where build numbers are repeated, which would produce an error on the Google Developer Console. ex)

1st arm64-v8a build: 2 * 1000 + 1 = 20011001st armeabi-v7a build: 1 * 1000 + 1001 = 2001

Using this method, when we get to the 1001st build of the app, the Google Developer API will reject the update! Sometimes with these types of mathematical conflicts, the numbers will never be reached because they are too far apart, but 1001 builds really doesn’t fit that bill.

I think if you concatenate the two numbers as strings, then use Integer.valueOf(), as in my solution, you have an equation that will never have conflicts provided you don’t change the array values… someone comment if I am wrong.

If you run a release build now, you should get something like this in your outputs. To check your version codes, inspect the APKs using Android Studio and open the AndroidManifest.xml inside. versionCode should be one of the first attributes for manifest at the top of the file.

Also note the change in size of your APK. Ours decreased from 20.7 MB to 13.7 MB. You can see in the screenshot below that only one copy of the Realm native distribution is included in the APK now.

That’s it! You should be good to go. There’s only one problem: uploading all those will be a total pain in the ass. I don’t know about you, but I don’t want to drag and drop 5+ APKs onto the Developer Console, waiting for each to upload. My time would be better spent writing code. Luckily, there are tools such as fastlane which can automate this process gracefully. I’ll publish a separate post about this soon, but you should check it out!

Now I want to bring this back up:

Also note that universalApk is false by default, so it doesn’t need to be included, but I am going to make a point about that later.

At first, I tried doing this with universalApk set to true after reading some Android documentation that said this: “assigning a lower versionCode to the universal APK ensures that Google Play Store tries to install one of your APKs before falling back to the universal APK.”

I took this to mean that I could use a universal APK as a backup for a failed app install, but this is not true. In my use case, the 5 ABI distributions from Realm cover all possible Android devices. When I tried uploading the APKs to the Google Play Developer Console, I kept getting an error that the universal APK out of the bunch would be superseded. It took me awhile to figure out that a universal APK was not necessary since I had all the possible use cases covered. However, note that if you have less than all 5 ABI type splits, or something to that effect, you can (and probably should) include a universal APK for your app.

As ever, QuarkWorks is available to help with any software application project — web, mobile, and more! If you are interested in our services you can check out our website. We would love to answer any questions you have! Just reach out to us on our Twitter, Facebook, LinkedIn, or Instagram.

--

--

Jacob Muchow
QuarkWorks, Inc.

Just trying to do something great. Software engineer and co-founder at QuarkWorks, Inc. Working on realtime animations for digital characters using your camera.