前言
应用程序启动库
StartUp
提供了一种在应用程序启动时初始化组件的简单、高效的方法。库开发者和应用开发者都可以使用app Startup来简化启动序列,并显式设置初始化顺序。
不同于为每个需要初始化的组件定义单独的内容提供程序,App Startup允许你定义共享单个内容提供程序的组件初始化程序。这可以显著提高应用程序的启动时间。
此组件主要解决的痛点问题是:简化各个外部引用SDK初始化操作,并优化SDK初始化时机,在确保启动速度的情况下以更优更稳定的方式按需初始化SDK(当然,也可以利用它的按需按序执行的功能来初始化某些类)。
【本系列文章演示案例均存储在github存储库ArchitecturalComponentExample中】
如何使用
引入依赖
在app/build.gradle
文件的dependencies
中加入以下内容:
implementation "androidx.startup:startup-runtime:1.0.0"
复制代码
若获取依赖包报错,请检查项目目录下build.gradle
中是否包含以下代码:
allprojects {
repositories {
google()
jcenter()
}
}
复制代码
示例项目结构说明
本项目示例主要结构如下:
图中②部分代表用来初始化各个组件SDK的初始化器。
图中①部分代表模拟的需初始化的三方SDK,SDK之间存在”有向无环”的相互依赖关系,依赖关系如下:
上述图中表述的依赖关系为:
- Cache 依赖于 DatabaseProxy 及 Logger
- DatabaseProxy 依赖于 DatabaseHelper 及 Logger
- DatabaseHelper 及 TXMap 均依赖于 Logger
创建初始化器
通过创建一个实现了初始化器接口的类来定义每个组件初始化器。这个接口定义了两个重要的方法:
- create()方法包含初始化组件所需的所有操作,并返回T的实例。
- dependencies()方法,它返回初始化器所依赖的其他初始化器对象的列表。你可以使用这个方法来控制应用程序在启动时运行初始化器的顺序。
依照上述规则,我们来编写Logger的初始化类LoggerInitializer
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的初始化器是如何定义的:
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。
接下来,我们尝试一下上述的两个思考题。
- 当Initializer<?>泛型传入Void会怎样?
此场景类似于:当我们仅单纯的想执行某组件的初始化而没有返回实例的情况
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:
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中声明初始化组件,此处我们暂只关注结果,下一节将介绍声明初始化组件):
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泛型,其均可正常执行,正常适用于仅执行初始化操作,无返回实例的情景。
- 如果dependencies返回值为null会发生什么?
我们将LoggerInitializer的依赖由return Collections.emptyList()
修改为return null
然后执行 (仅编写初始化器的代码仍不能正常工作,还未在AndroidManifest中声明初始化组件,此处我们只关注结果,下一节将介绍声明初始化组件):
// 报错日志
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()方法中列出,它来自一个已经可以发现的初始化器。
此案例中,为了确保应用程序启动时可以发现这些初始化器,请将以下内容添加到清单文件中:
<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
:
<meta-data android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
复制代码
- 在需要初始化的时机调用以下代码,以Logger为例:
AppInitializer.getInstance(context)
.initializeComponent(LoggerInitializer.class);
复制代码
您在条目中使用工具:node=”remove”,而不是简单地删除条目,以确保合并工具也从所有其他合并的清单文件中删除条目。
注意:禁用组件的自动初始化也会禁用该组件的依赖项的自动初始化。
若你想完全禁止组件自动初始化,你可以将InitializationProvider
组件声明为remove
:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
复制代码
并在需要初始化的时机调用以下代码:
AppInitializer.getInstance(context)
.initializeComponent(ExampleLoggerInitializer.class);
复制代码
执行
StartUp的使用仅需完成上述两个步骤即可,让我们来看一下器执行结果:
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中声明的是链尾的初始化器CacheInitializer
及MapInitializer
, 他却是从依赖链的头开始执行的初始化,他是如何做到完全符合依赖链顺序执行的呢?
让我们来简单看一下其原理。
源码分析
上面使用篇中我们埋下了一些伏笔和问题:
- 为什么dependencies返回值不能为null?
- 为什么要将每条独立的链的链尾初始化器传递给InitializationProvider?
- 他是如何做到完全符合依赖链顺序执行的?
为解答这些问题,我们先从初始化的入口类InitializationProvider
看起。
InitializationProvider
@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 维持了一个单例结构,主要包含两个重要方法discoverAndInitialize
及doInitialize
, 分别负责发现初始化器及执行初始化器,我们一一看来。
discoverAndInitialize()
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()
<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() 执行至依赖链的链头的初始化
- 链执行完毕,则移除此链的执行记录
- 链执行完毕,将元数据中声明的初始化器加入至已完成集合中,用以避免重复初始化
那么至此,我们似乎可以解释此节初始留下的三个伏笔问题了。
- 为什么dependencies()返回值不能为null?
答:因为在执行初始化前,会判断当前初始化器的dependencies是否为空,但此时使用的时 List.isEmpty() 并未对dependencies判空,故而,若dependencies()返回null,会导致null.isEmpty()的调用,导致空指针异常。
- 为什么要将每条独立的链的链尾初始化器传递给InitializationProvider?
答:因为可以根据链尾的初始化器可以通过遍历递归dependencies()获取该链所有以来的初始化器,从而找到链首的初始化器,然后从头至尾折叠递归运算,同时也保证了依赖链顺序执行
- 他是如何做到完全符合依赖链顺序执行的?
答同问题2