Jetpack之应用启动组件-StartUp

2021年02月24日 99 字 Jetpack


前言

应用程序启动库StartUp提供了一种在应用程序启动时初始化组件的简单、高效的方法。库开发者和应用开发者都可以使用app Startup来简化启动序列,并显式设置初始化顺序。

不同于为每个需要初始化的组件定义单独的内容提供程序,App Startup允许你定义共享单个内容提供程序的组件初始化程序。这可以显著提高应用程序的启动时间。

此组件主要解决的痛点问题是:简化各个外部引用SDK初始化操作,并优化SDK初始化时机,在确保启动速度的情况下以更优更稳定的方式按需初始化SDK(当然,也可以利用它的按需按序执行的功能来初始化某些类)。

【本系列文章演示案例均存储在github存储库ArchitecturalComponentExample中】

如何使用

引入依赖

app/build.gradle文件的dependencies中加入以下内容:

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

若获取依赖包报错,请检查项目目录下build.gradle中是否包含以下代码:

1
2
3
4
5
6
allprojects {
repositories {
google()
jcenter()
}
}

示例项目结构说明

本项目Java版本地址

本项目Kotlin版本地址

最简实践请参考官方开发文档

本项目示例主要结构如下:

yL3Fz9.png

图中②部分代表用来初始化各个组件SDK的初始化器。

图中①部分代表模拟的需初始化的三方SDK,SDK之间存在”有向无环”的相互依赖关系,依赖关系如下:

yXhL40.png

上述图中表述的依赖关系为:

  • Cache 依赖于 DatabaseProxy 及 Logger
  • DatabaseProxy 依赖于 DatabaseHelper 及 Logger
  • DatabaseHelper 及 TXMap 均依赖于 Logger

创建初始化器

通过创建一个实现了初始化器接口的类来定义每个组件初始化器。这个接口定义了两个重要的方法:

  • create()方法包含初始化组件所需的所有操作,并返回T的实例。
  • dependencies()方法,它返回初始化器所依赖的其他初始化器对象的列表。你可以使用这个方法来控制应用程序在启动时运行初始化器的顺序。

依照上述规则,我们来编写Logger的初始化类LoggerInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LoggerInitializer implements Initializer<Logger> {

@NonNull
@Override
public Logger create(@NonNull Context context) {
Log.i("loglog", "create: LoggerInitializer");
Logger.initialize();
return Logger.getInstance();
}

@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
// 预示没有其他依赖需要初始化
return Collections.emptyList();
}
}
  • 首先初始化器需要实现Initializer接口,并传入需初始化的对象的泛型,思考如果不传或传入Void会怎样?
  • 实现create方法,此方法中完成组件初始化,如有需要可返回实例
  • 实现dependencies方法,此方法中声明当前初始化组件依赖于其他组件,并返回被依赖组件的初始化器,此处Logger未依赖其他组件,故返回空数组,思考如果返回null会怎样?

类似的,我们根据组件依赖关系依次定义出各个组件的初始化器,对比上述无依赖的Logger,我们再来看看多个依赖的DatabaseProxy的初始化器是如何定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DatabaseProxyInitializer implements Initializer<DatabaseProxy> {
@NonNull
@Override
public DatabaseProxy create(@NonNull Context context) {
Log.i("loglog", "create: DatabaseProxyInitializer");
DatabaseProxy.initialize(DatabaseHelper.getInstance());
return DatabaseProxy.getInstance();
}

@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
return Arrays.asList(LoggerInitializer.class, DatabaseHelperInitializer.class);
}
}

我们看到在dependencies中声明了DatabaseProxy依赖于Logger和DatabaseHelper。

接下来,我们尝试一下上述的两个思考题。

  1. 当Initializer<?>泛型传入Void会怎样?

此场景类似于:当我们仅单纯的想执行某组件的初始化而没有返回实例的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SomethingInitializer implements Initializer<Void> {

@NonNull
@Override
public Void create(@NonNull Context context) {
Log.i("loglog", "create: SomethingInitializer");
return null;
}

@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
return Collections.emptyList();
}
}

此时,我们让TXMap的初始化器去依赖这SomethingInitializer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MapInitializer implements Initializer<TXMap> {
@NonNull
@Override
public TXMap create(@NonNull Context context) {
Log.i("loglog", "create: MapInitializer");
return new TXMap();
}

@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
// map依赖了log,现在我们让它依赖Something
return Arrays.asList(LoggerInitializer.class, SomethingInitializer.class);
}
}

让我们看看其执行结果 (当然,仅编写初始化器的代码仍不能正常工作,还未在AndroidManifest中声明初始化组件,此处我们暂只关注结果,下一节将介绍声明初始化组件)

1
2
3
4
5
I/tag: create: LoggerInitializer
I/tag: Logger initialized
I/tag: create: SomethingInitializer
I/tag: create: MapInitializer
I/tag: Map initialized

我们可以看到,其依旧正常按需执行了初始化操作,先调用了被TXMap依赖的Logger及Something的初始化器,最终完成Map的初始化。说明,Initializer<?>中有无传入泛型及传入Void泛型,其均可正常执行,正常适用于仅执行初始化操作,无返回实例的情景。

  1. 如果dependencies返回值为null会发生什么?

我们将LoggerInitializer的依赖由return Collections.emptyList()修改为return null然后执行 (仅编写初始化器的代码仍不能正常工作,还未在AndroidManifest中声明初始化组件,此处我们只关注结果,下一节将介绍声明初始化组件)

1
2
3
4
5
// 报错日志
java.lang.RuntimeException: Unable to get provider androidx.startup.InitializationProvider:
androidx.startup.StartupException:
androidx.startup.StartupException:
java.lang.NullPointerException: Attempt to invoke interface method 'boolean java.util.List.isEmpty()' on a null object reference

我们发现程序报错了,故dependencies返回值不能为null,至于原因将在原理篇中具体说明,记住此处【伏笔1】。

注册初始化器提供者

上文提到,仅编写初始化器并不能完成组件初始化,显然的,我们没有配置其执行的时机。

应用程序启动包括一个叫做InitializationProvider的特殊内容提供程序,它用来发现和调用你的组件初始化器。应用启动时通过首先检查InitializationProvider清单项下的项来发现组件初始化器。然后,App Startup为它已经发现的任何初始化器调用dependencies()方法。
这意味着,为了让组件初始化器在应用启动时被发现,必须满足以下条件之一:

  • 组件初始化器在InitializationProvider清单项下有一个对应的项。
  • 组件初始化器在dependencies()方法中列出,它来自一个已经可以发现的初始化器。

此案例中,为了确保应用程序启动时可以发现这些初始化器,请将以下内容添加到清单文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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.tinlone.startupexamplejava.initializers.CacheInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.tinlone.startupexamplejava.initializers.MapInitializer"
android:value="androidx.startup" />
</provider>

上述代码将初始化器传递给初始化提供者。

若初始化链有多个,类似本示例中包含:

  • Logger -> TXMap
  • Logger -> DatabaseHelper -> DatabaseProxy

以上两条独立的链,故此处为每条独立的链的链尾初始化器传递给InitializationProvider,为何要这样写呢?我们也将在源码篇中具体探讨,记住此处【伏笔2】.

手动初始化组件

当然,如果你不想InitializationProvider自动初始化某些组件,你也可以手动调用初始化流程,此时你应该这样做:

  • 将不需要初始化的组件标记为remove:
1
2
<meta-data android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
  • 在需要初始化的时机调用以下代码,以Logger为例:
1
2
AppInitializer.getInstance(context)
.initializeComponent(LoggerInitializer.class);

您在条目中使用工具:node=”remove”,而不是简单地删除条目,以确保合并工具也从所有其他合并的清单文件中删除条目。

注意:禁用组件的自动初始化也会禁用该组件的依赖项的自动初始化。

若你想完全禁止组件自动初始化,你可以将InitializationProvider组件声明为remove

1
2
3
4
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />

并在需要初始化的时机调用以下代码:

1
2
AppInitializer.getInstance(context)
.initializeComponent(ExampleLoggerInitializer.class);

执行

StartUp的使用仅需完成上述两个步骤即可,让我们来看一下器执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
I/loglog: create: LoggerInitializer
I/loglog: Logger initialized
I/loglog: create: SomethingInitializer
I/loglog: create: MapInitializer
I/loglog: Map initialized
I/loglog: create: DatabaseHelperInitializer
I/loglog: DatabaseHelper initialized
I/loglog: create: DatabaseProxyInitializer
I/loglog: DatabaseHelper initialized
I/loglog: DatabaseProxy initialized
I/loglog: create: CacheInitializer
I/loglog: Cache initialized

我们在AndroidManifest中声明的是链尾的初始化器CacheInitializerMapInitializer, 他却是从依赖链的头开始执行的初始化,他是如何做到完全符合依赖链顺序执行的呢?
让我们来简单看一下其原理。

源码分析

上面使用篇中我们埋下了一些伏笔和问题:

  • 为什么dependencies返回值不能为null?
  • 为什么要将每条独立的链的链尾初始化器传递给InitializationProvider?
  • 他是如何做到完全符合依赖链顺序执行的?

为解答这些问题,我们先从初始化的入口类InitializationProvider看起。

InitializationProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestrictTo(RestrictTo.Scope.LIBRARY)
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;
}
// something else ...
}

InitializationProvider内容相对简单,其作为内容提供者在初始化时调用了 AppInitializer.getInstance(context).discoverAndInitialize()去扫描初始化器配置并执行后续操作。

AppInitializer

AppInitializer 维持了一个单例结构,主要包含两个重要方法discoverAndInitializedoInitialize, 分别负责发现初始化器及执行初始化器,我们一一看来。

discoverAndInitialize()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void discoverAndInitialize() {
try {
Trace.beginSection(SECTION_NAME);
// 创建组件标识符,获取InitializationProvider信息
ComponentName provider = new ComponentName(mContext.getPackageName(),
InitializationProvider.class.getName());
// 根据组件标识符从包管理器中取得组件,并获取组件元数据
ProviderInfo providerInfo = mContext.getPackageManager()
.getProviderInfo(provider, GET_META_DATA);
// 取得元数据
Bundle metadata = providerInfo.metaData;
String startup = mContext.getString(R.string.androidx_startup);
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
// 取出元数据中 key的集合, 此处metadata的本质实为ArrayMap,参见{@link Bundle#keySet}
Set<String> keys = metadata.keySet();
for (String key : keys) {
String value = metadata.getString(key, null);
// 校验是否为androidx_startup元数据
if (startup.equals(value)) {
// 根据元数据key中的全类名生成字节码对象
Class<?> clazz = Class.forName(key);
// 校验字节码对象是否是 Initializer 的实现类
if (Initializer.class.isAssignableFrom(clazz)) {
Class<? extends Initializer<?>> component =
(Class<? extends Initializer<?>>) clazz;
// 加入Set备用
mDiscovered.add(component);
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();
}
}

由代码可知,其主要做了以下事情:

  • 创建组件标识符,获取InitializationProvider信息
  • 根据组件标识符从包管理器中取得组件,并获取组件元数据
  • 取得元数据, 取出元数据中 key的集合
  • 遍历元数据集合(ArrayMap)中的数据,校验是否为androidx_startup元数据
  • 根据元数据key中的全类名生成字节码对象,校验字节码对象是否是 Initializer 的实现类
  • 将Initializer 的实现类 字节码对象加入变量mDiscovered的Set中备用
  • 执行初始化
doInitialize()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<T> T doInitialize(
@NonNull Class<? extends Initializer<?>> component,
@NonNull Set<Class<?>> initializing) {
synchronized (sLock) {
boolean isTracingEnabled = Trace.isEnabled();
try {
if (isTracingEnabled) {
// 这里使用simpleName,因为否则节名会太大。
Trace.beginSection(component.getSimpleName());
}
// 保证初始化链无环,initializing为执行记录集合
// 从discoverAndInitialize中传递的initializing值为空,故此判断不会执行
// 从本函数递归执行传递的是已包含执行的初始化器,若此时初始化器有重复,说明相互依赖关系成环了,应抛错
if (initializing.contains(component)) {
String message = String.format(
"Cannot initialize %s. Cycle detected.", component.getName()
);
throw new IllegalStateException(message);
}
Object result;
// 若(被多次依赖且)已初始化则跳过
if (!mInitialized.containsKey(component)) {
// 将需要执行的初始化器字节码对象加入执行记录中
initializing.add(component);
try {
// 获取初始化器实例
Object instance = component.getDeclaredConstructor().newInstance();
Initializer<?> initializer = (Initializer<?>) instance;
// 获取初始化器依赖关系
List<Class<? extends Initializer<?>>> dependencies =
initializer.dependencies();
// 若此初始化器有相关依赖
// 是否记得【伏笔1】有提到为何dependencies不能返回null吗?
// 此处dependencies取得的值并未作空判断,故 null.isEmpty()会报空指针异常
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()));
}
// 执行完毕,移除记录
initializing.remove(component);
// 记录初始化完成
mInitialized.put(component, result);
} catch (Throwable throwable) {
throw new StartupException(throwable);
}
} else {
result = mInitialized.get(component);
}
// 返回执行结果
return (T) result;
} finally {
Trace.endSection();
}
}
}

由代码可知,其主要做了以下事情:

  • 首先根据单链执行记录判断此链中是否有依赖关系成环,成环则抛错
  • 根据初始换执行完成的记录判断(被多次依赖的)依赖是否已被初始化,已初始化则跳过
  • 将需要执行的初始化器字节码对象加入执行记录中备用,用以判断依赖链中是否成环
  • 调用 initializer.dependencies() 获取初始化器依赖关系
  • 若此初始化器有其他依赖则遍历依赖递归调用 doInitialize() 执行至依赖链的链头的初始化
  • 链执行完毕,则移除此链的执行记录
  • 链执行完毕,将元数据中声明的初始化器加入至已完成集合中,用以避免重复初始化

那么至此,我们似乎可以解释此节初始留下的三个伏笔问题了。

  1. 为什么dependencies()返回值不能为null?

答:因为在执行初始化前,会判断当前初始化器的dependencies是否为空,但此时使用的时 List.isEmpty() 并未对dependencies判空,故而,若dependencies()返回null,会导致null.isEmpty()的调用,导致空指针异常。

  1. 为什么要将每条独立的链的链尾初始化器传递给InitializationProvider?

答:因为可以根据链尾的初始化器可以通过遍历递归dependencies()获取该链所有以来的初始化器,从而找到链首的初始化器,然后从头至尾折叠递归运算,同时也保证了依赖链顺序执行

  1. 他是如何做到完全符合依赖链顺序执行的?

答同问题2