/   今日科技快讯   /

微软昨日宣布,将以75亿美元的现金收购游戏开发商Bethesda Softworks母公司ZeniMax Media。ZeniMax Media是世界上最大的私人持股游戏开发商和发行商之一。Bethesda Softworks广受游戏界好评,代表作包括角色扮演游戏《上古卷轴》和《辐射》等。

/   作者简介   /

本篇文章来自叶志陈的投稿,分享了他对Startup框架的使用以及理解,或许会对大家有一定的借鉴意义。同时也感谢作者贡献的精彩文章!

叶志陈的博客地址:

https://juejin.im/user/923245496518439

/   前言   /

Google Jetpack官网上新增了一个名为App Startup的组件。官方地址为:

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

根据官方文档的介绍,App Startup提供了一种直接、高效的方式用来在应用程序启动时对多个组件进行初始化,开发者可以依靠它来显式地设置多个组件间的初始化顺序并优化应用的启动时间。

本文内容均基于App Startup当前最新的alpha版本:

implementation "androidx.startup:startup-runtime:1.0.0-alpha01"

/   AppStartup的意义   /

App Startup允许Library开发者和App开发者共享同一个ContentProvider来完成各自的初始化逻辑,并支持设置组件之间的初始化先后顺序,避免为每个需要初始化的组件都单独定义一个ContentProvider,从而大大缩短应用的启动时间。

目前很多第三方依赖库为了简化使用者的使用成本,就选择通过声明一个ContentProvider来获取Context对象并自动完成初始化过程。例如Lifecycle组件就声明了一个ProcessLifecycleOwnerInitializer用于获取context对象并完成初始化。而在AndroidManifest.xml文件中声明的每一个ContentProvider,在Application的onCreate()函数被调用之前就会预先被执行并调用内部的onCreate()方法。应用每构建并执行一个ContentProvider都是有着内存和时间的消耗成本,如果应用的ContentProvider过多,无疑会大大增加应用的启动时间。

因此,App Startup的存在,无疑是可以为很多依赖项(应用自身的组件和第三方组件)提供一个统一的初始化入口,当然这也需要等到App Startup发布 release 版本并被大多数三方依赖组件采用之后了。

/   如何使用   /

假设我们的项目中一共有三个Library需要进行初始化。当中,Library A依赖于Library B,Library B依赖于Library C,Library C不需要其它依赖项,则此时可以分别为三个Library建立三个Initializer实现类。

Initializer是Startup提供的用于声明初始化逻辑和初始化顺序的接口,在 create(context: Context)方法中完成初始化过程并返回结果值,在dependencies()中指定初始化此Initializer前需要先初始化的其它 Initializer。

class InitializerA : Initializer<A> {        //在此处完成组件的初始化,并返回初始化结果值        override fun create(context: Context): A {            return A.init(context)        }         //获取在初始化自身之前需要先初始化的 Initializer 列表        //如果不需要依赖于其它组件,则可以返回一个空列表        override fun dependencies(): 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(): 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(): List<Class<out Initializer<*>>> {            return listOf()        }       }

Startup提供了两种初始化方法,分别是自动初始化和手动初始化(延迟初始化)。

自动初始化

在AndroidManifest文件中对Startup提供的InitializationProvider进行声明,并且用meta-data标签声明Initializer实现类的包名路径,value必须是 "androidx.startup"。在这里我们只需要声明InitializerA即可,因为InitializerB和InitializerC均可以通过InitializerA的dependencies()方法的返回值链式定位到。

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

只要完成以上步骤,当应用启动时,Startup就会自动按照我们规定的顺序依次进行初始化。需要注意的是,如果Initializer之间不存在依赖关系,且都希望由InitializationProvider为我们自动初始化的话,此时所有的Initializer就必须都进行显式声明,且Initializer的初始化顺序会和在provider中的声明顺序保持一致。

手动初始化

大部分情况下自动初始化的方式都能满足我们的要求,但在某些情况下并不适用,例如:组件的初始化成本(性能消耗或者时间消耗)较高且该组件最终未必会使用到,此时就可以将之改为在使用到的时候再来对其进行初始化了,即懒加载组件。

手动初始化的Initializer不需要在AndroidManifest中进行声明,只需要通过调用以下方法进行初始化即可。

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

由于Startup内部会缓存Initializer的初始化结果值,所以重复调用initializeComponent方法不会导致多次初始化,该方法也可用于自动初始化时获取初始化结果值。如果应用内的所有Initializer都不需要进行自动初始化的话,也可以不在AndroidManifest中声明InitializationProvider。

/   注意事项   /

移除Initializer

假设我们在项目中引入的某个第三方依赖库自身使用到了Startup进行自动初始化,我们希望将之改为懒加载的方式,但我们无法直接修改第三方依赖库的AndroidManifest文件,此时就可以通过AndroidManifest的合并规则来移除指定的Initializer。

假设第三方依赖库的Initializer的包名路径是xxx.xxx.InitializerImpl,在主项目工程的AndroidManifest文件中主动对其进行声明,并添加tools:node="remove"语句要求在合并AndroidManifest文件时移除自身,这样Startup就不会自动初始化InitializerImpl了。

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

禁止自动初始化

如果希望禁止Startup的所有自动初始化逻辑,但又不希望通过直接删除provider声明来实现的话,那么可以通过如上所述的方法来实现此目的。

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

Lint 检查

App Startup包含一组Lint规则,可用于检查是否已正确定义了组件的初始化程序,可以通过运行./gradlew :app:lintDebug来执行检查规则。例如,如果项目中声明的InitializerB没有在AndroidManifest中进行声明,且也不包含在其它Initializer的依赖项列表里时,通过Lint检查就可以看到如下的警告语句:

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.
[EnsureInitializerMetadata]  class InitializerB : Initializer<B> {

/   源码解析   /

Startup整个依赖库仅包含五个Java文件,整体逻辑比较简单,这里依次介绍下每个文件的作用。

StartupLogger

StartupLogger是一个日志工具类,用于向控制台输出日志。

public final class StartupLogger {private StartupLogger() {// 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);    }}

StartupException

StartupException是一个自定义的RuntimeException子类,当Startup在初始化过程中遇到意外之外的情况时(例如,Initializer存在循环依赖、Initializer反射失败等情况),就会抛出StartupException。

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

Initializer

Initiaizer是Startup提供的用于声明初始化逻辑和初始化顺序的接口,在create(context: Context)方法中完成初始化过程并返回结果值,在dependencies()中指定初始化此Initializer前需要先初始化的其它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`.*/@NonNull    List<Class<? extends Initializer<?>>> dependencies();}

InitializationProvider

InitializationProvider就是需要我们主动声明在AndroidManifest.xml文件中的ContentProvider,Startup的整个初始化逻辑都是在这里进行统一触发的。由于InitializationProvider的作用仅是用于统一多个依赖项的初始化入口并获得Context对象,所以除了onCreate()方法会由系统自动调用外,query、getType、insert、delete、update等方法本身是没有意义的,如果开发者调用了这几个方法就会直接抛出异常。

public final class InitializationProvider extends ContentProvider {@Overridepublic boolean onCreate() {Context context = getContext();if (context != null) {AppInitializer.getInstance(context).discoverAndInitialize();} else {throw new StartupException("Context cannot be null");}return true;}@Nullable@Overridepublic Cursor query(@NonNull Uri uri,@Nullable String[] projection,@Nullable String selection,@Nullable String[] selectionArgs,@Nullable String sortOrder) {throw new IllegalStateException("Not allowed.");}@Nullable@Overridepublic String getType(@NonNull Uri uri) {throw new IllegalStateException("Not allowed.");}@Nullable@Overridepublic 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.");}    @Overridepublic int update(@NonNull Uri uri,@Nullable ContentValues values,@Nullable String selection,@Nullable String[] selectionArgs) {throw new IllegalStateException("Not allowed.");}}

AppInitializer

AppInitializer是Startup整个库的核心重点,整体代码量不足两百行,AppInitializer的整体流程是:

  • 由InitializationProvider传入Context对象以此来获得AppInitializer唯一实例,并调用discoverAndInitialize()函数完成所有的自动初始化逻辑

  • discoverAndInitialize()函数会先对InitializationProvider进行解析,获取到包含的所有metadata,然后按声明顺序依次反射构建每个metadata指向的Initializer对象

  • 当在初始化某个Initializer对象之前,会首先判断其关联的依赖项dependencies是否为空。如果为空的话则直接调用其 create(Context) 函数进行初始化。如果不为空的话则先对dependencies进行初始化,对每个dependency均重复此遍历操作,直到不包含dependencies的Initializer最先初始化完成后才原路返回依次进行初始化,从而保证了Initializer之间初始化顺序的有序性

  • 当存在这几种情况时,Startup会抛出异常:Initializer实现类不包含无参构造函数、Initializer之间存在循环依赖关系、Initializer的初始化过程(create(Context) 函数)抛出了异常

AppInitializer对外开放了getInstance(@NonNull Context context)方法用于获取唯一的静态实例。

public final class AppInitializer {/*** 唯一的静态实例* The {@link AppInitializer} instance.*/private static AppInitializer sInstance;/*** 同步锁* Guards app initialization.*/private static final Object sLock = new Object();//用于存储所有已进行初始化了的 Initializer 及对应的初始化结果@NonNullfinal Map<Class<?>, Object> mInitialized;@NonNullfinal 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);}return sInstance;}}···    }

discoverAndInitialize()方法由InitializationProvider进行调用,由其触发所有需要进行默认初始化的依赖项的初始化操作。

@SuppressWarnings("unchecked")
void discoverAndInitialize() {try {Trace.beginSection(SECTION_NAME);//获取 InitializationProvider 包含的所有 metadataComponentName provider = new ComponentName(mContext.getPackageName(),InitializationProvider.class.getName());ProviderInfo providerInfo = mContext.getPackageManager().getProviderInfo(provider, GET_META_DATA);Bundle metadata = providerInfo.metaData;//获取到字符串 androidx.startup//因为 Startup 是以该字符串作为 metaData 的固定 value 来进行遍历的//所以如果在 AndroidManifest 文件中声明了不同 value 则不会被初始化String startup = mContext.getString(R.string.androidx_startup);if (metadata != null) {//用于标记正在准备进行初始化的 Initializer//用于判断是否存在循环依赖的情况Set<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);//确保 metaData 声明的包名路径指向的是 Initializer 的实现类 if (Initializer.class.isAssignableFrom(clazz)) {Class<? extends Initializer<?>> component =(Class<? extends Initializer<?>>) clazz;if (StartupLogger.DEBUG) {StartupLogger.i(String.format("Discovered %s", key));                            }//进行实际的初始化过程doInitialize(component, initializing);}}}}} catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {            throw new StartupException(exception);} finally {Trace.endSection();}}

doInitialize()函数是实际调用了Initializer的create(context: Context)的地方,其主要逻辑就是通过嵌套调用的方式来完成所有依赖项的初始化,当判断出存在循环依赖的情况时将抛出异常。

@NonNull
@SuppressWarnings({"unchecked","TypeParameterUnusedInFormals"})
<T> T doInitialize(            @NonNull Class<? extends Initializer<?>> component,            @NonNull Set<Class<?>> initializing) {        synchronized (sLock) {            boolean isTracingEnabled = Trace.isEnabled();            try {                if (isTracingEnabled) {                    // Use the simpleName here because p names would get too big otherwise.                    Trace.beginSection(component.getSimpleName());                }                if (initializing.contains(component)) {                    //initializing 包含 component,说明 Initializer 之间存在循环依赖                    //直接抛出异常                    String message = String.format("Cannot initialize %s. Cycle detected.", component.getName());                    throw new IllegalStateException(message);}Object result;if (!mInitialized.containsKey(component)) {//如果 mInitialized 不包含 component//说明 component 指向的 Initializer 还未进行初始化initializing.add(component);                    try {                        //通过反射调用 component 的无参构造函数并初始化Object instance = component.getDeclaredConstructor().newInstance();                        Initializer<?> initializer = (Initializer<?>) instance;//获取 initializer 的依赖项List<Class<? extends Initializer<?>>> dependencies =initializer.dependencies();                        //如果 initializer 的依赖项 dependencies 不为空//则遍历 dependencies 每个 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()));}//进行初始化result = initializer.create(mContext);if (StartupLogger.DEBUG) {StartupLogger.i(String.format("Initialized %s", component.getName()));}//将已经进行初始化的 component 从 initializing 中移除掉//避免误判循环依赖initializing.remove(component);//将初始化结果保存起来mInitialized.put(component, result);} catch (Throwable throwable) {throw new StartupException(throwable);}} else {//component 指向的 Initializer 已经进行初始化//此处直接获取缓存值直接返回即可result = mInitialized.get(component);}return (T) result;} finally {Trace.endSection();}}}

/   AppStartup的不足点   /

App Startup 的优点我在上边已经列举了,最后再来列举下它的几个不足点。

  • InitializationProvider的onCreate()函数是在主线程被调用的,导致我们的每个Initializer默认就都是运行在主线程,这对于某些初始化时间过长,需要运行在子线程的组件来说就不太适用了。且Initializer的create(context: Context) 函数的本意是完成组件的初始化并返回初始化的结果值,如果在此处通过主动new Thread来运行耗时组件的初始化,那么我们就无法返回有意义的结果值,间接导致后续也无法通过AppInitializer获取到缓存的初始化结果值

  • 如果某组件的初始化需要依赖于其它耗时组件(初始化时间过长,需要运行在子线程)的结果值,此时App Startup一样不适用

  • 对于已经使用ContentProvider完成初始化逻辑的第三方依赖库,我们一般也无法直接修改其初始化逻辑(除非clone该项目导到本地直接修改源码),所以在初始阶段App Startup的意义主要在于统一项目本地组件的初始化入口,需要等到App Startup被大多数开发者接受并使用后,才更加具有性能优势

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Jetpack新成员,App Startup一篇就懂

鸿蒙开发初体验

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

Startup攻略秘籍,从头到尾,一篇搞定!相关推荐

  1. 最全中文leetcode解题攻略:思路知识点代码...搞定AI大厂笔试

    本文经AI新媒体量子位(公众号ID:qbitai)授权转载,转载请联系出处. 本文约多图,建议阅读5分钟. 本文为你分享中文leetcode解题攻略,助你通过AI大厂笔试. 当代程序员的困惑可能大致分 ...

  2. Lattice ddr3教程全攻略之时序约束篇

    Lattice ddr3教程全攻略之时序约束篇 在看这篇教程之前,建议先看看我的<Lattice ddr3教程全攻略之仿真篇>,假定你自己的工程仿真好了,自己的代码综合编译通过,但是呢,在 ...

  3. java第七封印游戏_第七封印游戏攻略秘籍集锦

    第七封印这个游戏目前还没有通关,不过小编已经给大家找到了一些游戏的秘籍攻略技巧,包括如何在战斗中逃跑.无限加血等等,虽然有点零零碎碎,但足够大家使用到通关了,一起来看下吧! 第七封印游戏攻略秘籍集锦. ...

  4. 计算机 游戏第14关,《帕拉世界》第十四关至第十六关攻略秘籍

    <帕拉世界>第十四关至第十六关攻略秘籍 2016-11-21 16:37:41来源:游戏下载编辑:评论(0) 第十四关:一开始没必要强攻监狱,部队沿着山道向上,在左上角的出海口那里建立基地 ...

  5. 一篇搞定 SpringBoot+Mybatis+Shiro 实现多角色权限管理

    初衷:我在网上想找整合springboot+mybatis+shiro并且多角色认证的博客,发现找了好久也没有找到想到的,现在自己会了,就打算写个博客分享出去,希望能帮到你. 原创不易,请点赞支持! ...

  6. vlan配置实例详解_网工知识角|MUXVLAN技术详解,基本原理一篇搞定

    学网络,就在IE-LAB 国内高端网络工程师培养基地 MUX VLAN(Multiplex VLAN )提供了一种通过VLAN进行网络资源控制的机制.通过MUX VLAN提供的二层流量隔离的机制可以实 ...

  7. CentOS7搭建LNMP+WordPress一篇搞定

    零.关于本文 本文首次完成于2019年5月12日,经历多次修改.本文所有的参考文献,均以超链接的形式给出.考虑到网上的部分教程不够完整,有的已经过时,我将我搭建环境的方法记录下来. 这篇文章适合: 希 ...

  8. SpringBoot的Web开发支持【超详细【一篇搞定】果断收藏系列】

    SpringBoot的Web开发支持 常用的服务器配置 使用Jetty服务器替换Tomcat 排除Tomcat的启动器,引入Jetty application.yml 编写入口程序 编写Control ...

  9. 软件测试求职攻略第三季:面试篇【乐搏TestPRO】 乐搏软件测试

    作为曾经的测试总监,在面试上我觉得是可以聊一聊这个话题.首先买个关子,如果你是面试官,你希望招一个什么样的人进来?如果这个问题搞明白了,那么可以说测试岗位的面试,就变得非常轻松了. 面试常规流程一般分 ...

最新文章

  1. 7-17 爬动的蠕虫 (C语言)
  2. python输出日期的模版_python按日期区间生成markdown日记模板
  3. B1015/A1062 . 德才论 (25)
  4. 新零售时代,美妆行业如何打造新主场?
  5. Pandas如何检测None和Nan
  6. Spring Boot 使用Dubbo 创建Hello Wrold
  7. 获取编译学习笔记 (十一年)—— 内的中间
  8. QQ防撤回9.0.2 软件 源码 源文件
  9. 系统wmi服务器,wmi的服务器实时监控系统
  10. hdu 6184 Counting Stars
  11. 狐狸逮兔子——链式存储方式
  12. DBeaver解决连接Oracle之后出现库名为数字问题
  13. 什么是网站的源代码?
  14. {经典演讲}庞加莱关于数学发现的心理学的演讲
  15. python人名最多数统计,【Python 测验03】人名最多数统计
  16. Wireshark协议源代码
  17. 快速从入门到精通,建议细读
  18. 初识 - Spring
  19. LM3S9B96开发套件Read Me First1
  20. Win10删除C盘临时文件

热门文章

  1. 用canvas画布画时钟
  2. 2023最新大数据毕设题目推荐100例
  3. WindowServer2012R2+Anoconda3.5.0.1+CUDA9.0+cuDNN7.1.3+Tensorflow-gpu1.6离线搭建深度学习开发环境
  4. t480s控制面板打开触摸板_今年买的thinkpad T480S,但是使用感觉还不如5年前买的S3 touch速度快,是什么原因?...
  5. 【TypeScript】tsc : 无法加载文件 C:\Users\XXX\AppData\Roaming\npm\tsc.ps1,因为在此系统上禁止运行脚本。
  6. 计算机类专业英文缩写,计算机专业英文缩写词汇汇总
  7. MAC 网桥-交换机
  8. pyqt5 向 QTableWidget添加元素以及锁定到某行
  9. php打包签名apk文件在哪,Android_android应用签名详细步骤,1、准备工作apk的签名工作可以 - phpStudy...
  10. 用java写一个图书类book