2020 年 10 月 28 日,JetPack | App Startup 1.0.0 终于迎来正式发布.
应用程序启动库提供了一种在应用程序启动时初始化组件的简单、高效的方法。库开发人员和应用程序开发人员都可以使用应用程序启动来简化启动顺序,并显式设置初始化顺序。
App Startup不需要为每个需要初始化的组件定义单独的内容提供程序,而是允许您定义共享单个内容提供程序的组件初始化程序。这可以显著缩短应用程序启动时间。
目录
前置知识
这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~
- ContentProvider 组件解析: Android | ContentProvider 的工作过程
1. 为什么要使用 App Startup?
这一节,我们来讨论为什么要使用 App Startup ,也就是 App Startup 解决了什么问题。
基于 ContentProvider 启动机制实现的无侵入获取 Contex 的方法:《Android | 使用 ContentProvider 无侵入获取 Context》。在这里我简单复述一下:
- 1、在二方库或三方库中,经常需要获取 Context 进行初始化;
- 2、因为 ContentProvider 会在应用启动的时候初始化,所以很多库都利用了 ContentProvider 的启动机制,在
Application#onCreate()
中进行初始化,例如 LeakCanary 2.4:
AppWatcherInstaller.java
internal sealed class AppWatcherInstaller : ContentProvider() {
internal class MainProcess : AppWatcherInstaller()
internal class LeakCanaryProcess : AppWatcherInstaller()
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
// 其他方法直接 return
}
- 3、这种做法的风险是 ContentProvider 过多,启动过多的 ContentProvider 会增加应用的启动时间。
- 4、AppStartup 的做法是:合并所有用于初始化的ContentProvider ,减少创建 ContentProvider,并提供全局管理。
2. 使用步骤
这一节,我们来总结 App Startup 的使用步骤,依赖如下:
build.gradle
implementation "androidx.startup:startup-runtime:1.0.0"
2.1 为组件实现 Initializer 接口
Initializer
接口是 Startup 封装的组件接口,用于指定组件的初始化逻辑和初始化顺序(也就是依赖关系)。
Initializer.java
public interface Initializer<T> {
1、初始化操作,返回的初始化结果将被缓存
@NonNull
T create(@NonNull Context context);
2、依赖关系,返回值是一个依赖组件的列表
@NonNull
List<Class<? extends Initializer<?>>> dependencies();
}
- 1、
create(...)
初始化操作: 返回的初始化结果将被缓存,其中context
参数是 Application; - 2、
dependencies()
依赖关系: 返回值是一个依赖组件的列表,如果不需要依赖于其它组件,返回一个空列表。App Startup 在初始化当前组件时,会保证所依赖的组件已经完成初始化。
2.2 自动初始化
前面提到,App Startup 合并所有用于初始化的 ContentProvider,合并后的 ContentProvider 就是 InitializationProvider
,我们需要在AndroidManifest
中进行声明,例如:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.ExampleLoggerInitializer"
android:value="androidx.startup" />
</provider>
要点如下:
- 1、组件名必须是
androidx.startup.InitializationProvider
; - 2、需要声明
android:exported="false"
,以限制其他应用访问此组件; - 3、要求
android:authorities
在整个手机唯一,通常使用${applicationId}作为前缀; - 4、需要声明
tools:node="merge"
,确保manifest merger tool
能够正确解析冲突的节点; - 5、meta-data
name
为组件的 Initializer 实现类全限定名,value
为androidx.startup
。
提示: 为什么要将
androidx.startup
设置为value
,而不是name
?因为键值对中,name
是唯一的,而value
是允许重复的。
关于AndroidManifest
中声明组件后,App Startup 是如何自动执行初始化的,我在 第 3 节说。
2.3 手动初始化
在组件需要进行懒加载时(耗时任务),可以进行手动初始化。需要手动初始化的 Initializer 不需要在AndroidManifest
中进行声明,也不应该被其它组件依赖。调用以下方即可进行手动初始化:
AppInitializer.getInstance(context)
.initializeComponent(ExampleLoggerInitializer::class.java)
需要注意的是,App Startup 中会缓存初始化后的结果,重复调用initializeComponent()
不会导致重复初始化。关于 App Startup 手动执行初始化部分的源码分析,我在 第 3 节说。
2.4 取消自动初始化
假如有些库已经使用 第 2.2 节 的方法配置了自动初始化,而我们又希望进行懒加载时,就需要利用manifest merger tool
的合并规则来移除这个库对应的 Initializer。具体如下:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
</provider>
2.5 禁止自动初始化
假如需要禁止 App Startup 自动初始化,同样也需要利用manifest merger tool
的合并规则:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
3. 源码分析
3.1 InitializationProvider 分析
前面我们提到,在AndroidManifest
文件中配置的组件名必须为androidx.startup.InitializationProvider
,现在我们来看这个类的源码:
InitializationProvider.java
已简化
public final class InitializationProvider extends ContentProvider {
@Override
public boolean onCreate() {
Context context = getContext();
if (context != null) {
初始化
AppInitializer.getInstance(context).discoverAndInitialize();
} else {
throw new StartupException("Context cannot be null");
}
return true;
}
@Override
public Cursor query(...) {
throw new IllegalStateException("Not allowed.");
}
@Override
public String getType(...) {
throw new IllegalStateException("Not allowed.");
}
@Nullable
@Override
public Uri insert(...) {
throw new IllegalStateException("Not allowed.");
}
@Override
public int delete(...) {
throw new IllegalStateException("Not allowed.");
}
@Override
public int update(...) {
throw new IllegalStateException("Not allowed.");
}
}
可以看到:
- 1、
InitializationProvider
其实也是利用了 ContentProvider 的启动机制,在ContentProvider#onCreate(...)
中执行初始化; - 2、由于 ContentProvider 的其他方法是没有意义的,所以都抛出了
IllegalStateException
。
3.2 自动初始化源码分析
从一节可以看到,App Startup 在 ContentProvider 中调用了AppInitializer#discoverAndInitialize()
执行自动初始化。AppInitializer
是 App StartUp 框架的核心类,整个 App Startup 框架的代码其实非常少,其中很大部分核心代码都在 AppInitializer 类中。
AppInitializer.java
final Set<Class<? extends Initializer<?>>> mDiscovered;
已简化
void discoverAndInitialize() {
1、获取 androidx.startup.InitializationProvider 组件信息
ComponentName provider = new ComponentName(mContext.getPackageName(), InitializationProvider.class.getName());
ProviderInfo providerInfo = mContext.getPackageManager().getProviderInfo(provider, GET_META_DATA);
2、androidx.startup 字符串
String startup = mContext.getString(R.string.androidx_startup);
3、获取组件信息中的 meta-data 数据
Bundle metadata = providerInfo.metaData;
4、遍历 meta-data 数据
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
Set<String> keys = metadata.keySet();
for (String key : keys) {
String value = metadata.getString(key, null);
4.1 判断 meta-data 数据中,value 为 androidx.startup 的键值对
if (startup.equals(value)) {
Class<?> clazz = Class.forName(key);
4.2 检查指定的类是 Initializer 接口的实现类
if (Initializer.class.isAssignableFrom(clazz)) {
Class<? extends Initializer<?>> component = (Class<? extends Initializer<?>>) clazz;
4.3 将 Class 添加到 mDiscovered Set 中
mDiscovered.add(component);
4.4 初始化此组件
doInitialize(component, initializing);
}
}
}
}
}
-> 4.3 mDiscovered 用于判断组件是否已经自动启动
public boolean isEagerlyInitialized(@NonNull Class<? extends Initializer<?>> component) {
return mDiscovered.contains(component);
}
上面的代码已经非常简化了,主要关注以下几点:
- 1、获取
androidx.startup.InitializationProvider
组件信息(在各个 Module 中声明的组件信息,会在manifest merger tool
的处理下合并); - 2、
androidx.startup
字符串 - 3、获取组件信息中的 meta-data 数据
- 4、遍历 meta-data 数据
- 4.1 判断 meta-data 数据中,value 为 androidx.startup 的键值对
- 4.2 检查指定的类是 Initializer 接口的实现类
- 4.3 将 Class 添加到 mDiscovered Set 中,这将用于后续 判断组件是否已经自动启动
- 4.4 初始化此组件
AppInitializer.java
private static final Object sLock = new Object();
缓存每个组件的初始化结果
final Map<Class<?>, Object> mInitialized;
-> 4.4 初始化此组件
已简化
<T> T doInitialize(Class<? extends Initializer<?>> component, Set<Class<?>> initializing) {
1、对 sLock 加锁,我后文再说。
Object result;
2、判断 initializing 中存在当前组件,说明存在循环依赖
if (initializing.contains(component)) {
String message = String.format("Cannot initialize %s. Cycle detected.", component.getName());
throw new IllegalStateException(message);
}
3、检查当前组件是否已初始化
if (!mInitialized.containsKey(component)) {
3.1 当前组件未初始化
3.1.1 记录正在初始化
initializing.add(component);
3.1.2 通过反射实例化 Initializer 接口实现类
Object instance = component.getDeclaredConstructor().newInstance();
Initializer<?> initializer = (Initializer<?>) instance;
3.1.3 遍历所依赖的组件
List<Class<? extends Initializer<?>>> dependencies = initializer.dependencies();
if (!dependencies.isEmpty()) {
for (Class<? extends Initializer<?>> clazz : dependencies) {
如果所依赖的组件未初始化,递归执行初始化
if (!mInitialized.containsKey(clazz)) {
doInitialize(clazz, initializing); 注意:这里将 initializing 作为参数传入
}
}
}
3.1.4 (到这里,所依赖的组件已经初始化完成)初始化当前组件
result = initializer.create(mContext);
3.1.5 移除正在初始化记录
initializing.remove(component);
3.1.6 缓存初始化结果
mInitialized.put(component, result);
} else {
3.2 当前组件已经初始化,直接返回
result = mInitialized.get(component);
}
return (T) result;
}
上面的代码已经非常简化了,主要关注以下几点:
- 1、对 sLock 加锁,我后文再说。
- 2、判断 initializing 中存在当前组件,说明存在循环依赖(这是因为递归初始化所依赖的组件时,会将 initializing 作为参数传入,如果 initializing 中存在当前组件,说明依赖关系形成回环,如果不抛出异常,将形成无限递归。)
- 3、检查当前组件是否已初始化,如果已经初始化过,则直接返回(3.2),否则:
- 3.1.1 记录正在初始化
- 3.1.2 通过反射实例化 Initializer 接口实现类
- 3.1.3 遍历所依赖的组件,如果所依赖的组件未初始化,递归调用
doInitialize(...)
执行初始化 - 3.1.4 (到这里,所依赖的组件已经初始化完成)初始化当前组件
- 3.1.5 移除正在初始化记录
- 3.1.6 缓存初始化结果
3.3 手动初始化源码分析
现在我们来看手动初始化(懒加载)的源码分析:
AppInitializer.java
public <T> T initializeComponent(@NonNull Class<? extends Initializer<T>> component) {
调用 doInitialize(...) 方法:
return doInitialize(component, new HashSet<Class<?>>());
}
其实非常简单,就是调用上一节的doInitialize(...)
执行初始化。需要注意的是,这个方法是允许在子线程调用的,换句话说,自动初始化与手动初始化是存在线程同步问题的,那么 App Startup 是如何解决的呢?
还记得我们前面有一个sLock
没有说吗?其实它就是用来保证线程同步的锁:
AppInitializer.java
<T> T doInitialize(Class<? extends Initializer<?>> component, Set<Class<?>> initializing) {
1、对 sLock 加锁
synchronized (sLock) {
...
}
}
4. 总结
-
优点:使用 App Startup 框架,可以简化启动序列并显式设置初始化依赖顺序,在简单、高效这方面,App Startup 基本满足需求。
-
不足:App Startup 框架的不足也是因为它太简单了,提供的特性太过简单,往往并不能完美契合商业化需求。例如以下特性 App Startup 就无法满足:
- 缺乏异步等待:同步等待指的是在当前线程先初始化所依赖的组件,再初始化当前组件,App Startup 是支持的,但是异步等待就不支持了。举个例子,所依赖的组件需要执行一个耗时的异步任务才能完成初始化,那么 App Startup 就无法等待异步任务返回;
- 缺乏依赖回调:当前组件所依赖的组件初始化完成后,未发出回调。
参考资料
- 《App Startup》 —— Android Developers
- 《合并多个清单文件》 —— Android Developers
- 《AndroidX: App Startup》 —— Husayn Hakeem 著
- 《Jetpack新成员,App Startup 一篇就懂》 —— 郭霖 著
- 《我为何弃用 Jetpack 的 App Startup?》 —— 午后一小憩 著
- 《更快!这才是我想要的 Android Startup 库!》 —— idisfkj 著
- 《组件化:代码隔离也难不倒组件的按序初始化》 —— leobert-lan 著
- 《从源码看 Jetpack(5)Startup 源码详解》 —— 叶志陈 著
为何弃用Jetpack的App Startup?
官方文档中只提到了可以通过一个ContentProvider
来统一管理需要初始化的组件,同时通过dependencies()
方法解决组件间初始化的依赖顺序,然后呢?没了?等等官方你是不是漏了什么?
异步处理呢?虽然我们可以在create()
方法中手动创建子线程进行异步任务,但一个异步任务依赖另一个异步任务又该如何处理呢?多个异步任务完成之后,统一逻辑处理又在哪里呢?依赖任务完成后的回调又在哪里?亦或者是依赖任务完成后的通知?
如果你的项目都是同步初始化的话,并且使用到了多个ContentProvider
,App Startup
可能有一定的优化空间,毕竟统一到了一个ContentProvider
中,同时支持了简单的顺序依赖。
值得一提的是,App Startup
中只提供了使用反射来获取初始化的组件实例,这对于一些没有过多依赖的初始化项目来说,盲目使用App Startup
来优化是否会对启动速度进一步造成影响呢?
坚持一下,就有了下面这个库,App Startup
的进阶版Android Startup
。
Android Startup
Android Startup
提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用Android Startup
来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。 与此同时,Android Startup
支持同步与异步等待,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。
由于Android Startup
是基于App Startup
进行的扩展,所以它的使用方式与App Startup
有点类似,该有的功能基本上都有,同时额外还附加其它功能。
下面是一张与google的App Startup功能对比的表格。
下面简单介绍一下Android Startup
的使用。
添加依赖
将下面的依赖添加到build.gradle
文件中:
dependencies {
implementation 'com.rousetime.android:android-startup:1.0.1'
}
依赖版本的更新信息: Release
快速使用
android-startup提供了两种使用方式,在使用之前需要先定义初始化的组件。
定义初始化的组件
每一个初始化的组件都需要实现AndroidStartup<T>
抽象类,它实现了Startup<T>
接口,它主要有以下四个抽象方法:
callCreateOnMainThread(): Boolean
用来控制create()
方法调时所在的线程,返回true代表在主线程执行。waitOnMainThread(): Boolean
用来控制当前初始化的组件是否需要在主线程进行等待其完成。如果返回true,将在主线程等待,并且阻塞主线程。create(): T?
组件初始化方法,执行需要处理的初始化逻辑,支持返回一个T
类型的实例。dependencies(): List<Class<out Startup<*>>>?
返回Startup<*>
类型的list集合。用来表示当前组件在执行之前需要依赖的组件。
例如,下面定义一个SampleFirstStartup
类来实现AndroidStartup<String>
抽象类:
class SampleFirstStartup : AndroidStartup<String>() {
override fun callCreateOnMainThread(): Boolean = true
override fun waitOnMainThread(): Boolean = false
override fun create(context: Context): String? {
// todo something
return this.javaClass.simpleName
}
override fun dependencies(): List<Class<out Startup<*>>>? {
return null
}
}
因为SampleFirstStartup
在执行之前不需要依赖其它组件,所以它的dependencies()
方法可以返回空,同时它会在主线程中执行。
注意:️虽然waitOnMainThread()
返回了false
,但由于它是在主线程中执行,而主线程默认是阻塞的,所以callCreateOnMainThread()
返回true
时,该方法设置将失效。
假设你还需要定义SampleSecondStartup
,它依赖于SampleFirstStartup
。这意味着在执行SampleSecondStartup
之前SampleFirstStartup
必须先执行完毕。
class SampleSecondStartup : AndroidStartup<Boolean>() {
override fun callCreateOnMainThread(): Boolean = false
override fun waitOnMainThread(): Boolean = true
override fun create(context: Context): Boolean {
// 模仿执行耗时
Thread.sleep(5000)
return true
}
override fun dependencies(): List<Class<out Startup<*>>>? {
return listOf(SampleFirstStartup::class.java)
}
}
在dependencies()
方法中返回了SampleFirstStartup
,所以它能保证SampleFirstStartup
优先执行完毕。 它会在子线程中执行,但由于waitOnMainThread()
返回了true
,所以主线程会阻塞等待直到它执行完毕。
例如,你还定义了SampleThirdStartup与SampleFourthStartup
Manifest中自动配置
第一种初始化方法是在Manifest中进行自动配置。
在Android Startup中提供了StartupProvider
类,它是一个特殊的content provider,提供自动识别在manifest中配置的初始化组件。 为了让其能够自动识别,需要在StartupProvider
中定义<meta-data>
标签。其中的name
为定义的组件类,value
的值对应为android.startup
。
<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${applicationId}.android_startup"
android:exported="false">
<meta-data
android:name="com.rousetime.sample.startup.SampleFourthStartup"
android:value="android.startup" />
</provider>
你不需要将SampleFirstStartup
、SampleSecondStartup
与SampleThirdStartup
添加到<meta-data>
标签中。这是因为在SampleFourthStartup
中,它的dependencies()
中依赖了这些组件。StartupProvider
会自动识别已经声明的组件中依赖的其它组件。
Application中手动配置
第二种初始化方法是在Application进行手动配置。
手动初始化需要使用到StartupManager.Builder()
。
例如,如下代码使用StartupManager.Builder()
进行初始化配置。
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
StartupManager.Builder()
.addStartup(SampleFirstStartup())
.addStartup(SampleSecondStartup())
.addStartup(SampleThirdStartup())
.addStartup(SampleFourthStartup())
.build(this)
.start()
.await()
}
}
如果你开启了日志输出,然后运行项目之后,将会在控制台中输出经过拓扑排序优化之后的初始化组件的执行顺序。
D/StartupTrack: TopologySort result:
================================================ ordering start ================================================
order [0] Class: SampleFirstStartup => Dependencies size: 0 => callCreateOnMainThread: true => waitOnMainThread: false
order [1] Class: SampleSecondStartup => Dependencies size: 1 => callCreateOnMainThread: false => waitOnMainThread: true
order [2] Class: SampleThirdStartup => Dependencies size: 2 => callCreateOnMainThread: false => waitOnMainThread: false
order [3] Class: SampleFourthStartup => Dependencies size: 3 => callCreateOnMainThread: false => waitOnMainThread: false
================================================ ordering end ================================================
完整的代码实例,你可以通过查看app获取。
更多
可选配置
LoggerLevel
: 控制Android Startup中的日志输出,可选值包括LoggerLevel.NONE
,LoggerLevel.ERROR
andLoggerLevel.DEBUG
。AwaitTimeout
: 控制Android Startup中主线程的超时等待时间,即阻塞的最长时间。
Manifest中配置
使用这些配置,你需要定义一个类去实现StartupProviderConfig
接口,并且实现它的对应方法。
class SampleStartupProviderConfig : StartupProviderConfig {
override fun getConfig(): StartupConfig =
StartupConfig.Builder()
.setLoggerLevel(LoggerLevel.DEBUG)
.setAwaitTimeout(12000L)
.build()
}
与此同时,你还需要在manifest中进行配置StartupProviderConfig
。
<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${applicationId}.android_startup"
android:exported="false">
<meta-data
android:name="com.rousetime.sample.startup.SampleStartupProviderConfig"
android:value="android.startup.provider.config" />
</provider>
经过上面的配置,StartupProvider
会自动解析SampleStartupProviderConfig
。
Application中配置
在Application需要借助StartupManager.Builder()
进行配置。
override fun onCreate() {
super.onCreate()
val config = StartupConfig.Builder()
.setLoggerLevel(LoggerLevel.DEBUG)
.setAwaitTimeout(12000L)
.build()
StartupManager.Builder()
.setConfig(config)
...
.build(this)
.start()
.await()
}
方法
AndroidStartup
createExecutor(): Executor
: 如果定义的组件没有运行在主线程,那么可以通过该方法进行控制运行的子线程。onDependenciesCompleted(startup: Startup<*>, result: Any?)
: 该方法会在每一个依赖执行完毕之后进行回调。
实战测试
AwesomeGithub中使用了Android Startup,优化配置的初始化时间与组件化开发的配置注入时机,使用前与使用后时间对比:
AwesomeGithub
AwesomeGithub是基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。
除了Android原生版本,还有基于Flutter的跨平台版本flutter_github。