This article describes ways of dealing with native libraries in Android applications.

Nowadays native libraries become more and more popular. They are mainly created for performance, security and cross-platform portability reasons. Popular examples are Unity, Realm, and OnFido.

As opposed to Android applications that run on Android Virtual Machine (Dalvik or Android Runtime(ART)), native libraries are compiled directly into native processor (CPU) code. This code varies depending on architecture (set of instructions) supported by the processor. The Android platform supports multiple processor architectures:

mips

mips64

armeabi

armeabi-v7a

arm64-v8a

x86

x86_64

If we want to use native libraries in our application, we must provide different compilations for each of the CPU architectures we want to support. For each architecture we have one or more separate .so files containing native code:

Native libraries in the project

When we decide to support CPU architectures, our APK (Application PacKage) file size can grow like crazy. When a user downloads the app, despite only needing the set for his device architecture, he’ll get all of them.

Let’s use APK Analyzer (that is provided with Android Studio 3) and check what happens inside our APK containing native libraries:

After running Analyze APK command and selecting APK file we can take a look at its internals. We can instantly notice that lib folder that contains native libraries is the biggest folder in the APK file:

lib folder 45MB

When we take a closer look we see how much space is consumed by native libraries for each architecture:

Current APK limit in Google Play Store is 100MB. Our native libraries consume 45MB which is almost half of allowed APK space. To decrease APK size we need to limit our APK to support certain architecture(s). There are two techniques that we should consider:

ABI Filters

APK Split

ABI filters

ABI (Application Binary Interface) is an interface between two program modules; often, one of these modules is a library or operating system facility

ABI filters allow us to filter certain kind of architecture(s) that we want to include into single APK file.

By removing unnecessary architectures we can generate single APK file that has much smaller size (notice that there are not many devices that support x86 architecture for Android, but the libraries for this architecture are the biggest ones).

We can apply ABI filters to default config to affect all the builds. The downside is that if we want to test our builds on emulator we usually need native libraries for x86/x86_64 architecture. That’s why it may be the better idea to configure filters only for our release build.

This solution has pros and cons, so let’s consider two points of view — user perspective and developer perspective.

From the user perspective, APK contains more libraries than user needs (two architectures instead of one), so this means bigger APK size. At the end, the user will use more mobile network data and wait longer for application to download install. This may decrease overall UX.

Keep in mind that in some emerging markets (like India) mobile data transfer is quite expensive. Users often share the application using Bluetooth or Hot-Spot via SHAREit or Xender (those apps have around 100-150M users). If we downloaded app that contains native libraries only for arm64-v8a architecture it will not also be working when we share with the device supporting armeabi-v7a after sharing the APK file (other ways around this would work because arm-v8a is backward compatible).

From a developer perspective, this solution gives single APK to maintain which can save us a lot of time. This means no additional overhead related to configuring build process and APK testing and maintenance (read here about supporting multiple APKs).

If we use a single library that adds only few MBs for each architecture then overall “user cost” may be small and multiple APKs may not be worth the our effort. However when we use multiple native libraries total size of the libraries for each platform may be quite big:

85–42MB, armebai-v7a-31MB, arm64-v8a 16.2MB

If we care about users transfer or exceed 100MB APK limit ABI filters are no longer an option. We need to make sure that users will download only the native libraries that they actually need. That’s where APK split comes into play.

APK split

Apk split allows us to generate multiple APK files. We can perform split by screen density (mdpi, hdpi, xhdpi…) or architecture (arm64-v8a, armeabi-v7a…). Let’s configure split by architecture:

One APK file per each architecture will be generated and (optionally) one universal file containing all architectures.

If we would also configure splits by screen density then APK files for all density+architecture combinations would be generated (app-uk-mdpiArmeabi-v7a-debug.apk, app-uk-mdpiArm64-v8a-debug.apk, app-uk-hdpiArmeabi-v7a-debug.apk, app-uk-hdpiArm64-v8a-debug.apk, etc.).

For debugging purpose split may be unnecessary, so we can configure splits only for our release builds. There are few ways to approach this. We can pass a parameter for Gradle build script to explicitly say that this build should be split. If you want to have ability to enable or disable splitting in your build server this is a way to go. However we can also configure splits for release build by retrieving Gradle task name while task is running. This will allows us to determine if the current Gradle task is a task that creates release build (eg. assamblePaidRelease) and make decision whatever we want to enable APK split or not:

Now running assembleRelease tasks for each flavour will generate multiple APK files splited by CPU architecture, while running assembleDebug tasks will generate single APK file.

Version codes

Google Play Store does not allow to upload multiple APK files with the same version code. We need to ensure each APK has its own unique versionCode.

This code will allows us to override version code for each APK

To check if version code was properly generated we can open generated APK file using APK Analyzer and select AndroidManifest.xml file to display it’s content.

We could also update version name (eg. 3.13.0.78-arm64-v8a) to reflect current architecture by modifying additional versionNameOverride property in the previous code. We want to do it only when abiName is not null (we want to leave the name of universal APK intact)

Which architectures to support

We definitely need to support armeabi-v7a and arm64-v8a architectures because they have around 99% of market propagation. With x86/x86_64 there is an interesting case. From the mobile market perspective, they are insignificant and this is unlikely to change in the future. However, Google is pushing developers to make their Android apps compatible with Chromebooks which are mostly using Intel processors based on x86/x86_64 architecture (meaning we can consider adding support for it).

Most of the Chromebooks have Intel processors supporting Intel® VT-x, allowing them to run HAXM (the same technology we use to make apps in our emulators run much faster), so the app emulation is quite fast.

Summing up here is a complete list of Android architectures with usage recommendations:

mips (deprecated)

mips64 (deprecated)

armeabi (deprecated)

armeabi-v7a (required — most popular architecture nowadays)

arm64-v8a (required — newer version of armeabi-v7a)

x86 (optional, very limited number of devices, may be useful for debugging in the emulator)

x86_64 (optional, very limited number of devices, may be useful for debugging in the emulator)

Conclusion

As we can see there are no golden rules here. The solution that we may need depends on our native libraries size, ability to properly configure and manage build/release process and market we want to target.

Before making a decision about using ABI filters or APK split we should look at the size of our native libraries, check our user base and determine how many users will actually benefit from this change. Also we should consider cost of overall overhead on our side.