应用启动性能 | 介绍 App Startup 库

最近我开始尝试使用 AndroidX 的应用启动 (App Startup) 库。在这个库发 布了 1.0 版本 之后,我觉得是时候深入理解一下为什么需要、什么时候以及如何使用这个库。

首先我注意到的是它的名字 —— 应用启动,其表明这个库的功能可能比它字面上的意义更广泛。这个库并不涉及普通的启动 (起码目前如此)。它主要是为了降低由 content provider 初始化导致的对应用启动速度的影响。

眼下您可能和我一样从来没有考虑过第三方库都是如何被初始化的。也许是因为所有这些处理过程都在底层完成。准确地说,您在 build.gradle 文件中添加了一行代码来使一个开发库作为工程的依赖项,大功告成 (当然您还需要在工程中调用这个库的 API,要不然您为什么要添加它呢?)。

可是有很多库并不是简单地封装好一堆方法以供调用,它们时常还需要首先被初始化,而往往这个初始化还是很耗时的过程。更糟糕的是,这其中还暗藏陷阱,因为这些库常常在应用启动的时候进行加载和初始化,究其原因是由于其内部使用了 content provider

敞开您的心扉 - Content Provider

Content provider 是 Android 中在不同应用之间共享数据的方式。举个例子,手机中的联系人是通过 content provider 来实现数据共享的,这也使得其他应用可以访问用户的联系人数据 (当然,我们假设用户给予这些应用访问联系人数据的权限)。您也同样可以为其他应用提供访问授权,来使用您应用创建的数据。或许您的应用管理着一个 甜甜圈评分的数据库,而作为如此重要的信息,其他应用可能需要频繁地使用。

只要一个应用通过任何一种方式声明 content provider 开启,此时就会自动创建并且启动 content provider。

使用 content provider 有一个重要但可能并不那么明显的问题,就是应用在声明 content provider 开启后,它会被自动创建并运行。而且需要注意的是,一个应用的启动并不只是通过用户启动,其还可以是通过系统访问该应用的服务,又或者是 job scheduler 触发了应用的一个循环作业等等。所有的这些都会触发 content provider 的资源开销以及产生相应的运算作业。当有需要访问该 content provider 的时候,系统需要该应用能够处于就绪状态,所以系统会在应用启动的时候自动运行 content provider。

这些细节对于仅仅调用这些库的开发者都是不可见的,因为具体实现都隐藏在自动生成的代码中。您需要查看 合并后的 manifest 文件 来理解这一切是如何发生的。

合并 Manifest

我针对 Android 应用清单的交互操作基本上都发生在工程自生成的 Manifest.xml 文件中,我会通过编辑该文件来添加 activity、服务和权限。但是这个 manifest 文件并不是最终提交到系统的那个,这个文件只是提供了关于您应用的信息,这些信息会被 “合并” 到最终的 manifest 文件中。合并后的文件包含了您的 Manifest.xml,以及编译工具挑选的其他信息,包括了您应用使用库的 manifest 文件。也正是这个合并后的 manifest 文件告诉我们库的 content provider 究竟发生了什么。

让我们来看一个具体的例子。并不是所有的库都使用了 content provider (尽管这还是很常见的),所以我们要用一个包含 content provider 的库 – WorkManager。为了在我的工程中使用 WorkManager,我在应用的 build.gradle 文件中添加了如下依赖:

// 查看最新的版本号 https://developer.android.google.cn/jetpack/androidx/releases/work
def work_version = "2.5.0"
implementation "androidx.work:work-runtime-ktx:$work_version"

在我同步以及构建了该应用之后,我测算了一下启动时间 (稍后会详细介绍) 来对比添加这一依赖前后启动时间上的差别。我注意到应用在添加依赖后,启动时间比之前多了 70ms,而且这是在还没有调用 WorkManager 任何功能的情况下,我只不过是添加了这个依赖。

我在合并后的 manifest 文件中发现了启动时间延迟的原因,您可以在查看 Manifest.xml 文件时,通过点击 Android Studio 编辑窗口左下方的 Merged Manifest 标签来查看合并后的 manifest 文件。

编辑窗口下方的标签控制着您所看到的是您应用的 manifest 文件还是最终合并后的 manifest 文件

编辑窗口下方的标签控制着您所看到的是您应用的 manifest 文件还是最终合并后的 manifest 文件

在合并后的 manifest 文件中,我发现声明 WorkManager 依赖增加了很多额外的信息,包括如下的 provider 代码块:

这个 provider 存在于添加 WorkManager 依赖后合并的 manifest 文件

我很好奇这个 provider 是从哪里来的,所以我点击了其中的第一行,编辑器直接跳到 WorkManager 的 manifest 文件,其中包含如下代码:

<application>
    <provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        android:directBootAware="false"
        android:exported="false"
        android:multiprocess="true"
        tools:targetApi="n" />
    <!-- ... and a bunch of other stuff ... -->
</application>

合并的 manifest 文件的工作原理就是合并了组成应用的所有模块的 manifest 文件,当然也包括我刚刚添加的 WorkManager 库的 manifest 文件。因为其包含的 content provider 现在是合并的 manifest 文件的一部分,系统会在应用启动的时候自动地创建并运行该 content provider。

现在我知道我是如何加载这个库以及运行相关的 content provider。但是这究竟有什么影响呢?

测算启动时间

我最近发布了一篇文章 - 测试应用启动性能,其中详细描述了如何测算应用的启动时间。我用了同样的方法来测算在添加 WorkManager 依赖前后的应用启动时间,并且发现 WorkManager 增加了平均 67ms 的应用启动时间。

请注意,正如我在 启动测试 的文章中提到的,我锁定了我的 Pixel 2 的 CPU 时钟频率,所以应用启动时间在其他用户的设备上可能会短一些,而在另外一些使用低端设备上会长一些。另外需要注意的是 (我也在那篇文章中提到),我可能并不需要锁定时钟频率,因为系统通常会在应用启动的时候以最高的频率运行。但是锁定时钟频率在性能测试的时候永远都是一个好做法,因为这样我们才能获得稳定的结果。同时,锁定时钟频率还通常会造成更长的运行时间 (由于更低的频率),这也会帮助我们降低由于过短运行时间造成的噪音数据。

还有一点需要强调的是,这个启动时间并不全是 content provider 产生的。Content provider 确实会需要一定时间来创建,但是其需要大概 1-2ms 而不是我看到的 67ms。其实这是这个库被加载以及初始化的总时间,外加创建和运行 content provider 的时间来初始化该代码库。

所以看起来仅仅是添加这个库到我的项目就造成了将近 70 毫秒的启动延迟。在一个真正的应用中,我可能会使用多个库,而在应用启动时它们中很多都有自己的 content provider 需要运行,这就会造成更严重的启动延迟。

所以,我们要做点什么来减轻这个问题的影响呢?敬请关注我们的后续文章,在下一篇文章中,我将深入探讨如何利用 AndroidX 的应用启动 (App Startup) 库来实现库的延迟加载。