Illustration by Virginia Poltrack

App Startup, Part 2

Lazy Initialization

Chet Haase
Android Developers
Published in
8 min readDec 16, 2020

--

In the previous article, I showed how content providers (which show up in an app’s merged manifest file) automatically load some libraries and modules at launch time.

In this article, I will cover the use of the AndroidX App Startup library to have more control over when and how those libraries get loaded. And maybe, just maybe, we’ll see how to save time at app startup along the way.

Auto-Init with the App Startup Library

The simplest way to use App Startup is to implicitly use its content provider to initialize other libraries. You do this by telling App Startup how to initialize these other libraries and by removing their content providers from the merged manifest. This essentially reduces all of those separate content providers into a single one for App Startup, which is used to load the Startup library and then everything else.

You do all of this in three steps, by adding App Startup as a dependency in your build.gradle file, creating an Initializer for each library that needs to be initialized, and adding information to your Manifest.xml file.

Let’s look again at the WorkManager example that I was using in Part 1. To load WorkManager via App Startup, I first added App Startup to my app’s build.gradle file:

implementation “androidx.startup:startup-runtime:1.0.0”

Next, I created an Initializer, which is an interface provided by App Startup:

class MyWorkManagerInitializer : Initializer<WorkManager> {
override fun create(context: Context): WorkManager {
val configuration = Configuration.Builder().build()
WorkManager.initialize(context, configuration)
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// No dependencies on other libraries.
return emptyList()
}
}

Every Initializer has two functions to override: create() and dependencies(). dependencies() is used to establish a specific ordering in which to init multiple libraries. I didn’t need that functionality in this case, since I was only dealing with WorkManager. If you use several libraries in your app, check out the App Startup user guide for details on using dependencies().

For the create() function, I mimicked what I saw in WorkManager’s content provider.

By the way, this tends to be the way to go about this part of using App Startup; a library’s content provider is responsible for initialization, so you can usually use the code in that class as a hint on how to do it manually instead. There can be problems in some libraries if they are calling hidden or private APIs, but fortunately WorkManager was not, so this worked for my situation. Hopefully it will work for yours.

Finally, I added two provider tags inside of the <application> block of Manifest.xml. The first was this:

<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />

This WorkManagerInitializer tag is important because it tells Android Studio to remove the auto-generated provider that comes from adding the WorkManager dependency to the build.gradle file. Without this special tag, the library would continue to be initialized at startup automatically, and you might get an error later when App Startup tries to initialize it, since it has already been initialized.

Here is the second provider tag I added:

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.startuplibtest.MyWorkManagerInitializer"
android:value="androidx.startup" />
</provider>

This InitializationProvider tag is almost the same as the one that is auto-generated by simply adding the startup dependency to the build.gradle file (as you can verify by looking at the merged manifest file — see Part 1 for more details on that). But there are two important differences:

tools:node="merge"

This is an attribute for manifest merging that is managed by Android Studio. It tells the tool to merge multiple incarnations of this tag in the final merged manifest. In this case, it will merge the <provider> that was auto-generated by the library dependency with this version of the provider, so there will be only one in the final merged manifest.

The next interesting line holds the meta-data:

<meta-data     android:name="com.example.startuplibtest.MyWorkManagerInitializer"
android:value="androidx.startup" />

This metadata tag inside of the provider tells the App Startup library where to find your Initializer code, which will be run at startup to init the library. Note the difference in how this happens: when you are not using App Startup, initialization happens automatically because Android creates and runs the content provider in that library, which then inits the library itself. But by telling App Startup about your Initializer, and by removing WorkManager’s provider from the merged manifest, you are telling Android to use the content provider of App Startup to load WorkManager’s library instead. If you init multiple libraries in this way, you effectively pool all of these requests through this single App Startup content provider instead of causing each library to create its own.

Be Lazy… If You Feel Like It

When addressing startup performance, we can’t change what happens in code that we don’t control. So the idea here is not to speed up the initialization time of the libraries that we use, but rather to control when and how those libraries are initialized. Specifically, we can decide for any given library that we need whether it should actually be initialized at launch time (either using the default mechanism of libraries adding content providers to the merged manifest, or by the technique of pooling initialization requests in App Startup’s content provider), or whether we want to load them later.

For example, maybe there is a particular flow in your app that needs some content-provider-initialized library which doesn’t happen immediately upon startup. Or may not even happen at all in some usages. In that case, why spend time initializing a large library at startup that is only needed in that code path? Why not, instead, wait until the library is actually needed to take on that initialization cost?

This is where App Startup shines: It helps you remove hidden content providers from the merged manifest and from the startup process, and to initialize these libraries later and more intentionally.

Lazy-Init with App Startup

So now we know how to load and initialize libraries automatically using App Startup. But let’s take it a step further to see how to do it lazily, in case you do not want init things at launch time.

We’re actually almost there with the code above: you need the same dependencies in build.gradle for startup and whatever other libraries you want to use. And you need the special “remove” provider tag to strip out the auto-generated content provider for each of the libraries. All we need to add at this point is a little more information to the manifest file to tell it to also remove the App Startup provider. Then none of these pieces will happen at start time, and it will be up to you to trigger initialization whenever you deem the time is right.

To do this, I replaced the InitializationProvider in the previous section with the one below. The one I showed above told the system where to find the code to auto-initialize your libraries in its content provider. Now I want to skip that part and just have it remove the auto-generated provider for startup instead, since I manually trigger the initialization later:

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />

After I made this change, there were no longer any content providers in the merged manifest, so neither App Startup nor WorkManager were being automatically initialized at launch time.

To initialize these libraries manually, I added the following code elsewhere in the app to do that:

val initializer = AppInitializer.getInstance(context)
initializer.initializeComponent(MyWorkManagerInitializer::class.java)

AppInitializer is provided by the App Startup library to connect these pieces. You create the AppInitializer object with a context object, then pass it a reference to the Initializer(s) you created for initializing the various libraries. In this case, I pointed it at my MyWorkManagerInitializer and I was done.

Timing Is Everything

I ran several tests (using the timing techniques covered in my Testing App Startup Performance article) to compare all of the different ways I had of launching my app and initializing the libraries. I timed launches of the app without any libraries, with WorkManager included (using the default auto-generated content provider), with WorkManager auto-initialized by App Startup at launch time, and with WorkManager and App Startup lazily initialized using AppInitializer.

Note, as mentioned before, that these timings were with locked clocks, as discussed in that testing article, so these durations are much larger than they would have been with unlocked clocks. They are only meaningful in comparison to each other, and not to any real-world situation. Here’s what I found:

  • Without WorkManager: 1244 ms
  • With WorkManager loaded via content provider: 1311 ms
  • With WorkManager loaded via App Startup: 1315 ms
  • With WorkManager (loaded lazily, not at startup): 1268 ms

Finally, I timed how long it took to initialize WorkManager manually with AppInitalizer:

  • WorkManager Init via AppInitializer: 51 ms

There are a couple of takeaways from this data. First, WorkManager added an average of 67 milliseconds (1311–1244) to the startup time of my app when it was loaded during startup. Note that loading it the usual way (with content providers) took roughly the same amount of time as loading it with App Startup (1315–1244 = 71 ms). This is because App Startup isn’t actually saving us anything for the single-library case; we’re just shifting the work to happen in a different code path. There can be benefits from loading several libraries through App Startup, but for the single-library case here, there is no time-savings benefit to this approach.

Meanwhile, initializing WorkManager lazily allowed me to defer about 51 ms of that duration until a later time.

Is this significant enough for you to worry about? The answer, as always, is “it depends.”

51 ms out of 1.3 seconds is less than 4% of the total, and it would be even less in a real app that was doing much more than my simple app. This duration might not be worth the bother in your situation. On the other hand, you might find that some of your libraries take much longer to initialize. Or, even more likely, you probably use several libraries with content providers, each of which adds another chunk to your startup time. If you can defer most or all of these to a more appropriate time and get them out of the startup path, then maybe you could see significant launch-time benefits from App Startup.

Like all performance projects, the most important thing you can do is to analyze the details, measure, and then decide:

  • Look at your merged manifest. How many <provider> tags do you see?
  • Can you remove some or all of those content providers out of the merged manifest using App Startup and see how it impacts startup time? And can you do so in a way that does not impact runtime behavior? (Note that you need to make sure to initialize libraries before your app starts implicitly depending on their functionality.

In the meantime, happy performance testing and improving. I’ll keep looking into more ways to analyze and improve app performance, and will post about it when I find anything worthy. When there’s time.

--

--

Chet Haase
Android Developers

Past: Android development Present: Student, comedy writer Future: ???