IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 【组件化】如何让Android Library拥有Application生命 -> 正文阅读

[移动开发]【组件化】如何让Android Library拥有Application生命

JetPack之App Startup改造过程

  1. 组件化过程中,遇到了什么困难?
  2. 对这困难,你有啥想法?
  3. App Startup
  4. App Startup优点缺点
  5. 改造思路
  6. 优化体验
  7. 开源代码
  8. 参考资料

组件化过程中,遇到了什么困难

我们希望将业务划分为一个个的模块(Library),从而更好的组装、拆卸、迭代业务。
通过业务模块在开发时,可能需要接入一些SDK(外部or内部),这些SDK都会要求在Application中初始化。

方案一:指定一个Application类,把这些初始化的代码,写到Application中。

public class GlobalApp extends BaseApplication {

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

    //以下依赖将在所有进程中初始化
    public void initAllProcessDependencies(Application app) {
        CApp.init(app);
        Utils.init(CApp.getApp());
        initTinker();
        initICBC(app);
    }

    //以下依赖仅在主进程中初始化
    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();
    }
	...
}

方案二:定义一个ContentProvider,将初始化的代码放到ContentProvider去初始化。

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

然后在AndroidManifest.xml文件中注册这个privoder,如下:

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

每一个Library都创建一个自己的ContentProvider,那么…
image.png
image.png

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

image.png

image.png

image.png

最后构建出来的apk就会有多个provider注册,官方称每一个空的provider都会带来2毫秒的启动延时消耗。

项目模块化之后,会存在很多的模块,如果每个模块都创建一个ContentProvider会导致App启动负担过大,而且如果有模块初始化存在依赖关系(比如:B模块初始化,要等A初始化完成后再初始化),这种方案就没法实现了。

方案三:将全类名,写到Application标签的meta-data中,在Application中,去反射这些全类名,从而加载它们。这样还是要将一部分代码放到Application中,仍然很难解决依赖的问题。

这个就不写例子了,Application中先读meta-data的name,通过反射把name中描述的全类名加载出来。


对这困难,你有啥想法?

嗯?如果结合方案二和方案三,是不是可以创造一个新东东,既可以不在Application中写代码,又可以不用写很多的ContentProvider…


App Startup

没错,这就是JetPack家族中App Startup组件的思路,它提供了一个ContentProvider代理类,取代了其它ContentProvider的开销,让项目不会因为ContentProvider越来越多而启动速度越来越慢,然后让其它模块注册meta-data全类名时,通过meger标识放到同一个ContentProvider代理类的上下文中,在ContentProvider代理类中,统一去加载。

App Startup到底能带来多大的提升呢?
image.png
这是Google在Pixel2的Android 10上做的测试,每一个ContentProvider的增加,至少会带来2毫秒的消耗,这仅仅是空ContentProvider的附加成本。

那么使用App Startup,在模块越多的项目中,效果越好,它会一定程度的加快App启动速度,而且JetPack家庭中的WorkManager和Lifecycle都是基于它来实现库初始化工作的。

这么优秀,那接下来我们了解一下它怎么用的吧。

  • 第一步,让Library依赖app startup库
dependencies {
    implementation "androidx.startup:startup-runtime:1.0.0-alpha02"
...
}
  • 第二步,写一个简单的初始化类
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","卢本伟牛逼~");
    }

    public Context getContext() {
        return applicationContext;
    }

}
  • 第三步,写一个组件初始化代理类
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();
    }
}
  • 第四步,清单文件中注册
<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>
  • 上面,第一个Library就写完了,使用同样的方法,写第一个Library,最后让app模块,依赖它俩,就可以运行了。
dependencies {
    implementation project(":FirstSdkInitialize")
    implementation project(":SecondSdkInitialize")
	...
}

image.png

我们看到,它是通过一个InitializationProvider类,加载两个模块的meta-data的。
image.png

  • 那么这里,能不能让卢本伟先回来,再夸他呢?

  • 可以,咱让第一个Library,依赖第二个Library,就可以实现。

dependencies {
    implementation "androidx.startup:startup-runtime:1.0.0-alpha02"
    implementation project(":SecondSdkInitialize")
...
}
  • 然后在第一个Library的初始化类中,传个依赖关系list给组件。
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;
    }
}

image.png

  • 这样顺序初始化就实现了,但…这样Library之间就存在依赖关系啦,咱们组件化的项目,希望组件间尽可能的不产生依赖,咋办…

App Startup优点缺点

优点:

  • 解决了多个sdk初始化导致Application文件和Mainfest文件需要频繁改动的问题,同时也减少了Application文件和Mainfest文件的代码量,更方便维护了
  • 方便了sdk开发者在内部处理sdk的初始化问题,并且可以和调用者共享一个ContentProvider,减少性能损耗。
  • 提供了所有sdk使用同一个ContentProvider做初始化的能力,并精简了sdk的使用流程。
  • 符合面向对象中类的单一职责原则
  • 有效解耦,方便协同开发

缺点:

  • 会通过反射实例化Initializer<>的实现类,在低版本系统中会有一定的性能损耗。
  • 模块间遇到(B模块初始化,要等A初始化完成后再初始化)这种情况,就会让模块间产生依赖。
  • 不支持后台初始化

关于App Startup,请自行官网了解,这里就不再赘述。
下面主要针对缺点,进行一些改造。
它让模块化之后的业务模块之间又产生了依赖关系,这个不能接受。


改造思路

试着动手改造App Startup

  1. 先解决(B模块初始化,要等A初始化完成后再初始化)的问题。
    1. 在初始化类上写个优先级注解
    2. 在ContentProvider代理类中,拿到Class后,去读优先级,然后排序
    3. 根据排序好的Class,逐个初始化
//定义初始化类
@QuickPriority(ModulePriority.LEVEL_2)
public class A extends InitService {
    @Override
    protected void bindApplication(Context context) {
        Log.d("cheetah","看我,我手指放松,我目光如龙,当敌人是空,我战法无穷。"+Thread.currentThread().getId());
    }
}
//注册到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>

//在代理类中加载
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;
    }
    // ...
}
  • 注:篇幅问题仅展示核心代码
  1. 再提供一种子线程初始化的方法,但通常不建议放子线程中,控制不好就会挂,极限启动优化,可能会用。
    1. 定义优先级注解为Integer的最大值,则标识需要子线程初始化
    2. 读取优先级时,将这些Class,单独拿出来,开线程初始化它们。
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);
        }
//延迟初始化实现类
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);
                            }
                        }
                    });
                }
            }
        });
    }
}

优化体验 - 编写插件的原因

清单文件这玩意写的多烦啊,每个模块都得Copy一遍,搞错一个字母都不知道问题出在哪里,除了meta-data的name不一样,其它的都一样,组件啊组件,你能不能帮我搞定这些重复代码块?

//注册到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>

好,我开发一个插件,帮你搞定重复的代码。

思路:

  1. Gradle编译期,拿到Library的清单ProcessManifestTask任务,自动写这玩意。
  2. meta-data的name变量,通过build.gradle传过来。

最终是这样的:

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

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

dependencies {
    implementation rootProject.ext.dependencies.deviceid
}

Gradle插件编写

  • 第一步,拿到Library的清单ProcessManifestTask的Hook点。
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')
        }
	//读变量
        project.extensions.create("ModuleStartup", ModuleStartupExtension)
        moduleStartupExtension = project['ModuleStartup']

	//找Hook点
        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 ->
                    //在这里去写清单文件
                }
            }

        }
    }
    static Task getProcessManifestTask(Project project, String variantName) {
        String mergeManifestTaskName = "process${variantName}Manifest"
        return project.tasks.findByName(mergeManifestTaskName)
    }
}
  • 第二步,编辑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>
     *     创建一个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
    }

  • 第三步把标签写进去
/** 追加knifeInfo */
    def appendManifest(def file) {
        if (file == null || !file.exists()) return
        println "${AppConstant.TAG} appendManifest: ${file}"
        ManifestStatBean bean = Presenter.parseManifestStat(file)
        //没有清单文件标签直接中断处理
        if (!bean.hasStartManifest) {
            println "${file} not found Manifest Tag"
            return
        }
        String updatedContent = file.getText("UTF-8")
        //没有tools命名空间,则先写一个tools的命名空间
        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')
    }
  • 到这里核心代码就结束啦,畅饮一杯可乐先。

开源代码

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

  • ModuleStartupPlugin(可选插件):http://igit.58corp.com/Finance_Android_Open_Source/ModuleStartupPlugin

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

  • 注:要再造轮子的各位,请阅读完整源代码,除了核心代码外,还是很多坑在非核心代码中。


参考资料

  • app-startup官方:https://developer.android.com/topic/libraries/app-startup
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-23 11:34:50  更:2021-09-23 11:35:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 20:11:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码