App Startup, Part 1
The first thing I realized was that its name, App Startup, implies perhaps a bit broader capability than it actually has. The library is not focused on general startup concerns (at least in its current incarnation); it is specifically aimed at minimizing the impact of initialization that happens automatically because of content providers.
Now maybe you’re like me and never thought about how libraries are initialized. And maybe this is because a lot of this happens behind the scenes. Specifically, you add a line to your
build.gradle file to include a library as a dependency and you’re done (apart from calling the APIs in that library — otherwise why did you add it?).
But many libraries aren’t just packs of methods waiting to be called; they often need to be initialized first, which can take significant time. And worse, especially in terms of hidden-gotchas, these libraries are often loaded and initialized at startup, by virtue of the fact that they use content providers.
Share to your Heart’s Content [Provider]
Content providers are Android’s way of sharing access to application data between applications. For example, contacts on a phone are shared through a content provider, allowing other apps to access the user’s contacts (assuming the user granted them permission, of course). Similarly, you can provide access for other applications to data that your app creates. Perhaps your app manages a database of donut ratings, which seems like pretty important information that other apps may need to, er, consume frequently.
Content providers are automatically created and run whenever an app using anything which declares content providers starts
The important, and perhaps unobvious, issue with content providers is that they are automatically created and run whenever an app using anything which declares content providers starts. And note that an app can be started not just by the user launching it, but also by the system accessing services in the app, or when a recurring job running on behalf of the app is launched by the job scheduler, and so on. All of these trigger the overhead of content providers and the work they spawn. The system needs that functionality to be present if and when someone asks for access to that provider, so it runs the content provider automatically at launch time.
This detail is not apparent to developers using these libraries, because the details of how it happens are buried in auto-generated code. In particular, you have to look at the merged manifest file to see how this happens.
Most of my interactions with Android’s application manifest have been solely with the
Manifest.xml file that is created for me, which I edit to add things like additional activities, services, and permissions. But this manifest file is not the final one given to the system; it just provides your app-specific information as input into the actual “merged” manifest. The merged file is created from your
Manifest.xml file, along with other information that the tools pick up, including manifests from libraries that your app uses. It is in this final merged manifest that we see what’s happening with library content providers.
Let’s take a look at a specific example. Not all libraries have content providers (though it is quite common), so we’ll look at one that does: WorkManager. To use
WorkManager in my project, I added this dependency to my app’s
After I sync’d and built the app, I ran some startup timings (more on that later), to compare my launch duration before and after this change. I noticed that my app was taking about 70ms longer to launch than it did before… and that was without calling any of the functionality in
WorkManager. All I did was add that dependency above.
The reason for the additional launch delay was found in the merged manifest file, which I could see by clicking on the Merged Manifest tab at the lower-left of the Android Studio editor window while viewing
In this merged version of the manifest, I saw that including
WorkManager had put a lot more information into the manifest, including this provider block:
Curious where this provider came from, I clicked on the first line of it, which conveniently took me to
WorkManager’s manifest file, which contained the following:
<!-- ... and a bunch of other stuff ... -->
What happened was that the merged manifest file combines the manifest files of all of the pieces that make up the app, including, now that I added it as a dependency, that of the
WorkManager library. And since there was now a content provider in the merged manifest, the system will automatically create and run it when my app starts.
Okay, so now I knew how I caused the library to load and run that content provider. But what impact did that have?
I posted an article recently, Testing App Startup Performance, detailing how to measure an application’s startup time. I used that approach to measure my application’s launch time before and after adding the
WorkManager dependency and found that
WorkManager added an average of 67ms to my app’s startup time.
Note, as described in the startup testing article, that I locked my Pixel 2 clocks, so this is probably a longer duration than a real user would see on that device… but maybe not as long as users on lower-end devices might experience. Also note (as also described in that article) that I probably didn’t need to lock the clocks, since the system generally runs the clock at max frequency during app startup. But locking clocks is generally a good practice in performance testing for getting consistent results. Also, locking the clocks tends to produce longer durations (from slower frequencies), which is helpful in reducing noise in the data for short durations.
Note also that this duration is not due just to the content provider itself. Content providers can take a measurable amount of time to be created, but closer to 1–2ms than the 67ms I was seeing. Instead, this is the total time that the library takes to load and initialize, in addition to creating and running the content provider to perform that work of actually initializing the library.
So it looks like simply adding the library to my project caused a startup hit of nearly 70 milliseconds. In a real app, I would probably use multiple libraries, many with their own content providers running at launch time, causing even more delays at startup while everything got initialized.
The question, of course, is whether I could do anything to mitigate that hit. And the answer, of course, is… come back for Part 2! (Spoiler: in Part 2, I’ll discuss how to use the AndroidX App Startup library to load libraries lazily).