Jetpack App Startup: A Deep Dive and Some Concerns

The Reddit thread for my blog post on Exploiting Content Providers had an interesting comment:

Almost four weeks later, the AndroidX Startup library was announced. Following up on my promise, let’s take a closer look at it.

The Library

Quoting the docs, here’s what App Startup is about:

The App Startup library provides a straightforward, performant way to initialize components at application startup. Both library developers and app developers can use App Startup to streamline startup sequences and explicitly set the order of initialization.

It promises a few things:

  • A simple API which can be used by both libraries and applications
  • Performant initialization of components to improve app startup times
  • Ability to explicitly request a particular order of initialization

I wanted to try it out in my own project WhatTheStack, so I decided to take a look at the source code and share my thoughts on it.

The Idea

A lot of libraries use empty Content Providers to run initialization logic automatically at application startup. As the number of libraries using this trick increases, so does the number of empty content providers. Apparently a large number of them affects startup time significantly, as illustrated by a chart in this video:

Cost of an empty Content Provider

The idea of the App Startup library is to reduce the number of such content providers by replacing them with just the one supplied by it.

Usage

App Startup relies on consumers registering the components they want to be initialized at startup. Let’s use the canonical example of a custom logger.

To register a component, create an initializer for it by implementing the Initializer<T> interface, and add a corresponding Manifest entry:

1
2
3
4
5
6
7
8
9
// LoggerInitializer.kt
class LoggerInitializer : Initializer<MyLogger> {
  override fun create(context: Context): MyLogger {
    MyLogger.init(context)
    return MyLogger.getInstance()
  }

  override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
  • The create method must initialize the component being registered and return an instance of it.
  • The dependencies method must return a list of Initializer classes which need to run before the create method of this initializer. This is how you specify the dependencies of the current component to make sure that they are available beforehand.
1
2
3
4
5
6
7
8
9
// AndroidManifest.xml
<provider
  android:name="androidx.startup.InitializationProvider"
  android:authorities="${applicationId}.androidx-startup"
  android:exported="false"
  tools:node="merge">
  <meta-data  android:name="com.example.LoggerInitializer"
    android:value="androidx.startup" />
</provider>
  • <provider> registers the content provider supplied by App Startup, which is called “InitializationProvider”.

  • <meta-data> of the provider registers the initializer we created above.

  • Each initializer must go in a separate metadata tag and contain "androidx.startup" in the android:value attribute. This is important because App Startup uses this string to filter out metadata tags which do not contain initializers.

With this setup complete, App Startup will discover the LoggerInitializer class and initialize it automatically at application startup.

Lazy Initialization

Components which are discovered through metadata tags are initialized automatically. To disable this for a particular component, add the tools:node="remove" attribute to the <meta-data> tag for it.

1
2
3
4
5
6
7
<provider
  android:name="androidx.startup.InitializationProvider"
  ...>
  <meta-data  android:name="com.example.LoggerInitializer"
    android:value="androidx.startup"
    tools:node="remove" />
</provider>

The component can then be lazily initialized using the AppInitializer singleton:

1
2
3
AppInitializer
  .getInstance(context)
  .initializeComponent(MyLogger::class.java)

To understand why this works, and how App Startup discovers initializers automatically, let’s take a look under the hood.

Under the hood

The source code for App Startup is available here. This post is based on the first public release (1.0.0-alpha01) of the library. There might be major changes as it matures. This snapshot represents what the code looks like at the time of writing.

Note
Parts of the code related to tracing, checking for dependency cycles, and handling exceptions have been omitted in the interest of brevity.

App Startup makes use of the Manifest Merger feature of Android’s build tooling. All declarations of the InitializationProvider in manifests of dependencies are merged into a single <provider> entry in the final manifest of our application. The final <provider> contains <meta-data> tags of all merged entries. On app startup, the OS sees this content provider in the manifest and therefore creates it automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java;l=42;drc=2f9dd0f2fe4642438ed3f657dc7f901241a16ca8

public final class InitializationProvider extends ContentProvider {
  @Override
  public boolean onCreate() {
    Context context = getContext();
    if (context != null) {
      AppInitializer.getInstance(context).discoverAndInitialize();
    } else {
      throw new StartupException("Context cannot be null");
    }
  return true;
  }
}

InitializationProvider invokes AppInitializer upon creation, which then accesses the merged <meta-data> tags to find registered initializers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java;l=160;drc=564aa855602e82a962ee9b2ca6dff38eaa2ee15e

public final class AppInitializer {
  void discoverAndInitialize() {
    ComponentName provider = new ComponentName(
      mContext.getPackageName(),
      InitializationProvider.class.getName()
    );
    ProviderInfo providerInfo = mContext.getPackageManager()
      .getProviderInfo(provider, GET_META_DATA);
    Bundle metadata = providerInfo.metaData;
    if (metadata != null) {
      // Iterate over all metadata elements
    }
}

The metadata bundle is like a Map containing merged tags as key-value pairs. In each pair, the key (declared with android:name) contains the fully qualified class name of an initializer, and value (android:value) contains the string "androidx.startup". Refer to the example shown in the Usage section to see how this is declared.

In each iteration, the key is used to instantiate the Initializer class using Reflection.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java;l=160;drc=564aa855602e82a962ee9b2ca6dff38eaa2ee15e

void discoverAndInitialize() {
  ...
  Bundle metadata = providerInfo.metaData;
  if (metadata != null) {
    Set<String> keys = metadata.keySet();
    for (String key : keys) {
      String value = metadata.getString(key, null);
      Class<?> clazz = Class.forName(key);
      if (Initializer.class.isAssignableFrom(clazz)) {
        Class<? extends Initializer<?>> component =
                (Class<? extends Initializer<?>>) clazz;
        doInitialize(component, initializing);
      }
    }
  }
}

The doInitialize method contains the bulk of the code related to component instantiation and initializing its dependencies. The dependencies are initialized before the component itself, repeating this process recursively for each dependency.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:startup/startup-runtime/src/main/java/androidx/startup/AppInitializer.java;l=105;drc=564aa855602e82a962ee9b2ca6dff38eaa2ee15e

<T> T doInitialize(
  Class<? extends Initializer<?>> component,
  Set<Class<?>> initializing
) {
  Object result;
  if (!mInitialized.containsKey(component)) {
    Object instance = component.getDeclaredConstructor().newInstance();
    Initializer<?> initializer = (Initializer<?>) instance;
    List<Class<? extends Initializer<?>>> dependencies = initializer.dependencies();
    if (!dependencies.isEmpty()) {
      for (Class<? extends Initializer<?>> clazz : dependencies) {
        if (!mInitialized.containsKey(clazz)) {
          doInitialize(clazz, initializing);
        }
      }
    }
    result = initializer.create(mContext);
  } else {
    result = mInitialized.get(component);
  }
  return (T) result;
}

After the component is initialized it is stored in a Map in AppInitializer.

Manifest Merger also explains why we need to add tools:node="remove" for lazy initialization to work. A metadata tag containing this attribute is not merged into the final content provider entry of the Manifest, and therefore its initializer is not discovered at startup.

In my opinion, using manifest merger this way is a neat trick. I wonder how many folks outside Google understand the Android build process well enough to come up with unique solutions like this.

Some Concerns

I like App Startup so far, but I have a few concerns about it in its current state:

  • Performance benefit of using App Startup depends on libraries rolling out support for it. If a library decides against supporting it, creating initializers for it on your own is futile because doing so will not prevent its content providers from running at startup. You might be able to manually disable them, though.
  • Initializers are required to have a no-arg constructor because they need to be instantiated through reflection. This is a drawback if you want to wire them with constructor injection.
  • The create method of initializers is required to return an instance of the component being initialized. This does not make sense for libraries which are never meant to interacted with in code (such as WhatTheStack) as there is no “instance” to return.
  • Someone will misuse the AppInitializer singleton as a service locator, and that’s not what its meant for.

With the negatives out of the way, there is also a lot to like here as well. I am happy to see that libraries do not need to declare App Startup as an api dependency in their build config. This means that its usage is opaque to applications consuming those libraries. I also like that now there is a standard way to write initialization code for libraries that run at startup.

Well, not “standard” yet. We will have to wait and see if App Startup takes off.


If you would like to see an example of migrating your app/library to use App Startup instead of Content Providers, check out this PR. While you are there, consider checking out some of my other projects on my GitHub.


Huge thanks to Subrajyoti Sen and Anubhav Gupta for helping me with this post.

Question, comments or feedback? Feel free to reach out to me on Twitter @haroldadmin