公众号:字节数组
Google Jetpack 自从推出以后,极大地改变了 Android 开发者们的开发模式,并降低了开发难度。这也要求我们对当中一些子组件的实现原理具有一定的了解,所以我就打算来写一系列 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣🤣
最近,Google Jetpack 官网上新增了一个名为 Startup 的组件。根据官方文档的介绍,Startup 提供了一种直接高效的方式用来在应用程序启动时对多个组件进行初始化,开发者可以依靠它来显式地设置多个组件间的初始化顺序并优化应用的启动时间
本文内容基于以下版本来进行讲解
1 | java复制代码implementation "androidx.startup:startup-runtime:1.0.0-alpha01" |
一、Startup 的意义
Startup 允许 Library 开发者和 App 开发者共享同一个 ContentProvider 来完成各自的初始化逻辑,并支持设置组件之间的初始化先后顺序,避免为每个需要初始化的组件都单独定义一个 ContentProvider,从而大大缩短应用的启动时间
目前很多第三方依赖库为了简化使用者的使用成本,就选择通过声明一个 ContentProvider 来获取 Context 对象并自动完成初始化过程。例如 Lifecycle 组件就声明了一个 ProcessLifecycleOwnerInitializer 用于获取 context 对象并完成初始化。而在 AndroidManifest 文件中声明的每一个 ContentProvider,在 Application 的 onCreate() 方法被调用之前就会预先被执行并调用内部的 onCreate() 方法。应用每构建并执行一个 ContentProvider 都是有着内存和时间的消耗成本,如果应用的 ContentProvider 过多,无疑会大大增加应用的启动时间
因此,Startup 的存在无疑是可以为很多依赖项(应用自身的组件和第三方组件)提供一个统一的初始化入口,当然这也需要等到 Startup 发布 release 版本并被大多数三方依赖组件采用之后了
二、如何使用
假设我们的项目中一共有三个 Library 需要进行初始化。当中,Library A 依赖于 Library B,Library B 依赖于 Library C,Library C 不需要其它依赖项,则此时可以分别为三个 Library 建立三个 Initializer 实现类
Initializer 是 Startup 提供的用于声明初始化逻辑和初始化顺序的接口,在 create(context: Context)方法中完成初始化过程并返回结果值,在dependencies()中指定初始化此 Initializer 前需要先初始化的其它 Initializer
1 | kotlin复制代码class InitializerA : Initializer<A> { |
Startup 提供了两种初始化方法,分别是自动初始化和手动初始化(延迟初始化)
自动初始化
在 AndroidManifest 文件中对 Startup 提供的 InitializationProvider 进行声明,并且用 meta-data 标签声明 Initializer 实现类的包名路径,value 必须是 androidx.startup。在这里我们只需要声明 InitializerA 即可,因为 InitializerB 和 InitializerC 均可以通过 InitializerA 的 dependencies()方法的返回值链式定位到
1 | xml复制代码<provider |
只要完成以上步骤,当应用启动时,Startup 就会自动按照我们规定的顺序依次进行初始化。需要注意的是,如果 Initializer 之间不存在依赖关系,且都希望由 InitializationProvider 为我们自动初始化的话,此时所有的 Initializer 就必须都进行显式声明,且 Initializer 的初始化顺序会和在 provider 中的声明顺序保持一致
手动初始化
大部分情况下自动初始化的方式都能满足我们的要求,但在某些情况下并不适用,例如:组件的初始化成本(性能消耗或者时间消耗)较高且该组件最终未必会使用到,此时就可以将之改为在使用到的时候再来对其进行初始化了,即懒加载组件
手动初始化的 Initializer 不需要在 AndroidManifest 中进行声明,只需要通过调用以下方法进行初始化即可
1 | kotlin复制代码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 了
1 | xml复制代码<provider |
禁止自动初始化
如果希望禁止 Startup 的所有自动初始化逻辑,但又不希望通过直接删除 provider 声明来实现的话,那么可以通过如上所述的方法来实现此目的
1 | xml复制代码<provider |
Lint 检查
Startup 包含一组 Lint 规则,可用于检查是否已正确定义了组件的初始化程序,可以通过运行 ./gradlew :app:lintDebug 来执行检查规则
例如,如果项目中声明的 InitializerB 没有在 AndroidManifest 中进行声明,且也不包含在其它 Initializer 的依赖项列表里时,通过 Lint 检查就可以看到如下的警告语句:
1 | xml复制代码Errors found: |
四、源码解析
Startup 整个依赖库仅包含五个 Java 文件,整体逻辑比较简单,这里依次介绍下每个文件的作用
StartupLogger
StartupLogger 是一个日志工具类,用于向控制台输出日志
1 | java复制代码public final class StartupLogger { |
StartupException
StartupException 是一个自定义的 RuntimeException 子类,当 Startup 在初始化过程中遇到意外之外的情况时(例如,Initializer 存在循环依赖、Initializer 反射失败等情况),就会抛出 StartupException
1 | java复制代码public final class StartupException extends RuntimeException { |
Initializer
Initiaizer 是 Startup 提供的用于声明初始化逻辑和初始化顺序的接口,在 create(context: Context)方法中完成初始化过程并返回结果值,在dependencies()中指定初始化此 Initializer 前需要先初始化的其它 Initializer
1 | java复制代码public interface Initializer<T> { |
InitializationProvider
InitializationProvider 就是需要我们主动声明在 AndroidManifest 文件中的 ContentProvider,Startup 的整个初始化逻辑都是在这里进行统一触发的
由于 InitializationProvider 的作用仅是用于统一多个依赖项的初始化入口并获得 Context 对象,所以除了 onCreate() 方法会由系统自动调用外,其它方法是没有意义的,如果开发者调用了这几个方法就会直接抛出异常
1 | java复制代码public final class InitializationProvider extends ContentProvider { |
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) 方法用于获取唯一的静态实例
1 | java复制代码public final class AppInitializer { |
discoverAndInitialize() 方法由 InitializationProvider 进行调用,由其触发所有需要进行默认初始化的依赖项的初始化操作
1 | java复制代码@SuppressWarnings("unchecked") |
doInitialize() 方法是实际调用了 Initializer 的 create(context: Context)的地方,其主要逻辑就是通过嵌套调用的方式来完成所有依赖项的初始化,当判断出存在循环依赖的情况时将抛出异常
1 | java复制代码@NonNull |
五、不足点
Startup 的优点我在上边已经列举了,最后再来列举下它的几个不足点
- InitializationProvider 的
onCreate()方法是在主线程被调用的,导致我们的每个 Initializer 默认就都是运行在主线程,这对于某些初始化时间过长,需要运行在子线程的组件来说就不太适用了。且 Initializer 的create(context: Context)方法的本意是完成组件的初始化并返回初始化的结果值,如果在此处通过主动 new Thread 来运行耗时组件的初始化,那么我们就无法返回有意义的结果值,间接导致后续也无法通过 AppInitializer 获取到缓存的初始化结果值 - 如果某组件的初始化需要依赖于其它耗时组件(初始化时间过长,需要运行在子线程)的结果值,此时 Startup 一样不适用
- 对于已经使用 ContentProvider 完成初始化逻辑的第三方依赖库,我们一般也无法直接修改其初始化逻辑(除非 clone 该项目导到本地直接修改源码),所以在初始阶段 Startup 的意义主要在于统一项目本地组件的初始化入口,需要等到 Startup 被大多数开发者接受并使用后,才更加具有性能优势
本文转载自: 掘金