Google Jetpack is one of the most fundamental architectural components in Android Developer today. Since its launch, Google Jetpack has dramatically changed our development model and made development easier. It also requires a certain level of understanding of how some of the sub-components are implemented. So I’m going to write a series of articles on Jetpack source parsing that I hope will be helpful to you 😇😇

Official account: byte array

Article Series Navigation

  • Jetpack (1) -Lifecycle
  • Jetpack (2) – Lifecycle derivatives
  • Look at Jetpack (3) from the source code – LiveData source details
  • Look at Jetpack (4) from the source code – LiveData derivatives source detailed explanation
  • Jetpack (5) -startup
  • Jetpack (6) – ViewModel
  • Jetpack (7) – SavedStateHandle

Recently, a new component called Startup has been added to the Google Jetpack website. Startup provides a straightforward and efficient way to initialize multiple components during application Startup. Developers can rely on it to explicitly set the initialization order of multiple components and optimize Startup time

This article is based on the latest alpha version of Startup:

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

The meaning of Startup

Startup allows Library developers and App developers to share the same ContentProvider to complete their own initialization logic. It also allows you to set the initialization sequence between components, avoiding the need to define a separate ContentProvider for each component that needs to be initialized. This greatly reduces the startup time of the application

Currently, many third-party dependent libraries, in order to simplify the cost to consumers, choose to obtain the Context object by declaring a ContentProvider and automatically complete the initialization process. Such as Lifecycle components will declare a ProcessLifecycleOwnerInitializer used to get the context object and completes the initialization. Each ContentProvider declared in the AndroidManifest file is preexecuted and calls the internal onCreate() method before the Application’s onCreate() method is called. Every time an application builds and executes a ContentProvider, it costs memory and time. If the application has too many ContentProviders, it will increase the startup time of the application

Therefore, Startup exists to provide a unified entry point for many dependencies (both application components and third-party components) once it is released and adopted by most of the third-party dependencies

How to use it

Suppose we have three libraries that need to be initialized in our project. If Library A depends on Library B, Library B depends on Library C, and Library C does not need other dependencies, you can create three Initializer classes for the three libraries

Initializer is an interface provided by Startup for specifying initialization logic and sequence. Context: Context method completes the initialization process and returns the result value. In dependencies(), specify other initializers that must be initialized before initializing this Initializer

	class InitializerA : Initializer<A> {

        // Initialize the component here and return the initialization value
        override fun create(context: Context): A {
            return A.init(context)
        }
		
        // Obtain the Initializer list that needs to be initialized before initializing itself
        // If you don't need to rely on other components, you can return an empty list
        override fun dependencies(a): List<Class<out Initializer<*>>> {
            return listOf(InitializerB::class.java)
        }

    }

    class InitializerB : Initializer<B> {

        override fun create(context: Context): B {
            return B.init(context)
        }

        override fun dependencies(a): List<Class<out Initializer<*>>> {
            return listOf(InitializerC::class.java)
        }

    }

    class InitializerC : Initializer<C> {

        override fun create(context: Context): C {
            return C.init(context)
        }

        override fun dependencies(a): List<Class<out Initializer<*>>> {
            return listOf()
        }

    }
Copy the code

Startup provides two initialization methods: automatic initialization and manual initialization (delayed initialization).

1. Automatic initialization

Declare the InitializationProvider provided by Startup in the AndroidManifest file, and use the meta-data tag to declare the package name path of the Initializer implementation class. Value must be androidx.startup. In this case, we only need to declare InitializerA, because InitializerB and InitializerC can be located chained by the return value of InitializerA’s dependencies() method

        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="leavesc.lifecyclecore.core.InitializerA"
                android:value="androidx.startup" />
        </provider>
Copy the code

Once the above steps are complete, Startup will be automatically initialized in the sequence specified by us when the application starts. Note that all initializationproviders must explicitly declare each Initializer if there is no dependency on each other and each InitializationProvider wants to automatically initialize each Initializer. In addition, the initialization order of Initializer is the same as that declared on the Provider

2. Manually initialize

In most cases of automatic initialization method can meet our requirements, but is not applicable in some cases, for example: the component initialization cost high (performance cost or time consumption) and the component may not be used to finally, at this point it can be instead of when to use to initialize it again, the lazy loading components

The manually initialized Initializer does not need to be declared in the AndroidManifest. You only need to call the following methods to initialize the Initializer

val result = AppInitializer.getInstance(this).initializeComponent(InitializerA::class.java)
Copy the code

The initialization result value of Initializer is cached inside Startup. Therefore, repeated calls to initializeComponent do not result in multiple initialization. This method can also be used to obtain the initialization result value during automatic initialization

If all initializers in the app do not need to be automatically initialized, you can also not declare the InitializationProvider in the AndroidManifest

3. Matters needing attention

1. Remove the Initializer

Let’s say one of the third-party dependencies we introduced in our project uses Startup itself for automatic initialization, and we want to change it to lazy loading, but we can’t directly modify the AndroidManifest file of the third-party dependencies. You can remove the specified Initializer using the AndroidManifest merge rules

InitializerImpl.InitializerImpl is declared in the AndroidManifest file of the main project project. Add tools:node=”remove” statement to remove itself when merging the AndroidManifest file, so Startup does not automatically initialize InitializerImpl

        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="leavesc.lifecyclecore.mylibrary.TestIn"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>
Copy the code

2. Disable automatic initialization

If you want to disable all automatic initialization logic for a Startup, but do not want to do so by directly removing the provider declaration, you can do so using the method described above

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

Lint check

Startup contains a set of Lint rules that you can use to check that a component’s initializer has been correctly defined. You can do this by running./gradlew :app:lintDebug

For example, if the project InitializerB is not declared in the AndroidManifest and is not included in the dependency lists of other Initializers, Lint will give you the following warning:

Errors found:

xxxx\leavesc\lifecyclecore\core\InitializerHodler.kt:52: Error: Every Initializer needs to be accompanied by a corresponding <meta-data> entry in the AndroidManifest.xml file. [Ensur
eInitializerMetadata]
  class InitializerB : Initializer<B>{^Copy the code

Four, source code analysis

Startup The entire dependency library consists of only five Java files. The overall logic is relatively simple

1, StartupLogger

StartupLogger is a logging utility class that outputs logs to the console

public final class StartupLogger {

    private StartupLogger(a) {
        // Does nothing.
    }

    /
     * The log tag.
     */
    private static final String TAG = "StartupLogger";

    /
     * To enable logging set this to true. * /static final boolean DEBUG = false;

    /
     * Info level logging.
     *
     * @param message The message being logged
     */
    public static void i(@NonNull String message) {
        Log.i(TAG, message);
    }

    /
     * Error level logging
     *
     * @param message   The message being logged
     * @param throwable The optional {@link Throwable} exception
     */
    public static void e(@NonNull String message, @Nullable Throwable throwable) { Log.e(TAG, message, throwable); }}Copy the code

2, StartupException

StartupException is a custom RuntimeException subclass. When Startup encounters an unexpected situation during initialization (for example, the Initializer is cyclally dependent or the Initializer fails to reflect), A StartupException is thrown

public final class StartupException extends RuntimeException {
    public StartupException(@NonNull String message) {
        super(message);
    }

    public StartupException(@NonNull Throwable throwable) {
        super(throwable);
    }

    public StartupException(@NonNull String message, @NonNull Throwable throwable) {
        super(message, throwable); }}Copy the code

3,, Initializer

Initiaizer is an interface provided by Startup to declare the initialization logic and order. Initiaizer is an interface provided by create(context: Context method completes the initialization process and returns the result value. In dependencies(), specify other initializers that must be initialized before initializing this Initializer

public interface Initializer<T> {

    /
     * Initializes and a component given the application {@link Context}
     * 
     * @param context The application context.
     */
    @NonNull
    T create(@NonNull Context context);

    /
     * @return A list of dependencies that this {@link Initializer} depends on. This is
     * used to determine initialization order of {@link Initializer}s.
     * <br/>
     * For e.g. if a {@link Initializer} `B` defines another
     * {@link Initializer} `A` as its dependency, then `A` gets initialized before `B`.
     */
    @NonNullList<Class<? extends Initializer<? >>> dependencies(); }Copy the code

4, InitializationProvider

InitializationProvider is we need to actively declared in AndroidManifest file ContentProvider, Startup the initialization logic are here to trigger

Since the purpose of the InitializationProvider is to unify the initialization entries for multiple dependencies and get the Context object, there is no point in doing anything other than onCreate(), which is automatically called by the system. If the developer calls these methods, the exception will be thrown directly

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;
    }

    @Nullable
    @Override
    public Cursor query(
            @NonNull Uri uri,
            @Nullable String[] projection,
            @Nullable String selection,
            @Nullable String[] selectionArgs,
            @Nullable String sortOrder) {
        throw new IllegalStateException("Not allowed.");
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        throw new IllegalStateException("Not allowed.");
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public int delete(
            @NonNull Uri uri,
            @Nullable String selection,
            @Nullable String[] selectionArgs) {
        throw new IllegalStateException("Not allowed.");
    }

    @Override
    public int update(
            @NonNull Uri uri,
            @Nullable ContentValues values,
            @Nullable String selection,
            @Nullable String[] selectionArgs) {
        throw new IllegalStateException("Not allowed."); }}Copy the code

5, AppInitializer

AppInitializer is the core focus of the Startup library. The total code amount is less than 200 lines. The overall process of AppInitializer is as follows:

  • The InitializationProvider passes in the Context object to obtain the unique instance of AppInitializer and calls itdiscoverAndInitialize()Method to complete all the automatic initialization logic
  • discoverAndInitialize()The InitializationProvider () method parses the InitializationProvider, obtains all metadata, and then reflects and builds Initializer objects pointed to by each metadata in the declaration order
  • Before Initializer is initialized, the system checks whether dependencies are empty. If it is empty, it is called directlycreate(Context)Method is initialized. If it is not empty, initialize dependencies first. Repeat this traversal for each dependency until Initializer without Dependencies is initialized first. This ensures the order of initializers
  • Startup raises exceptions when the Initializer implementation class does not contain a no-parameter constructor, there is a cyclic dependency between initializers, and initializers are initialized.create(Context)Method) throws an exception

AppInitializer provides the getInstance(@nonnull Context Context) method for obtaining a unique static instance

public final class AppInitializer {/ * Unique static instance * The {@link AppInitializer} instance.
     */
    private staticAppInitializer sInstance; / * Synchronization lock * Guards app initialization. */private static final Object sLock = new Object();

    // Stores all initializers that have been initialized and their initialization results
    @NonNull
    finalMap<Class<? >, Object> mInitialized;@NonNull
    final Context mContext;

    /
     * Creates an instance of {@link AppInitializer}
     *
     * @param context The application context
     */
    AppInitializer(@NonNull Context context) {
        mContext = context.getApplicationContext();
        mInitialized = new HashMap<>();
    }

    /
     * @param context The Application {@link Context}
     * @return The instance of {@link AppInitializer} after initialization.
     */
    @NonNull
    @SuppressWarnings("UnusedReturnValue")
    public static AppInitializer getInstance(@NonNull Context context) {
        synchronized (sLock) {
            if (sInstance == null) {
                sInstance = new AppInitializer(context);
            }
            returnsInstance; ...}}}Copy the code

The discoverAndInitialize() method is called by the InitializationProvider, which triggers the initialization of all dependencies that need to be initialized by default

	@SuppressWarnings("unchecked")
    void discoverAndInitialize(a) {
        try {
            Trace.beginSection(SECTION_NAME);
            
            // Obtain all metadata contained by the InitializationProvider
            ComponentName provider = new ComponentName(mContext.getPackageName(),
                    InitializationProvider.class.getName());
            ProviderInfo providerInfo = mContext.getPackageManager()
                    .getProviderInfo(provider, GET_META_DATA);
            Bundle metadata = providerInfo.metaData;
            
            // Get the string androidx.startup
            // Because Startup traverses the metaData using the string as a fixed value
            // So if different values are declared in the AndroidManifest file, they will not be initialized
            String startup = mContext.getString(R.string.androidx_startup);
            
            if(metadata ! =null) {
                // Indicates the Initializer that is being initialized
                // Is used to determine whether circular dependencies existSet<Class<? >> initializing =new HashSet<>();
                Set<String> keys = metadata.keySet();
                for (String key : keys) {
                    String value = metadata.getString(key, null);
                    if(startup.equals(value)) { Class<? > clazz = Class.forName(key);// Ensure that the metaData declared package name path points to the Initializer implementation class
                        if(Initializer.class.isAssignableFrom(clazz)) { Class<? extends Initializer<? >> component = (Class<? extends Initializer<? >>) clazz;if (StartupLogger.DEBUG) {
                                StartupLogger.i(String.format("Discovered %s", key));
                            }
                            // Perform the actual initialization process
                            doInitialize(component, initializing);
                        }
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
            throw new StartupException(exception);
        } finally{ Trace.endSection(); }}Copy the code

The doInitialize() method is where the create(Context: context) of the Initializer is actually called. The main logic of the doInitialize() method is to complete the initialization of all dependencies through nested calls. An exception will be thrown when circular dependencies are detected

	@NonNull
    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
    <T> T doInitialize(
            @NonNullClass<? extends Initializer<? >> component,@NonNullSet<Class<? >> initializing) {
        synchronized (sLock) {
            boolean isTracingEnabled = Trace.isEnabled();
            try {
                if (isTracingEnabled) {
                    // Use the simpleName here because section names would get too big otherwise.
                    Trace.beginSection(component.getSimpleName());
                }
                if (initializing.contains(component)) {
                    // Initializing contains component, indicating cyclic dependence between Initializers
                    // Throw an exception directly
                    String message = String.format(
                            "Cannot initialize %s. Cycle detected.", component.getName()
                    );
                    throw new IllegalStateException(message);
                }
                Object result;
                if(! mInitialized.containsKey(component)) {// If mInitialized does not include component
                    // The Initializer pointed by component has not been initialized
                    initializing.add(component);
                    try {
                        // Use reflection to invoke component's parameterless constructor and initializeObject instance = component.getDeclaredConstructor().newInstance(); Initializer<? > initializer = (Initializer<? >) instance;// Obtain the dependencies of InitializerList<Class<? extends Initializer<? >>> dependencies = initializer.dependencies();// If the dependencies of Initializer are not empty, run the following command
                        // Initialize dependencies by traversing each item
                        if(! dependencies.isEmpty()) {for(Class<? extends Initializer<? >> clazz : dependencies) {if(! mInitialized.containsKey(clazz)) { doInitialize(clazz, initializing); }}}if (StartupLogger.DEBUG) {
                            StartupLogger.i(String.format("Initializing %s", component.getName()));
                        }
                        // Initialize
                        result = initializer.create(mContext);
                        if (StartupLogger.DEBUG) {
                            StartupLogger.i(String.format("Initialized %s", component.getName()));
                        }
                        // Remove the already initialized component from initializing
                        // Avoid misjudging circular dependencies
                        initializing.remove(component);
                        // Save the initialization result
                        mInitialized.put(component, result);
                    } catch (Throwable throwable) {
                        throw newStartupException(throwable); }}else {
                    // Initializer is initialized for the Component
                    // Get the cache value and return it
                    result = mInitialized.get(component);
                }
                return (T) result;
            } finally{ Trace.endSection(); }}}Copy the code

Five, insufficient points

I’ve listed the pros and cons of Startup

  1. The InitializationProvideronCreate()Methods are called on the main thread, causing each Initializer to run on the main thread by default, which is not suitable for components that take too long to initialize and need to run on a child thread. And, Initializercreate(context: Context)The method is intended to complete the initialization of the component and return the initialization result value. If we run the initialization of the time-consuming component with the active new Thread here, we will not be able to return a meaningful result value, which will indirectly lead to the failure to obtain the cached initialization result value through AppInitializer
  2. If the initialization of a component depends on the result of another component that takes a long time to initialize and needs to run on a child thread, Startup does not apply
  3. For third-party dependent libraries that have already initialized their initialization logic using a ContentProvider, we generally cannot modify the initialization logic directly (unless clone the project to the local source code). Therefore, the significance of Startup in the initial phase is to unify the initialization entry of the local components of the project. Startup has to be accepted and used by a majority of developers before it has a performance advantage