SKOUMAL studio
Published in

SKOUMAL studio

From zero to hero: Optimizing Android app startup time

Photo by Mathew Schwartz on Unsplash

I’m personally bothered by poor performance, wanting to leverage given resources to the best of my knowledge and abilities. This applies especially to the startup time of my (our) Android applications, where, gradually, you wouldn’t notice the change in performance that much.

That all changed when I got a work assignment on one of our legacy apps.

To my surprise, the legacy app launched comparatively fast. Really, really fast. Back in 2016, we didn’t even bother with obfuscations via ProGuard, so knowing this, I was really stunned. So I started to do some digging into what might be causing such a noticeable drop in performance.

Let’s talk first about how I measured the app startup performance.

  • OEM locked Pixel 5
  • Verbose adb reports
  • Debug app variant

When researching this topic I stumbled upon this article from Chet Haase. I however wanted to get the average app startup time, so I updated the script like so:

#!/bin/zsh

CUMULATIVE_TIME=0
LOOP_COUNT=0
PACKAGE_NAME="name.package.your"
MAIN_ACTIVITY="name.package.your.MainActivity"
getLaunchTime() {
adb shell am start-activity -W -n $PACKAGE_NAME/$MAIN_ACTIVITY | grep "TotalTime" | cut -d ' ' -f 2
}

echo ">> Test start <<"

for i in $(seq 1 25); do
LOOP_COUNT=$((LOOP_COUNT + 1))

adb shell am force-stop $PACKAGE_NAME
sleep 1

THIS_LAUNCH_TIME=$(getLaunchTime)
CUMULATIVE_TIME=$((CUMULATIVE_TIME + THIS_LAUNCH_TIME))

echo -n "."
done

printf "\n>> Test end <<\n"
echo "Average startup time: $((CUMULATIVE_TIME / LOOP_COUNT))ms"

There’s one important thing to mention. I did not lock my clock as Chet describes in his article. The reason being I don’t have my device (Pixel 5) unlocked and I really like to play with it. With that in mind, please take the results with a grain of salt as they will be a little bit skewed.

I mitigated the deltas as much as I could, not touching the device for at least 30 seconds before beginning the test to avoid “touch boost” triggering on the chip’s governor.

Since the old application I was talking about was not obfuscated I wanted to at least match the performance of this application and that’ll be best done in debug configuration. Don’t worry though I’ll be obfuscating the newer app and reporting the results.

Step 0: Initial Performance

I naturally started from the current state. So the usual routine, build debug, launch and measure. For the sake of consistency, I let the app launch about 25 times and had the script make an average.

I’m going to round to the tens because the unlocked governor can cause these 0–10ms delays anyway.

The number was 1170ms.

Step 1: Koin

My first thought was: “Koin… That must be it.”. We use it everywhere nowadays and the module declarations could potentially take some time on app startup time. I know the drawbacks of using a service locator instead of a “true” dependency injection, so I went ahead and rewrote the DI with Dagger in one of my own apps. Took me about half a day to have the app in the exact same state as it was before but with Dagger providing the dependencies.

I’d like to report that I wasted half of my day to confirm that Koin is insanely fast… and that I potentially may not have implemented the Dagger ideally.

App startup time: 1180ms. Naturally, I reverted the changes.

Step 2: Profiling

Saving no app startup time whatsoever, I turned to profiling. …only to discover that the Android Studio Profiler needs to startup before it even shows me the UI so I can start recording traces. That said I gave up on this idea since it wouldn’t really help me that much, knowing there are other options.

I found out the next day that I can have the device record all the traces for me and use Perfetto to show very detailed visualization of what’s really happening.

I employed logging through AppComponentFactory to deduce whether constructing objects is fast.

App startup time unchanged.

Step 3: Clearing up the Application class

Another very common way to halt the startup with initializing SharedPreferences, Coroutines, or generally any Thread Pools or long-running tasks in the Application class. I went ahead and removed or postponed (via Handler.postDelayed) everything that wasn’t necessary in order for the app to launch successfully.

Once I was done with these changes, although confident it would not change much because I’m really prudent about having the Application class clean, I ran the test again, and…

App startup time unchanged.

Step 4: Content Providers are evil?

I set out on a crusade to manually disable all ContentProviders. I found out there are some 5 Providers residing inside my app. 2 of which are Lifecycle based, 1 being Firebase owned, 1 WorkManager owned and the remaining one being my FileProvider.

I went ahead and rebuilt the app to test the results. Inexplicably the app crashed immediately after launch due to issues with Lifecycles. Who knew? With that failure, I moved to Step 5, since that was imminent anyway.

App startup time unchanged.

Step 5: androidx.startup

Implementing this library before, the process was quick and breezy, except for the Lifecycle Providers. As these two aforementioned Providers are not public to instantiate. Worry not, reflection to the rescue. Speaking of which, the Initializers are not created through AppComponentFactory, so I needed to add more measuring inline clauses. Build, launch, test!

App startup time unchanged. …but why?

Well, that’s because the Startup library aggregates the Initializers and runs the create methods right after each other. It lifts a few milliseconds here and there when initializing dozens of ContentProviders, but in this case, it didn’t really do much.

That said, you can always provide “Unit” and delay the initialization to a later time. It, however, had a negligible impact on everything but Firebase. It takes well over 60ms (on my device) to start up and find every component through its Services.

App startup time: 1110ms! Progress!

Step 6: Inspecting AppComponentFactory logs

I noticed that the Main Activity takes some 20ms to start up, which is weird as it only executes code in its onCreate method. The culprits were dependencies filled with “Lazy” implementations in order to execute some logic in onCreate. We can use some Koin magic and use get() instead of inject() right at the call site, which will help a little bit with the memory as the reference will be cleared eventually.

Everything else seemed to be in order, taking 1–5ms to create the instances, nothing weird or unusual.

App startup time: 1100ms.

Step 7: useEmbeddedDex=true

This comes as a surprise to me. I wanted to try out all options and this struck me as worth a try. I expected that by bypassing Android’s AOT, the startup time and overall performance would be worse, if not the same. Quite the opposite! It shaved off 140ms.

It doesn’t come as a recommendation though. It seems to be disabled by default for a reason and I’d argue that AOT performance improvements are more noticeable in heavy content-oriented apps, but I might as well be wrong. I’ll roll with it, for now, keeping in mind that I’d need to disable that later if any errors do occur.

App startup time: 960ms.

Step 8: Inspecting onCreate(s)

I don’t know whether I’m surprised at this point really, but implementing androidx.navigation exactly as the documentation says is deeply flawed. The fragment instantiation at layout inflation time costs about 300ms.

The fix is obviously easy enough… You can optionally wait until the entire layout draws, then create the NavHostFragment manually, assign it as the primary navigation fragment and commit it. The only thing remaining is to wait until it’s attached and start up your code. (or don’t at all if you don’t depend on NavControllers in your activity)

Surprise, surprise the app launches in 660ms. Wonderful.

Step 9: Obfuscation

At this point, I figured out the Perfetto method described at the beginning, but I did not really find any more long-running tasks other than classloader. This is understandable though as it loads even classes that are completely unused.

After applying the obfuscation the app crashed upon startup again. It’s a kind of obvious error I made before, since I’m loading the Lifecycle features through reflection, the system cannot find the newly obfuscated names. There are nonetheless two ways out of this trap. The first one is enabling the ContentProviders and removing the reflection, second add a proguard rule to keep names.

Because I’m a sadist who likes to have the app broken by updates to random libraries, I’ve chosen the latter. I strongly recommend to use the former though. Do as I say, not as I do (:

Finally, the app has built successfully, launched just fine and it’s time for our final measurements.

190ms!

That’s f*cking fast.

You have an ambitious startup idea. We create mobile apps and love challenges. Let's make your ideas happen.

Recommended from Medium

Android Kotlin DSL configured lists

React Native and Android Studio: Everything you need to get started in Linux

Generating Android signed release APK file in ReactNative

Modern features to consider for your Android app

Share Android Screen on your System

how to get image from gallery in android 10 and above

Configure URL in Flutter Web

What happens when volumeManager in the kubelet starts?

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
viktor de pasquale

viktor de pasquale

Android Developer @ Skoumal, s.r.o.

More from Medium

Our experience using MotionLayout in NFC payments

Kotlin Inline Classes in an Android World

Android Generic TableView — Fully Customizable Library for Presenting Data

[Kotlin][Android Studio]unable to resolve class org.jetbrains.plugins.gradle.tooling.internal.ExtraM