[componentization] how to make Android Library have Application life

Keywords: Android

App Startup transformation process of JetPack

  1. What are the difficulties encountered in the process of componentization?
  2. What do you think of this difficulty?
  3. App Startup
  4. App Startup pros and cons
  5. Transformation ideas
  6. Optimize experience
  7. open source
  8. reference material

We hope to divide the business into modules (Libraries), so as to better assemble, disassemble and iterate the business.
During development through the business module, you may need to access some SDK s (external or internal), which require initialization in the Application.

Scheme 1: specify an Application class and write these initialization codes into the Application.

public class GlobalApp extends BaseApplication {

    @Override
    protected void setup() {
        initAllProcessDependencies(this);
        if (ProcessUtils.isMainProcess()) {
            initMainProcessDependencies();
        }
    }

    //The following dependencies will be initialized in all processes
    public void initAllProcessDependencies(Application app) {
        CApp.init(app);
        Utils.init(CApp.getApp());
        initTinker();
        initICBC(app);
    }

    //The following dependencies are initialized only in the main process
    public void initMainProcessDependencies() {
        ClideFactory.init(CApp.getApp(), R.mipmap.image_error);
        initReporter();
        KV.init(CApp.getApp());
        initRouter();
        initCrash();
        initBugly();
        initFPush(UDIDUtils.getUniqueID_NotPermission());
        initWOS();
        initLocation();
        initHeader();
        initDeviceId();
        initDun();
        initFFMpeg();
        initStackManage();
    }
	...
}

Scheme 2: define a ContentProvider and put the initialization code into the ContentProvider for initialization.

public class Sdk1InitializeProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        Sdk1.init(getContext());
        return true;
    }
	...
}

Then register the privoder in the AndroidManifest.xml file, as follows:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="top.gradle.sdk1">
    <application>
        <provider
            android:authorities="${applicationId}.init-provider"
            android:name=".Sdk1InitializeProvider"
            android:exported="false"/>
    </application>
</manifest>

Each Library creates its own ContentProvider, then

dependencies {
    implementation project(":FirstSdkInitialize")
    implementation project(":SecondSdkInitialize")
	...
}

Finally, the apk built will have multiple providers registered. Officials say that each empty provider will bring a 2-millisecond startup delay consumption.

After the project is modularized, there will be many modules. If each module creates A ContentProvider, the burden of App startup will be too large. Moreover, if there are dependencies in module initialization (for example, module B initialization, wait until module A initialization is completed), this scheme cannot be implemented.

Scheme 3: write the full class names into the meta data of the Application tag. In the Application, reflect these full class names to load them. In this way, it is still difficult to solve the dependency problem by putting part of the code into the Application.

There is no need to write an example. In the Application, first read the name of meta data and load the full class name described in name through reflection.

Huh? If you combine scheme 2 and scheme 3, can you create a new thing, not only without writing code in the Application, but also without writing a lot of ContentProvider

Yes, this is the JetPack family App Startup Component, which provides a ContentProvider proxy class instead of the overhead of other ContentProviders, so that the project will not start more and more slowly because there are more and more ContentProviders. Then, when other modules register the full class name of meta data, they will put it into the context of the same ContentProvider proxy class through the meger ID. in the ContentProvider proxy class, Load uniformly.

How much improvement can App Startup bring?

This is a test done by Google on Android 10 of Pixel2. The increase of each ContentProvider will bring a consumption of at least 2 milliseconds, which is only the additional cost of empty ContentProvider.

Then, using App Startup, the effect will be better in projects with more modules. It will speed up App Startup to a certain extent. Moreover, WorkManager and Lifecycle in JetPack family are based on it to realize library initialization.

So excellent, let's learn how to use it next.

  • First, let the Library rely on the app startup Library
dependencies {
    implementation "androidx.startup:startup-runtime:1.0.0-alpha02"
...
}
  • The second step is to write a simple initialization class
public class FirstSdkInitialize {

    private Context applicationContext;

    private FirstSdkInitialize() {
    }

    public static FirstSdkInitialize getInstance() {
        return FirstSdkInitializeHold.INSTANCE;
    }

    private static class FirstSdkInitializeHold {
        public static final FirstSdkInitialize INSTANCE = new FirstSdkInitialize();
    }

    public void init(Context applicationContext) {
        this.applicationContext = applicationContext.getApplicationContext();
        Log.d("cheetah","Lu Benwei~");
    }

    public Context getContext() {
        return applicationContext;
    }

}
  • Step 3: write a component to initialize the proxy class
public class FirstInitializeProxy implements Initializer<FirstSdkInitialize> {
    @NonNull
    @Override
    public FirstSdkInitialize create(@NonNull Context context) {
        FirstSdkInitialize.getInstance().init(context);
        return FirstSdkInitialize.getInstance();
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        return Collections.emptyList();
    }
}
  • Step 4: register in the manifest file
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.wuba.financial.firstsdkinitialize">

    <application>
        <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.wuba.financial.firstsdkinitialize.FirstInitializeProxy"
                android:value="androidx.startup" />
        </provider>
    </application>
</manifest>
  • Above, the first Library is finished. Use the same method to write the first Library. Finally, let the app module rely on them to run.
dependencies {
    implementation project(":FirstSdkInitialize")
    implementation project(":SecondSdkInitialize")
	...
}

We can see that it loads the meta data of two modules through an InitializationProvider class.

  • So here, can you let Lu Benwei come back first and then praise him?

  • Yes, let's let the first Library rely on the second Library.

dependencies {
    implementation "androidx.startup:startup-runtime:1.0.0-alpha02"
    implementation project(":SecondSdkInitialize")
...
}
  • Then, in the initialization class of the first Library, pass a dependency list to the component.
public class FirstInitializeProxy implements Initializer<FirstSdkInitialize> {
    @NonNull
    @Override
    public FirstSdkInitialize create(@NonNull Context context) {
        FirstSdkInitialize.getInstance().init(context);
        return FirstSdkInitialize.getInstance();
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        List<Class<? extends Initializer<?>>> dependencies = new ArrayList<>();
        dependencies.add(SecondInitializeProxy.class);
        return dependencies;
    }
}

  • In this way, sequential initialization is realized, but... In this way, there are dependencies between libraries. For our component-based project, we hope that there will be no dependencies between components as much as possible. What should we do

advantage:

  • It solves the problem that Application files and Mainfest files need to be changed frequently due to multiple sdk initialization. At the same time, it also reduces the amount of code of Application files and Mainfest files, which is more convenient for maintenance
  • It is convenient for sdk developers to handle sdk initialization internally, and can share a ContentProvider with callers to reduce performance loss.
  • It provides the ability for all SDKs to use the same ContentProvider for initialization, and simplifies the use process of SDKs.
  • Conform to the principle of single responsibility of classes in object-oriented
  • Effective decoupling to facilitate collaborative development

Disadvantages:

  • The implementation class of initializer < > will be instantiated through reflection, which will cause some performance loss in low version systems.
  • When modules encounter this situation (module B is initialized, and it will be initialized after module A is initialized), dependencies will occur between modules.
  • Background initialization is not supported

about App Startup , please learn about it on your official website. I won't repeat it here.
The following mainly aims at the shortcomings and makes some modifications.
It creates dependencies between business modules after modularization, which is unacceptable.

Try to transform App Startup

  1. First solve the problem (initialization of module B, which will be initialized after initialization of module A).
    1. Write a priority annotation on the initialization class
    2. In the ContentProvider proxy Class, after getting the Class, read the priority and sort it
    3. Initialize one by one according to the sorted classes
//Define initialization class
@QuickPriority(ModulePriority.LEVEL_2)
public class A extends InitService {
    @Override
    protected void bindApplication(Context context) {
        Log.d("cheetah","Look at me, my fingers relax, my eyes are like dragons. When the enemy is empty, my tactics are infinite."+Thread.currentThread().getId());
    }
}
//Register in xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.wuba.borrowfinancials.cid">
    <application>
        <provider
            android:name="com.wuba.borrowfinancials.knife.init.ModuleInitProxy"
            android:authorities="${applicationId}.module-init"
            android:exported="false"
            tools:node="merge">
            <!-- This entry makes ExampleLoggerInitializer discoverable. -->
            <meta-data
                android:name="com.wuba.borrowfinancials.cid.CidInitService"
                android:value="module.knife" />
        </provider>
    </application>
</manifest>

//Load in proxy class
public class ModuleInitProxy extends ContentProvider {

    @Override
    public boolean onCreate() {
        try {
            ComponentName provider = new ComponentName(mContext.getPackageName(),
                    ModuleInitProxy.class.getName());
            ProviderInfo providerInfo = mContext.getPackageManager()
                    .getProviderInfo(provider, GET_META_DATA);
            Bundle metadata = providerInfo.metaData;
            if (metadata != null) {
                Set<Class<?>> initializing = new HashSet<>();
                List<ModuleInitOrderBean> orderBeans = new ArrayList<>();
                Set<String> keys = metadata.keySet();
                for (String key : keys) {
                    String value = metadata.getString(key, null);
                    if (ModuleInitUtils.MODULE_KNIFE.equals(value)) {
                        Class<?> clazz = Class.forName(key);
                        if (InitService.class.isAssignableFrom(clazz)) {
                            Class<? extends IModuleInitializer> component =
                                    (Class<? extends IModuleInitializer>) clazz;
                            ModuleInitOrderBean bean = new ModuleInitOrderBean();
                            bean.setClazz(component);
                            bean.setPriority(ModuleInitUtils.getPriority(clazz));
                            orderBeans.add(bean);
                        }
                    }
                }
                Collections.sort(orderBeans);
                for (ModuleInitOrderBean bean : orderBeans) {
                    try {
                        Object instance = bean.getClazz().getDeclaredConstructor().newInstance();
                        IModuleInitializer initializer = (IModuleInitializer) instance;
                        initializer.create(mContext);
                        initializing.remove(bean.getClazz());
                    } catch (Throwable throwable) {
                        throw new ModuleInitException(throwable);
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
            throw new ModuleInitException(exception);
        }
        return true;
    }
    // ...
}
  • Note: the length question only shows the core code
  1. Another method for initializing the sub thread is provided, but it is usually not recommended to put it in the sub thread. If it is not well controlled, it will hang. Extreme startup optimization may be used.
    1. If the priority annotation is defined as the maximum value of Integer, the identification needs child thread initialization
    2. When reading the priority, take out these classes separately and start the thread to initialize them.
try {
            ComponentName provider = new ComponentName(mContext.getPackageName(),
                    ModuleInitProxy.class.getName());
            ProviderInfo providerInfo = mContext.getPackageManager()
                    .getProviderInfo(provider, GET_META_DATA);
            Bundle metadata = providerInfo.metaData;
            if (metadata != null) {
                Set<Class<?>> initializing = new HashSet<>();
                List<ModuleInitOrderBean> orderBeans = new ArrayList<>();
		List<ModuleInitOrderBean> delayBeans = new ArrayList<>();
                Set<String> keys = metadata.keySet();
                for (String key : keys) {
                    String value = metadata.getString(key, null);
                    if (ModuleInitUtils.MODULE_KNIFE.equals(value)) {
                        Class<?> clazz = Class.forName(key);
                        if (InitService.class.isAssignableFrom(clazz)) {
                            Class<? extends IModuleInitializer> component =
                                    (Class<? extends IModuleInitializer>) clazz;
                            ModuleInitOrderBean bean = new ModuleInitOrderBean();
                            bean.setClazz(component);
                            bean.setPriority(ModuleInitUtils.getPriority(clazz));
                            if (bean.isDelay()) {
                                delayBeans.add(bean);
                            } else {
                                orderBeans.add(bean);
                            }
                        }
                    }
                }
                Collections.sort(orderBeans);
                for (ModuleInitOrderBean bean : orderBeans) {
                    try {
                        Object instance = bean.getClazz().getDeclaredConstructor().newInstance();
                        IModuleInitializer initializer = (IModuleInitializer) instance;
                        initializer.create(mContext);
                        initializing.remove(bean.getClazz());
                    } catch (Throwable throwable) {
                        throw new ModuleInitException(throwable);
                    }
                }
		 if (null != delayBeans && !delayBeans.isEmpty()) {
                    new DelayInitializer().subMit(mContext, delayBeans);
                }
            }
        } catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
            throw new ModuleInitException(exception);
        }
//Delay initialization implementation class
public class DelayInitializer {

    /**
     * Executes calls. Created lazily.
     */
    private @NonNull
    ExecutorService executorService;

    public DelayInitializer() {
        executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), Util.threadFactory("ModuleDelayInitializer", false));
        initializing = new HashSet<>();
    }

    private void addRunnable(Runnable r) {
        executorService.submit(r);
    }

    private @NonNull
    Set<Class<?>> initializing;

    public void subMit(final Context mContext, final List<ModuleInitOrderBean> list) {
        addRunnable(new Runnable() {
            @Override
            public void run() {
                for (final ModuleInitOrderBean bean : list) {
                    addRunnable(new Runnable() {
                        @Override
                        public void run() {
                            if (initializing.contains(bean.getClazz())) {
                                String message = String.format(
                                        "Cannot initialize %s. Cycle detected.",
                                        bean.getClazz().getName()
                                );
                                throw new IllegalStateException(message);
                            }
                            initializing.add(bean.getClazz());
                            try {
                                Object instance = bean.getClazz()
                                        .getDeclaredConstructor().newInstance();
                                IModuleInitializer initializer = (IModuleInitializer) instance;
                                initializer.create(mContext);
                            } catch (Throwable throwable) {
                                throw new ModuleInitException(String.format(
                                        "Cannot initialize %s. not found DeclaredConstructors.",
                                        bean.getClazz().getName()
                                ), throwable);
                            }
                        }
                    });
                }
            }
        });
    }
}

How annoying it is to write a list file. Every module has to Copy it. I don't know what the problem is. Except for the different name of meta data, everything else is the same. Components, components, can you help me deal with these repeated code blocks?

//Register in xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.wuba.borrowfinancials.cid">
    <application>
        <provider
            android:name="com.wuba.borrowfinancials.knife.init.ModuleInitProxy"
            android:authorities="${applicationId}.module-init"
            android:exported="false"
            tools:node="merge">
            <!-- This entry makes ExampleLoggerInitializer discoverable. -->
            <meta-data
                android:name="com.wuba.borrowfinancials.cid.CidInitService"
                android:value="module.knife" />
        </provider>
    </application>
</manifest>

OK, I'll develop a plug-in to help you deal with duplicate code.

Idea:

  1. During Gradle compilation, get the list ProcessManifestTask task task of the Library and write it automatically.
  2. The name variable of meta data is passed through build.gradle.

In the end:

apply {
    from "${rootDir.path}/gradle/output/lib.gradle"
}

ModuleStartup {
    initClass = "com.wuba.borrowfinancials.cid.CidInitService"
}

dependencies {
    implementation rootProject.ext.dependencies.deviceid
}

Written by Gradle plug-in

  • The first step is to get the Library list and the Hook point of ProcessManifestTask.
class ModuleStartup implements Plugin<Project> {

    ModuleStartupExtension moduleStartupExtension

    @Override
    void apply(Project project) {
        if (!project.plugins.hasPlugin('com.android.library')) {
            throw new GradleException('ModuleStartup: Android library plugin required')
        }
	//Read variable
        project.extensions.create("ModuleStartup", ModuleStartupExtension)
        moduleStartupExtension = project['ModuleStartup']

	//Find Hook point
        def android = project.extensions.android
        android.libraryVariants.all { variant ->
            String variantName = variant.name.capitalize()
            getProcessManifestTask(project, variantName).doLast {
                println "${AppConstant.TAG} processManifest: ${it.outputs.files} "
                it.outputs.files.each { File file ->
                    //Write the manifest file here
                }
            }

        }
    }
    static Task getProcessManifestTask(Project project, String variantName) {
        String mergeManifestTaskName = "process${variantName}Manifest"
        return project.tasks.findByName(mergeManifestTaskName)
    }
}
  • The second step is to edit the xml
    def static final START_MANIFEST_TAG = "<manifest"
    def static final TOOLS_TAG = "xmlns:tools=\"http://schemas.android.com/tools\""
    def static final APPLICATION_TAG = "<application"
    def static final MANIFEST_END_TAG = "</manifest>"

    def static final APPLICATION_END_TAG = "</application>"
    def static final MANIFEST_EMPTY_END = "/>"


    def static final provider_name = "android:name"
    def static final authorities = "android:authorities"
    def static final exported = "android:exported"
    def static final value = "android:value"
    def static final node = "tools:node"

    def static final provider_name_value = "com.wuba.financial.base.module.startup.ModuleInitProxy"
    def static final authorities_value = ".module-init"
    def static final exported_value = "false"
    def static final node_value = "merge"

    def static final meta_value = "module.knife"

/**
     * <b>
     *     Create a label string for the Provider
     * </b>
     * @since phantom 2020/7/28 8:10 PM
     *
     * @param null
     * @return
     */
    def static generatorProviderTag(Boolean hasApplication, String applicationID, String initClass) {
        def strXml = new StringWriter()
        MarkupBuilder mb = new MarkupBuilder(strXml)
        if (hasApplication) {
            mb.provider(
                    "${provider_name}": "${provider_name_value}",
                    "${authorities}": "${applicationID}${authorities_value}",
                    "${exported}": "${exported_value}",
                    "${node}": "${node_value}") {
                "meta-data"(
                        "${provider_name}": "${initClass}",
                        "${value}": "${meta_value}"
                )
            }
        } else {
            mb.application {
                provider(
                        "${provider_name}": "${provider_name_value}",
                        "${authorities}": "${applicationID}${authorities_value}",
                        "${exported}": "${exported_value}",
                        "${node}": "${node_value}") {
                    "meta-data"(
                            "${provider_name}": "${initClass}",
                            "${value}": "${meta_value}"
                    )
                }
            }
        }
        strXml
    }

  • Step 3 write the label in
/** Append knifeInfo */
    def appendManifest(def file) {
        if (file == null || !file.exists()) return
        println "${AppConstant.TAG} appendManifest: ${file}"
        ManifestStatBean bean = Presenter.parseManifestStat(file)
        //No manifest file tag directly interrupts processing
        if (!bean.hasStartManifest) {
            println "${file} not found Manifest Tag"
            return
        }
        String updatedContent = file.getText("UTF-8")
        //If there is no tools namespace, write a tools namespace first
        if (!bean.hasTools) {
            updatedContent = Presenter.writeTools(updatedContent)
        }
        String content = Presenter.generatorProviderTag(
                bean.hasApplication
                , moduleStartupExtension.applicationId
                , moduleStartupExtension.initClass)
        if (bean.hasApplication) {
            if (bean.hasEndApplication) {
                updatedContent = Presenter.replaceEndApplicationTag(updatedContent, content)
            } else {
                updatedContent = Presenter.replaceApplicationEmptyEndTag(updatedContent, content)
            }
        } else {
            if (bean.hasEndManifest) {
                updatedContent = Presenter.replaceEndManifest(updatedContent, content)
            } else {
                updatedContent = Presenter.replaceManifestEmptyEndTag(updatedContent, content)
            }
        }
        file.write(updatedContent, 'UTF-8')
    }
  • Here, the core code is over. Drink a coke first.

  • ModuleStartup: http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartup

  • Modulestartupplugin (optional plug-in): http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartupPlugin

  • Demo: http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartupDemo

  • Note: to rebuild the wheel, please read the complete source code. In addition to the core code, there are many pits in the non core code.

  • App startup official: https://developer.android.com/topic/libraries/app-startup

Posted by jeremyphphaven on Wed, 22 Sep 2021 10:20:26 -0700