点赞关注,不再迷路,你的支持对我意义重大!
Hi,我是丑丑。本文「Android 路线」| 导读 —— 从零到无穷大 已收录。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
前言
- 2020 年 10 月 28 日,JetPack | App Startup 1.0.0 终于迎来正式发布,正好最近在总结组件化架构专题,所以也专门学习下 App Startup 的工作原理;
- 在这篇文章里,我将带你总结 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 {
1、初始化操作,返回的初始化结果将被缓存
@NonNull
T create(@NonNull Context context);
2、依赖关系,返回值是一个依赖组件的列表
@NonNull
List>> dependencies();
}
- 1、
create(...)
初始化操作: 返回的初始化结果将被缓存,其中context
参数是 Application; - 2、
dependencies()
依赖关系: 返回值是一个依赖组件的列表,如果不需要依赖于其它组件,返回一个空列表。App Startup 在初始化当前组件时,会保证所依赖的组件已经完成初始化。
2.2 自动初始化
前面提到,App Startup 合并所有用于初始化的 ContentProvider,合并后的 ContentProvider 就是 InitializationProvider
,我们需要在AndroidManifest
中进行声明,例如:
要点如下:
- 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。具体如下:
2.5 禁止自动初始化
假如需要禁止 App Startup 自动初始化,同样也需要利用manifest merger tool
的合并规则:
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>> 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> initializing = new HashSet<>();
Set 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, Object> mInitialized;
-> 4.4 初始化此组件
已简化
T doInitialize(Class extends Initializer>> component, Set> 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>> 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 initializeComponent(@NonNull Class extends Initializer> component) {
调用 doInitialize(...) 方法:
return doInitialize(component, new HashSet>());
}
其实非常简单,就是调用上一节的doInitialize(...)
执行初始化。需要注意的是,这个方法是允许在子线程调用的,换句话说,自动初始化与手动初始化是存在线程同步问题的,那么 App Startup 是如何解决的呢?
还记得我们前面有一个sLock
没有说吗?其实它就是用来保证线程同步的锁:
AppInitializer.java
T doInitialize(Class extends Initializer>> component, Set> 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 源码详解》 —— 叶志陈 著
创作不易,你的「三连」是丑丑最大的动力,我们下次见!