preface

StartUp, the application StartUp library, provides a simple, efficient way to initialize components at application StartUp. Both library and application developers can use App Startup to simplify the Startup sequence and explicitly set the initialization order.

Instead of defining separate content providers for each component that needs to be initialized, App Startup allows you to define component initializers that share a single content provider. This can significantly improve application startup time.

The main pain points of this component are to simplify the initialization of various external references to the SDK and optimize the timing of SDK initialization to initialize the SDK on demand in a better and more stable manner while ensuring the startup speed (of course, it is also possible to use its on-demand execution functionality to initialize certain classes).

【 this series demonstration cases are stored in making the repository in ArchitecturalComponentExample 】

How to use

Introduction of depend on

Add the following to the dependencies of the app/build.gradle file:

implementation "Androidx. Startup: startup - runtime: 1.0.0"
Copy the code

If there is an error in obtaining the dependency package, please check whether the following code is contained in the project directory build.gradle:

allprojects {
    repositories {
        google()
        jcenter()
    }
}
Copy the code

Sample project structure description

Java version address of this project

Kotlin version address of this project

Refer to the official development documentation for the best practices

The main structure of this project example is as follows:

Part ② in the figure represents the initializer used to initialize the SDKS of each component.

Part ① in the figure represents the simulated three-party SDK that needs to be initialized. There is a “directed acyclic” interdependence between the SDKS, which is as follows:

The dependencies expressed in the above figure are:

  • The Cache relies on DatabaseProxy and Logger
  • DatabaseProxy relies on DatabaseHelper and Logger
  • DatabaseHelper and TXMap both rely on Logger

Create an initializer

Define each component initializer by creating a class that implements the initializer interface. This interface defines two important methods:

  • The create() method contains all the actions needed to initialize the component and returns an instance of T.
  • The dependencies() method, which returns a list of other initializer objects on which the initializer depends. You can use this method to control the order in which your application runs its initializers at startup.

Following these rules, we write LoggerInitializer for Logger

public class LoggerInitializer implements Initializer<Logger> {
    
    @NonNull
    @Override
    public Logger create(@NonNull Context context) {
        Log.i("loglog"."create: LoggerInitializer");
        Logger.initialize();
        return Logger.getInstance();
    }

    @NonNull
    @Override
    publicList<Class<? extends Initializer<? >>> dependencies() {// Indicates that no other dependencies need to be initialized
        returnCollections.emptyList(); }}Copy the code
  • First the initializer needs to be implementedInitializer<? >Interface, passing in the generic type of the object to be initialized,Think about what happens if Void is not passed or passed?
  • Implement the create method, which completes component initialization and returns instances if necessary
  • Implement the Dependencies method, which declares that the currently initialized component depends on other components and returns the initializer of the dependent component. This Logger does not depend on other components, so it returns an empty array.What if you return null?

Similarly, we define component initializers for each component based on the dependency of the component. Compare the initializers for multiple dependent DatabaseProxy initializers.

public class DatabaseProxyInitializer implements Initializer<DatabaseProxy> {
    @NonNull
    @Override
    public DatabaseProxy create(@NonNull Context context) {
        Log.i("loglog"."create: DatabaseProxyInitializer");
        DatabaseProxy.initialize(DatabaseHelper.getInstance());
        return DatabaseProxy.getInstance();
    }

    @NonNull
    @Override
    publicList<Class<? extends Initializer<? >>> dependencies() {returnArrays.asList(LoggerInitializer.class, DatabaseHelperInitializer.class); }}Copy the code

We see in Dependencies that DatabaseProxy relies on Logger and DatabaseHelper.

Next, let’s try two of the above thought questions.

  1. When, Initializer
    what happens when a generic type passes Void?

This scenario is similar to when we simply want to perform an initialization of a component without returning an instance

public class SomethingInitializer implements Initializer<Void> {

    @NonNull
    @Override
    public Void create(@NonNull Context context) {
        Log.i("loglog"."create: SomethingInitializer");
        return null;
    }

    @NonNull
    @Override
    publicList<Class<? extends Initializer<? >>> dependencies() {returnCollections.emptyList(); }}Copy the code

At this point, we make TXMap’s initializer depend on this SomethingInitializer:

public class MapInitializer implements Initializer<TXMap> {
    @NonNull
    @Override
    public TXMap create(@NonNull Context context) {
        Log.i("loglog"."create: MapInitializer");
        return new TXMap();
    }

    @NonNull
    @Override
    publicList<Class<? extends Initializer<? >>> dependencies() {// Map depends on log, now we make it depend on Something
        returnArrays.asList(LoggerInitializer.class, SomethingInitializer.class); }}Copy the code

Let’s take a look at the result (of course, the code for just writing the initializer still doesn’t work, and we haven’t declared the initialization component in AndroidManifest yet, so we’ll focus on the result here, and declare the initialization component in the next section) :

I/tag: create: LoggerInitializer
I/tag: Logger initialized
I/tag: create: SomethingInitializer
I/tag: create: MapInitializer
I/tag: Map initialized
Copy the code

As we can see, it still performs initialization operations normally as required. It first calls the Logger which is dependent on TXMap and the initializer of Something, and finally completes the initialization of Map. That, Initializer <? > pass generic and pass Void generic, which can be executed normally, normally applicable to only perform initialization operation, no return instance situation.

  1. What happens if dependencies returns a null value?

We changed the LoggerInitializer dependency from return collections.emptyList () to return NULL and then executed. We will focus only on the results here, and declare the initialization component in the next section) :

/ / an error log Java. Lang. RuntimeException: Unable to get the provider androidx. Startup. InitializationProvider: androidx.startup.StartupException: androidx.startup.StartupException: java.lang.NullPointerException: Attempt to invoke interface method 'boolean java.util.List.isEmpty()' on a null object referenceCopy the code

We found that the program reported an error. Therefore, the return value of dependencies should not be null. As for the reason, it will be explained in the principles section.

Register the initializer provider

As mentioned above, writing an initializer alone does not accomplish component initialization, and obviously we did not configure the timing of its execution.

Application startup includes a special content provider called InitializationProvider, which is used to discover and invoke your component initializer. The application starts by first checking the items under the InitializationProvider manifest to discover the component initializer. App Startup then calls the Dependencies () method for any initializers it has found. This means that in order for the component initializer to be discovered when the application starts, one of the following conditions must be met:

  • The component initializer has a corresponding entry under the InitializationProvider manifest.
  • Component initializers are listed in the Dependencies () method and come from an already-found initializer.

In this case, to ensure that these initializers are found when the application starts, add the following to the manifest file:

<provider android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup"  android:exported="false" tools:node="merge"> <! -- This entry makes ExampleLoggerInitializer discoverable. --> <meta-data android:name="com.tinlone.startupexamplejava.initializers.CacheInitializer" android:value="androidx.startup" /> <meta-data android:name="com.tinlone.startupexamplejava.initializers.MapInitializer" android:value="androidx.startup" />  </provider>Copy the code

The code above passes the initializer to the initialization provider.

If there are multiple initialization chains, such as the one in this example:

  • Logger -> TXMap
  • Logger -> DatabaseHelper -> DatabaseProxy

These two separate chains are passed to the InitializationProvider as the tail initializer for each separate chain. Why? We will also explore this in detail in the source code section, keeping this in mind.

Manually initialize components

Of course, if you don’t want the InitializationProvider to automatically initialize some components, you can also call the initialization process manually. You should do this:

  • Mark components that do not need to be initialized asremove:
<meta-data android:name="com.example.ExampleLoggerInitializer"
          tools:node="remove" />
Copy the code
  • Call the following code when initialization is required, using Logger as an example:
AppInitializer.getInstance(context)
    .initializeComponent(LoggerInitializer.class);
Copy the code

Instead of simply deleting the entry, you use the tool :node=”remove” in the entry to ensure that the merge tool also removes the entry from all other merged manifest files.

Note: Disabling automatic initialization of a component also disables automatic initialization of that component’s dependencies.

If you want to completely disable automatic component initialization, you can declare the InitializationProvider component as remove:

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

And calls the following code when initialization is needed:

AppInitializer.getInstance(context)
    .initializeComponent(ExampleLoggerInitializer.class);
Copy the code

perform

The first two steps are required to complete the StartUp process.

I/loglog: create: LoggerInitializer
I/loglog: Logger initialized
I/loglog: create: SomethingInitializer
I/loglog: create: MapInitializer
I/loglog: Map initialized
I/loglog: create: DatabaseHelperInitializer
I/loglog: DatabaseHelper initialized
I/loglog: create: DatabaseProxyInitializer
I/loglog: DatabaseHelper initialized
I/loglog: DatabaseProxy initialized
I/loglog: create: CacheInitializer
I/loglog: Cache initialized
Copy the code

In AndroidManifest, we declare CacheInitializer and MapInitializer for the end of the chain. However, this initializer is executed from the head of the dependency chain. Let’s take a quick look at how this works.

Source code analysis

Here are some of the foreshadows and questions we’ve raised in the usage section above:

  • Why not return null for Dependencies?
  • Why pass the tail initializer for each individual chain to the InitializationProvider?
  • How does he execute exactly in the chain of dependencies order?

To answer these questions, let’s start with the initialized entry class InitializationProvider.

InitializationProvider

@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class InitializationProvider extends ContentProvider {
    @Override
    public boolean onCreate(a) {
        Context context = getContext();
        if(context ! =null) {
            AppInitializer.getInstance(context).discoverAndInitialize();
        } else {
            throw new StartupException("Context cannot be null");
        }
        return true;
    }
    // something else ...
}
Copy the code

The InitializationProvider content is relatively simple, As a content provider when the initialization calls the AppInitializer. GetInstance (context). DiscoverAndInitialize () to scan the initializer configuration and perform follow-up operations.

AppInitializer

AppInitializer maintains a singleton structure containing two important methods discoverAndInitialize and doInitialize, which are responsible for discovering and executing initializers, respectively.

discoverAndInitialize()
void discoverAndInitialize(a) {
    try {
        Trace.beginSection(SECTION_NAME);
        // Create a component identifier to get the InitializationProvider information
        ComponentName provider = new ComponentName(mContext.getPackageName(),
                InitializationProvider.class.getName());
        // Get the component from the package manager based on the component identifier and get the component metadata
        ProviderInfo providerInfo = mContext.getPackageManager()
                .getProviderInfo(provider, GET_META_DATA);
        // get metadata
        Bundle metadata = providerInfo.metaData;
        String startup = mContext.getString(R.string.androidx_startup);
        if(metadata ! =null) { Set<Class<? >> initializing =new HashSet<>();
            {@link Bundle#keySet}
            Set<String> keys = metadata.keySet();
            for (String key : keys) {
                String value = metadata.getString(key, null);
                // Check whether it is androidx_startup metadata
                if (startup.equals(value)) {
                    // Generate bytecode objects based on the full class name in the metadata keyClass<? > clazz = Class.forName(key);// Verify that the bytecode object is an implementation class of Initializer
                    if(Initializer.class.isAssignableFrom(clazz)) { Class<? extends Initializer<? >> component = (Class<? extends Initializer<? >>) clazz;// add to Set
                        mDiscovered.add(component);
                        if (StartupLogger.DEBUG) {
                            StartupLogger.i(String.format("Discovered %s", key));
                        }
                        // Perform initialization
                        doInitialize(component, initializing);
                    }
                }
            }
        }
    } catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
        throw new StartupException(exception);
    } finally{ Trace.endSection(); }}Copy the code

From the code, it mainly does the following:

  • Create a component identifier to get the InitializationProvider information
  • Get the component from the package manager based on the component identifier and get the component metadata
  • Retrieve metadata and retrieve the set of keys in metadata
  • Traverses the data in the metadata set (ArrayMap) and verifies whether it is androidX_STARTUP metadata
  • Generate a bytecode object based on the full class name in the metadata key and verify that the bytecode object is an implementation class of Initializer
  • Add Initializer’s implementation class bytecode object to the variable mDiscovered Set
  • Perform initialization
doInitialize()
<T> T doInitialize( @NonNull Class<? extends Initializer<? >> component, @NonNull Set<Class<? >> initializing) { synchronized (sLock) { boolean isTracingEnabled = Trace.isEnabled(); Try {if (isTracingEnabled) {// Use simpleName here because otherwise the section name would be too large. Trace.beginSection(component.getSimpleName()); } // Initializing is a set of execution records; // Initializing is a set of execution records; // Initializing is a set of execution records; // Initializing is a set of execution records; If the initializer repeats at this time, the interdependence is looped, Error if (initializing. Contains (Component)) {String message = string. format("Cannot initialize % S. Cycle detected.", component.getName() ); throw new IllegalStateException(message); } Object result; // skip if (! MInitialized. ContainsKey (Component)) {// Add the initialization bytecode object to the execution log; Try {/ / for instance initializer Object instance = component. GetDeclaredConstructor (). The newInstance (); Initializer<? > initializer = (Initializer<? >) instance; // Get initializer dependencies List<Class<? extends Initializer<? >>> dependencies = initializer.dependencies(); < span style = "box-sizing: border-box! Important; word-wrap: break-word! Important;" If (! = null) {// If (! = null) { Dependencies. IsEmpty ()) {// the dependencies are iterated over, their initializer is executed, and the initialization record is passed to prevent dependencies from being looped for (Class<? extends Initializer<? >> clazz : dependencies) { if (! mInitialized.containsKey(clazz)) { doInitialize(clazz, initializing); } } } if (StartupLogger.DEBUG) { StartupLogger.i(String.format("Initializing %s", component.getName())); } result = initializer.create(mContext); if (StartupLogger.DEBUG) { StartupLogger.i(String.format("Initialized %s", component.getName())); } // Remove initializing. Remove (component); // Record the initialization completed mInitialized. Put (component, result); } catch (Throwable throwable) { throw new StartupException(throwable); } } else { result = mInitialized.get(component); } return (T) result; } finally { Trace.endSection(); }}}Copy the code

From the code, it mainly does the following:

  • First, according to the single chain execution record, determine whether there is a dependency loop in the chain, which will throw errors
  • Determine whether a dependency has been initialized based on the record of the completion of the initialization. If it has been initialized, skip it
  • Initializer bytecode objects that need to be executed are added to the execution record for standby use to determine whether the dependency chain is looped
  • Call initializer.dependencies() to get the initializer dependencies
  • If the initializer has other dependencies, the recursive call doInitialize() traverses the dependencies to initialize the header of the dependency chain
  • When the chain execution is completed, the execution record of the chain is removed
  • After the chain is executed, the initializer declared in the metadata is added to the completed collection to avoid repeated initialization

So at this point, we seem to be able to explain the three foreshadowing questions left at the beginning of this section.

  1. Dependencies () returns null.

A: If list.isempty () returns null for dependencies(), the value of list.isempty is null for dependencies(). Causes a call to null.isEmpty(), which causes the null pointer to be abnormal.

  1. Why pass the tail initializer for each individual chain to the InitializationProvider?

A: We can find all initializers of the chain by iterating over the recursion dependencies(), and then fold the recursion from beginning to end, ensuring that the dependencies are executed in sequence

  1. How does he execute exactly in the chain of dependencies order?

Answer the same question as Question 2