Reducing Android app size in practice at LinkedIn

This post has been republished via RSS; it originally appeared at: Microsoft Mobile Engineering - Medium.

Photo: Daniel Romero from Unsplash

App size is a key metric impacting user acquisition and retention: If the app is too large to download, users may skip installation or cancel the installation during downloading; if the app takes too much storage space after use, users may choose to uninstall.

There are also different measurements of Android app sizes:

  • Download size: The compressed size of the release downloaded by users at install time.
  • Install size: The approximate size of the recent release on the user’s device after installation, but before the app is opened for the first time.
  • Space used shown in Storage Settings: Sizes of files under cache directory and other directories under /data/data/com.app.package.name

This post mainly focuses on the download size. This is displayed directly in the Google Play console for developers, and also displayed in the about this app section in Play Store. The official documentation on Android Developers already covers many different ways to optimize the app size. However, in this blog post, we would like to emphasize some of the less known yet powerful techniques to reduce app size. These techniques helped LinkedIn to minimize its app size in recent years.

Play Feature Delivery

a.k.a Dynamic Features, Dynamic Modules, or On-Demand modules

Android App Bundle, as a publishing format in Google Play, was recently escalated to the requirement for publishing new applications in August 2021. The app bundle is used to generate multiple optimized APKs. Then the user will only download the specific APK set optimized for their device. The download size savings come from:

  • ABI libraries that do not match the device’s processor architecture
  • Resource files that do not match screen density
  • Language resources that do not match the user’s device language
Application size savings from Android App Bundle (Google I/O 2018) — https://youtu.be/flU42CTF3MQ

Migrating to an Android App Bundle should be a streamlined process. At least this was the case for LinkedIn. With Android App Bundle, we were able to explore some advanced features, such as Play Feature Delivery.

This feature was particularly helpful when we introduced the Native Video Meeting on LinkedIn in 2021. In this feature, we need to add Azure Communication Services SDK into our application. If we add this SDK as a plain dependency, our app size would increase by 56% (~17MB) using our baseline device configuration.

Adopting Play Feature Delivery for the Native Video Meeting is obviously a necessity because this Native Video Meeting would be used by a small group of users in the initial launch and its entry point is quite deep in 1:1 messaging. If this feature becomes popular in the future, we also have the flexibility of configuring the feature module to be downloaded at install time.

Compare to the Android App Bundle, the road to adopting Play Feature Delivery was bumpy. Below are our learnings and impressions in this journey:

🔑 Android App Bundle

Android App Bundle is indeed a mandate for Play Feature Delivery. You have to upload the app signing key to Google Play Store (Play App Signing). Please do consult your security engineering before doing so, and consider code transparency if needed.

🤷 Conflict with resource shrinking

Although reported back in 2018, Play Feature Delivery and resource shrinking cannot both be enabled at the same time. There is a new gradle property android.enableNewResourceShrinker introduced in Android Gradle Plugin 4.2 and is promised to be the default behavior in the incoming Android Gradle Plugin 7.1.

🧪 Testing

It is necessary to upload the app bundle to Play Console to accurately test out the actual user experience. Additionally, to remove the necessity of connecting to the Play Store, especially to set up the CI test pipeline, we need to leverage the option --local-testing from bundletool , as introduced in the testing section on Play Feature Delivery. All these additional steps for both manual testing and CI testing are well documented, but they are not trivial at all. We dedicated a certain amount of time to learning and integrating with the existing tooling and processes.

📈 App size tracking

We used to report a single app size for our baseline device configuration. Now we added a new feature module, and potentially more in the future. This resulted in a set of numbers, rather than a scalar value. To describe the app size, we may use the base app size, the full app size (Here is a Github feature request), and the size of each individual feature module. A significant amount of additional attention became necessary to monitor the app size.

R8

R8 is a built-in toolchain shipped with Android Gradle Plugin. It helps reduce the app size through minification. The word is chosen meticulously as we encounter unexpected confusion among our developers. R8 supports 3 different operations and each of them is a unique concept!

Shrink the code and resources

This operation removes unused classes and resources from the app. The official documentation covers this part very nicely, and there are plenty of other resources like Proguard Manual and Android’s Built-in ProGuard Rules: The Missing Guide.

There were some additional positive surprises when we upgraded to Android Gradle Plugin 4.1. Because R class is no longer kept by default, we observed a 6MB reduction in our app size. As such types of changes are often not announced (The release note for R class was requested to be announced), a good practice is always to try out the latest preview versions of AGP.

Obfuscation

Obfuscation indicates shortening the names of classes, methods, and fields. There are two side effects: 1) App size gets smaller 2) Stacktrace becomes unreadable.

We have not been able to try out obfuscation in production, but our local testing results showed a noticeable decrease in app size at around 4MB. See the Retrace section for why we waited for so long.

Optimization

R8 can rewrite the compiled code in a more optimized fashion. Jake Wharton’s blog post series talked about different R8 optimizations with insightful details. The most common misunderstanding though, is that R8 optimization is actually not configurable. Except for android.enableR8.fullMode enabling more aggressive options and allowaccessmodification to determine the scope of lambda groups optimization, R8 optimization is actually all or nothing. Quoted from Android Developer Documentation, maintaining a standard behavior for optimizations helps the Android Studio team easily troubleshoot and resolve any issues that you might encounter.

We enabled the R8 optimization recently and already observed a 4.5MB decrease in our app size.

Decoding stack traces

Either obfuscation or optimization may incur unreadable stack traces. You may see random strings in the method name, such as $InternalSyntheticLambda$0$0342c10baa0d2338bea683e42a28268917f26404fd49a57d48a68962076c0860$0.onChanged$bridge. You may also see Unknown Source in the file name. If you are experiencing this, check out how to decode the stack trace using Play Console or the command-line tool Retrace.

One issue we encountered when enabling R8 optimization, is that the same crash could have different stack traces. Some nonsense stackframes are removed with R8 optimization, but this still breaks the error grouping in our in-house crash reporting tool. Some newly popped-up crashes were actually known crashes with a few stackframes removed.

Unfortunately, these scenarios are quite common. Any stack trace containing lambda group looks different with R8 optimization enabled, and our analysis showed that 25% of the crashes fall into this category. To resolve this, we would need to use the retrace bundled in the latest R8 according to this AOSP issue, rather than using the standalone command-line tool. The command-line tool is released at a much slower cycle.

Since AGP releases with a bundled R8 version, you may need to consider using an R8 version ahead of AGP, in order to get the latest bug fixes and features. I am attaching the Gradle script here since it was not easy to find on the open Internet.

buildscript {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven {
url = "https://storage.googleapis.com/r8-releases/raw"
}
}
dependencies {
// Must declare this before declaring AGP
classpath "com.android.tools:r8:3.1.30"
classpath "com.android.tools.build:gradle:7.0.3"
}
}

Resources

There are plenty of techniques mentioned in the section Reduce resource count and size:

  • Prefer the OS ImageDecoder rather than a third-party solution
  • Leverage WebP format (There is a new AVIF format supported in Android 12)
  • Use VectorDrawable to create resolution-independent drawable resources.

Beyond the aforementioned, we also employed the following techniques:

Loading image resources after installation

While adding image resources to Android is a fairly common implementation, there are cases where we need to display promotion or sample images on the screen with high resolution, such as the sample images for the profile background photo. Rather than shipping those images with the app, we actually push those images on the CDN and download them at the runtime. This was proven to be an effective way to save app size and we standardized the process of adding images likely this.

Are we actually saving users’ storage size and network usage? I believe this is a worthy trade-off that we reduce the app size at download/install time, at the cost of potentially increasing network usage if those images are frequently evicted from the disk cache.

Jetpack Compose

Jetpack Compose is on its way to becoming the recommended way to build UI declaratively on Android. When we first prototyped Jetpack Compose in our app of building a new page, we found that the app size increased by 740KB, because of all the library methods that need to be dexed. But as shown by the APK comparison performed in Jetpack Compose — Before and after, we are expecting app size to decrease over time as we migrate pages from Data Binding to Jetpack Compose.

If you are still doubtful about adopting Jetpack Compose in your production app, long-term app size reduction could be yet a convincing argument.

Takeaway

In order to make data-driven decisions, we conducted a quantitative analysis internally on the correlation between the app size and new app installs. With data, it is much easier for us to convince engineers, products, and management on the importance of maintaining a healthy app size.

If you are interested in optimizing the app size, I would suggest following the steps with the official documentation. Some of the advanced techniques we adopted at LinkedIn include:

  • Play Feature Delivery (Dynamic Modules, Dynamic Delivery)
  • R8 optimization and obfuscation
  • Serving image resources with CDN
  • Adopting Jetpack Compose

I would like to appreciate Kyle Koser from my team and Cesar Valiente from Microsoft to review this blog post!


Reducing Android app size in practice at LinkedIn was originally published in Microsoft Mobile Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.

REMEMBER: these articles are REPUBLISHED. Your best bet to get a reply is to follow the link at the top of the post to the ORIGINAL post! BUT you're more than welcome to start discussions here:

This site uses Akismet to reduce spam. Learn how your comment data is processed.