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

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. mybatis查询mysql 数据库中 BLOB字段,结果出现乱码

    起因 mybatis-plus 通过Mapper 查询数据,映射出来的BLOB字段中的yml数据中文是乱码的 --- DefaultValue: '' Formula: '' HintContent: ...

  2. MySQL-3-DML

    DML 数据操作语言 插入insert 语法一:insert into 表名(列名,...)values(值1,...): 语法二:insert into 表名 set 列名=值,列名=值,... 插 ...

  3. 拥抱云原生 2.0 时代,Tapdata 入选阿里云首期云原生加速器!

      3月9日,阿里云首期云原生加速器官宣,Tapdata 突出重围,成功入选31 强,将与多家行业知名企业,携手阿里云共建云原生行业新生态,加速拥抱云原生新时代的无限潜能.   2021年,阿里云正式 ...

  4. FileNameFilter过滤器的使用和Lambda优化程序--IO概述(概念&分类)

    FileNameFilter过滤器的使用和Lambda优化程序 public class Demo02Filter { public static void main(String[] args) { ...

  5. Lambda表达式有参数有返回值的练习(自定义接口)和Lambda省略格式&Lambda使用前提

    给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数字相加得到和值 使用L ambdo的标准格式调用invokeCalc方法,完成120和130的相加计算 public in ...

  6. .NET自定义认证虽然简单,但好用

    前言 有这样一种场景,就是新项目已经集成了认证中心,或者是都用了统一的认证方式(比如现在常用的JWT),这样对于项目之间的对接就显得比较方便,至少在认证这块还是能减少一些工作量的.但当上线的老项目需要 ...

  7. 阿里 Maven仓库

    <?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://mav ...

  8. String类常用的API

    String类常用的API 字符串内容的比较: 注意: 不能使用 == 去比较两个字符串的内容.原理:比较的是字符串的地址. (如果两个字符串都是使用""进行赋值,那么他们都是放在 ...

  9. 005_面试题 Java_传递方式

    面试题: 问:java是值传递还是引用传递? 答:java只有值传递,基本类型传递的是具体的数,引用类型传递的是具体的地址

  10. 流程控制语句break

    break语句 用于结束循环结构,通常与分支结构if一起使用 即非正常循环,在中间循环的时候直接退出 注意break打断的是循环语句,不是if语句 注意while循环中一般需要有改变变量这个操作,否则 ...