请点赞关注,你的支持对我意义重大。

Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

大家好,我是小彭。

过去两年,我们在掘金平台上发布 JetPack 专栏文章,小彭也受到了大家的意见和鼓励。最近,小彭会陆续搬运到公众号上。

2020 年 10 月 28 日,JetPack | App Startup 1.0.0 终于迎来正式发布,正好最近在总结组件化架构专题,所以也专门学习下 App Startup 的工作原理。在这篇文章里,我将带你总结 App Startup 的使用方法 & 实现原理 & 源码分析。有用请点赞给 Star,给小彭一点创作的动力,谢谢。


这篇文章是 Jetpack 系列文章第 13 篇,专栏文章列表:

二、其他:

  • 1、AppStartup:轻量级初始化框架(本文)
  • 2、DataStore:新一代键值对存储方案
  • 3、Room:ORM 数据库访问框架
  • 4、WindowManager:加强对多窗口模式的支持
  • 5、WorkManager:加强对后台任务的支持
  • 6、Compose:新一代视图开发方案

学习路线图:


1. 认识 AppStartup

1.1 App Startup 解决了什么问题?

App Startup 是 Google 提供的 Android 轻量级初始化框架:

  • 优点:使用 App Startup 框架,可以简化启动序列并显式设置初始化依赖顺序,在简单、高效这方面,App Startup 基本满足需求。
  • 不足:App Startup 框架的不足也是因为它太简单了,提供的特性太过简单,往往并不能完美契合商业化需求。例如以下特性 App Startup 就无法满足:
    • 缺乏异步等待: 同步等待指的是在当前线程先初始化所依赖的组件,再初始化当前组件,App Startup 是支持的,但是异步等待就不支持了。举个例子,所依赖的组件需要执行一个耗时的异步任务才能完成初始化,那么 App Startup 就无法等待异步任务返回;
    • 缺乏依赖回调: 当前组件所依赖的组件初始化完成后,未发出回调。

1.2 App Startup 如何实现自动初始化?

App Startup 利用了 ContentProvider 在应用启动的时候初始化的特性,提供了一个自定义 ContentProvider 来实现自动初始化。很多库都利用了 ContentProvider 的启动机制,来实现无侵入初始化,例如 LeakCanary 等

使用 AppStartup 还能够合并所有用于初始化的 ContentProvider ,减少创建 ContentProvider,并提供全局管理。

App Startup 示意图

详细的源码分析下文内容。


2. App Startup 使用方法

这一节,我们来总结 App Startup 的使用步骤。

2.1 基本用法

  • 1、添加依赖

在模块级 build.gradle 添加依赖:

模块级 build.gradle

implementation "androidx.startup:startup-runtime:1.0.0"
  • 2、实现 Initializer 接口

Initializer 接口是 App Startup 定义组件接口,用于指定组件的初始化逻辑和初始化顺序(也就是依赖关系),接口定义如下:

  • 1、create(...) 初始化操作: 返回的初始化结果将被缓存,其中 context 参数就是当前进程的 Application 对象;
  • 2、dependencies() 依赖关系: 返回值是一个依赖组件的列表,如果不需要依赖于其它组件,返回一个空列表。App Startup 在初始化当前组件时,会保证所依赖的组件已经完成初始化。

Initializer.java

public interface Initializer<T> {

    // 1、初始化操作,返回值将被缓存??
@NonNull
T create(@NonNull Context context); // 2、依赖关系,返回值是一个依赖组件的列表
@NonNull
List<Class<? extends Initializer<?>>> dependencies();
}

示例程序

// LeakCanary 2.9.1
internal class AppWatcherStartupInitializer : Initializer<AppWatcherStartupInitializer> {
override fun create(context: Context) = apply {
// 实现初始化操作
val application = context.applicationContext as Application
AppWatcher.manualInstall(application)
} override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}
  • 3、配置

在 Manifest 文件中将 Initializer 实现类配置到 androidx.startup.InitializationProvider<meta-data> 中。

示例程序

<!-- LeakCanary 2.9.1 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge"> <meta-data
android:name="leakcanary.internal.AppWatcherStartupInitializer"
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 android:name 为组件的 Initializer 实现类的全限定类名,android:value 固定为 androidx.startup

提示: 为什么要将 androidx.startup 设置为 value,而不是 name?因为在键值对中,name 是唯一的,而 value 是允许重复的,将 androidx.startup 放到 value 的话才能允许同时配置多个相同语义的 <meta-data>

至此,App Startup 基本的使用与配置完成,在应用启动时,App Startup 会自动收集各个模块配置的 Initializer 实现类,并按照依赖顺序依次执行。

2.2 进阶用法

  • 1、手动初始化

当你的组件需要进行手动初始化,而不是自动初始化时(例如存在耗时任务),可以进行手动初始化,而且手动初始化是可以在子线程调用的,而自动初始化均是在主线程执行的。

  • App Startup 中会缓存初始化后的结果,重复调用 initializeComponent() 也不会导致重复初始化;
  • 要手动初始化的 Initializer 实现类不能在声明到 AndroidManifest 中,也不能被其它组件依赖,否则它依然会自动初始化。

调用以下方即可进行手动初始化:

示例程序

AppInitializer.getInstance(context).initializeComponent(ExampleLoggerInitializer::class.java)
  • 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>
  • 3、禁用 App Startup

假如需要完全禁用 App Startup 自动初始化,同样也可以利用到 manifest merger tool 的合并规则:

示例程序

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

3. App Startup 原理分析

3.1 App Startup 如何实现自动初始化?

App Startup 利用了 ContentProvider 的启动机制实现自动初始化。ContentProvider 通常的用法是为当前进程 / 远程进程提供内容服务,它们会在应用启动的时候初始化。利用这个特性,App Startup 的方案就是自定义一个 ContentProvider 的实现类 InitializationProvider,在 onCreate(…) 方法中执行初始化逻辑。

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.");
}
}

由于 ContentProvider 的其他方法是没有意义的,所以都抛出了 IllegalStateException

3.2 说一下 App Startup 的初始化过程

从上一节可以看到,App Startup 在 InitializationProvider 中调用了AppInitializer#discoverAndInitialize()执行自动初始化。AppInitializer是 App StartUp 框架的核心类,整个 App Startup 框架的代码其实非常少,其中很大部分核心代码都在 AppInitializer 类中。

我将整个自动初始化过程概括为 3 个阶段:

  • 步骤 1 - 获取 数据: 扫描 Manifest 中定义在 InitializationProvider 里面的 数据,从中筛选出 Initializer 的配置信息;
  • 步骤 2 - 递归执行初始化器: 通过 Initializer#create() 执行每个初始化器的逻辑,并且会通过 Initializer#dependencies() 优先保证依赖项已经初始化;
  • 步骤 3 - 缓存初始化结果: 将初始化后的结果缓存到映射表中,避免重复初始化。

源码摘要如下:

AppInitializer.java

private static final Object sLock = new Object(); // 后面会提到

// 记录扫描 <meta-data> 得到的初始化器(可用于判断组件是否已经自动启动)
final Set<Class<? extends Initializer<?>>> mDiscovered; // 缓存每个组件的初始化结果
final Map<Class<?>, Object> mInitialized; 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 筛选 value 为 androidx.startup 的 meta-data 数据中
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);
} // -> 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)) {
// 注意:这里将 initializing 作为参数传入,用于判断循环依赖
doInitialize(clazz, 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;
}

3.3 手动初始化的执行过程

前面我们提到使用 initializeComponent() 方法可以手动初始化,我们来看手动初始化(懒加载)的源码:

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 只是一个轻量级的初始化框架,能做的事情有限。市面上有开发者开源了基于 DAU 有向无环图的初始化框架,这个我们下次再说。关注我,带你了解更多。


参考资料

我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。

食之无味?App Startup 可能比你想象中要简单的更多相关文章

  1. eclipse java项目中明明引入了jar包 为什么项目启动的时候不能找到jar包 项目中已经 引入了 com.branchitech.app 包 ,但时tomcat启动的时候还是报错? java.lang.ClassNotFoundException: com.branchitech.app.startup.AppStartupContextListener java.lang.ClassN

    eclipse java项目中明明引入了jar包 为什么项目启动的时候不能找到jar包 项目中已经 引入了 com.branchitech.app 包 ,但时tomcat启动的时候还是报错?java. ...

  2. 探究 | App Startup真的能减少启动耗时吗

    前言 之前我们说了启动优化的一些常用方法,但是有的小伙伴就很不屑了: "这些方法很久之前就知道了,不知道说点新东西?比如App Startup?能对启动优化有帮助吗?" ok,既然 ...

  3. Jetpack架构组件学习(4)——APP Startup库的使用

    最近在研究APP的启动优化,也是发现了Jetpack中的App Startup库,可以进行SDK的初始化操作,于是便是学习了,特此记录 原文:Jetpack架构组件学习(4)--App Startup ...

  4. 修改和获取web.config或app.config文件appSettings配置节中的Add里的value属性 函数

    1: /// <summary> 2: /// 修改web.config或app.config文件appSettings配置节中的Add里的value属性 3: /// </summ ...

  5. 下载企业级证书打包的app 出现“正在下载”或“等待中”的图标并且无法删除的问题

    下载企业级证书打包的app 出现“正在下载”或“等待中”的图标并且无法删除的问题: 原因分析:手机上的bundleid 与后台plist文件中的bundleid不一致导致的. 解决方案:用plist文 ...

  6. repo: 创建local manifest以及如何添加app到CM/Android build系统中

    The local manifest Creating a local manifest allows you to customize the list of repositories on you ...

  7. SpriteBuilder中如何简单的重置APP保存的数据

    在任意一款APP中,我们可能需要在磁盘上保存一些游戏数据,以便在下一次运行APP时恢复游戏数据. 但是由于在测试阶段,我们需要快速恢复初始状态的游戏数据,该如何做呢? 非常简单,只需要将APP从真机或 ...

  8. Android购物车的实现,仿淘宝天猫京东等APP。处理RecyclerView或listview中的选中事件;

    很久之前的代码了,拉出来晾晾! 购物车大致思路: 分为:商品.店铺.全选: 商品全部选中后--店铺自动选中:商品未全部选中(若有一个商品未选中)--店铺不选中. 店铺全部选中后--全选自动选中:店铺未 ...

  9. App.js实现使用js开发app的应用,此文是中文文档

    在阅读前,在此说明下,本人英文一直不好,所以该文档是借助翻译工具翻译的,阅读起来可能有点不好,请各位谅解,哪位大神有标准的中文文档请分享下 Github下载地址:https://github.com/ ...

随机推荐

  1. tomcat JDK环境变量配置及tomcat多项目的配置

    1.解压JDK tar xzf jdk-8u171-linux-i586.tar.gz -C /usr/local -->mv /usr/local/jdk1.8.0_171 /usr/loca ...

  2. UiPath图片操作截图的介绍和使用

    一.截图(Take Screenshot)的介绍 截取指定的UI元素屏幕截图的一种活动,输出量仅支持图像变量(image) 二.Take Screenshot在UiPath中的使用 1. 打开设计器, ...

  3. Elasticsearch学习系列七(Es分布式集群)

    核心概念 集群(Cluster) 一个Es集群由多个节点(Node)组成,每个集群都有一个共同的集群名称作为标识 节点(Node) 一个Es实例就是一个Node.Es的配置文件中可以通过node.ma ...

  4. 论文阅读 Inductive Representation Learning on Temporal Graphs

    12 Inductive Representation Learning on Temporal Graphs link:https://arxiv.org/abs/2002.07962 本文提出了时 ...

  5. 可控线性序列机(查看除了inout端口外的其他变量的波形的方法)

    可控线性序列机: 可控:有个控制端控制何时输出线性序列. 线性序列机:输出一个线性序列. 知识点: 1.包含多个判定条件时用英文()括起来,用&&连接. 2.使能端EN的设置(类似于D ...

  6. 用kubeadm简单部署k8s

    一.环境准备 1.三台CentOS6.7虚拟机 master:192.168.0.54 注意:主节点最好是2颗cpu,否则在k8s控制平面初始化的时候会报错: node1:192.168.0.68 n ...

  7. 清北学堂 2020 国庆J2考前综合强化 Day5

    目录 1. 题目 T1 a 题目描述 Sol T2 b 题目描述 Sol T3 c 题目描述 Sol T4 d 题目描述 Sol 2. 算法 - 贪心 & 数学 1. 贪心 2. 数学 2.1 ...

  8. 【原创】Selenium获取请求头、响应头

    本文仅供学习交流使用,如侵立删! Selenium获取请求头.响应头 操作环境 win10 . mac Python3.9 selenium.seleniumwire selenium是没有办法直接获 ...

  9. 使用 Vagrant 在 VirtualBox 安装 Linux 虚拟机

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 目录 1. 导入 2.工具介绍 3.通过Vagrant为VirtualBox安装CentOS 7 4.总结 文章推荐: 关 ...

  10. 只要9.9元!零基础学习MySQL

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 导语 经过一段时间的筹备和整理,万里数据库<零基础学习MySQL>课程正式在腾讯课堂上线了. 课程地址:htt ...