阅读 1192

Android 应用启动性能 | 延迟初始化

上一篇文章 中,我展示了 content provider (它出现在应用合并后的 manifest 文件) 是如何在应用启动的时候自动加载第三方库以及模块的。

在这篇文章中,我会介绍如何使用 AndroidX 的 应用启动 (App Startup) 库来进一步控制那些库该在何时以及以何种方式被加载。也许,我是说也许,我们也会顺便发现该如何缩短应用的启动时间。

使用应用启动库自动初始化

使用应用启动库 (App Startup) 最简单的方式是利用它的 content provider 在后台初始化其他库。您既可以指定应用启动库该如何初始化其他的库,也可以从合并后的 manifest 文件中移除其他库的 content provider。避免使用多个 content provider 执行启动任务,而是将资源用于加载应用启动库,然后再加载其他内容。

您可以通过如下三步实现上述操作,首先在您工程的 build.gradle 文件中添加应用启动库作为依赖,其次为每一个需要初始化的库创建一个 Initializer,最后在您工程的 Manifest.xml 文件中添加相关信息。

让我们再看一遍我在 第一篇文章 中使用的 WorkManager 示例。为了通过应用启动库加载 WorkManager,我先在应用的 build.gradle 文件中添加了应用启动库:

// 查看最新的版本号 https://developer.android.google.cn/jetpack/androidx/releases/startup
def startup_version = "1.0.0"
implementation “androidx.startup:startup-runtime:$startup_version”
复制代码

然后,基于应用启动库提供的 Initializer 接口,我创建了一个 Initializer:

class MyWorkManagerInitializer : Initializer<WorkManager> {
    override fun create(context: Context): WorkManager {
        val configuration = Configuration.Builder().build()
        WorkManager.initialize(context, configuration)
        return WorkManager.getInstance(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 没有其他依赖库
        return emptyList()
    }
}
复制代码

每一个 Initializer 有两个方法需要复写: create()dependencies()dependencies() 被用来指定多个依赖库的初始化顺序。在这个示例中我并不需要这个功能,因为我只需要处理 WorkManager。如果您需要在应用中使用多个库,请查看 应用启动使用手册 中关于使用 dependencies() 的详情。

对于 create() 方法,我模仿了 WorkManager’s content provider 中的实现。

顺便说一下,其实这个方法在使用应用启动库的时候很常用。一个库的 content provider 负责了其初始化的实现,所以您通常都可以参考那个类中的代码来手动实现它。有些库可能比较麻烦,因为它们使用了隐藏的或者内部的 API,但是好在 WorkManager 并不是,所以我可以这么做,希望该方法也适用于您的情况。

最后,我在 Manifest.xml 文件的 <application> 代码块中添加了两个 provider 的标签。第一个如下所示:

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:exported="false"
    tools:node="remove" />
复制代码

WorkManagerInitializer 标签很重要,因为它表示需要 Android Studio 删除自动生成的 provider,而该 provider 是在 build.gradle 文件中添加 WorkManager 后生成的。如果没有这个特殊的标签,这个库仍然会在应用启动的时候自动初始化,继而在应用启动库尝试初始化它的时候报错,因为它已经被初始化了。

下面是我添加的第二个 provider 标签:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data     android:name="com.example.startuplibtest.MyWorkManagerInitializer"
    android:value="androidx.startup" />
</provider>
复制代码

InitializationProvider 标签和通过添加应用启动库到 build.gradle 文件中自动生成的标签基本相同 (您可以通过查看合并后的 manifest 文件来验证 -- 详情请查看 第一篇文章),但是它们有两个很重要的不同点:

tools:node="merge"
复制代码

这个参数主要用于 Android Studio 所负责的 manifest 合并操作。它告诉工具在最终合并的 manifest 文件中合并这个标签的多个实例。在这个例子中,它会合并由库依赖自动生成的 <provider> 到这个版本的 provider,这样在最终合并的 manifest 文件中只会有这一个标签实例。

另一行包含了这个 meta-data:

<meta-data  android:name="com.example.startuplibtest.MyWorkManagerInitializer"
    android:value="androidx.startup" />
复制代码

这个 provider 中的 metadata 标签告诉应用启动库如何找到您的 Initializer 代码,这些代码会在应用启动的时候执行来初始化这个库。请注意这导致的区别: 如果您没有使用应用启动库,就会自动执行相关初始化,因为 Android 会在那个库中创建并执行 content provider,之后会自动初始化这个库本身。但是通过应用启动库指定您的 Initializer,以及在合并 manifest 文件中去除 WorkManager 的 provider,相当于告诉 Android 转而使用应用启动库的 content provider 来加载 WorkManager 库。如果通过这个方式初始化多个库,您可以利用应用启动库的这个单独的 content provider 有效地管理这些请求,而不是导致每个库都创建自己的 content provider。

偷个懒...如果您想的话

当优化应用启动性能的时候,我们不能改变那些无法控制的代码实现。所以这里的思路并不是加速我们使用库的初始化,而是控制这些库什么时候以及如何被初始化。尤其是我们可以决定任一个库是否需要在应用启动的时候被初始化 (要么使用库的默认机制添加 content provider 到合并的 manifest 文件,或者也可以利用应用启动库的 content provider 来集中管理初始化请求),还是需要稍候再加载它们。

举个例子,或许在您应用的一个特殊的流程中需要某一个包含 content provider 初始化的库,但是这个库并不需要在应用启动的时候立即被加载,又或者在某些情况下它根本不需要被加载。如果是这样的话,为什么要因为只在某个特殊代码路径中需要而在应用启动时花时间初始化一个很大的库呢?为什么不等到这个库真正被需要的时候再引入相关的初始化开销呢?

这正是应用启动库高明的地方,它能帮您从合并的 manifest 文件中和应用启动的过程中移除隐藏的 content provider,也能帮您延迟或者更有目的地加载这些库。

使用应用启动库实现延迟初始化

现在我们已经知道该如何使用应用启动库实现自动加载以及初始化库。接下来让我们更进一步地来看看,如果您不想在启动的时候初始化,该如何实现延迟初始化。

其实上面的代码已经很接近了,在 build.gradle 文件中您需要同样的启动依赖和其他您想使用的库,也还是需要特殊的 "移除" provider 标签来去除每个库自动生成的 content provider。我们只需要向 manifest 文件添加多一点信息来告诉它同样移除应用启动库的 provider。这样在应用启动的时候就不会有任何 content provider 初始化发生,而完全由您来决定什么时候应该触发相关初始化。

为了达到这个目的,我用下面的代码替换了前面使用的 InitializationProvider。上面所展示的代码告诉了系统该如何定位 content provider 中自动初始化您库的代码。因为稍后要手动触发初始化,这一次我要跳过那个部分,而只留下在应用启动的时候去除自动生成的 content provider 的部分。

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

在我做了这个改动后,在合并的 manifest 文件中不再有任何 content provider 了,所以应用启动库和 WorkManager 都不会在应用启动的时候被自动初始化了。

为了手动初始化这些库,我在应用的其他地方添加了如下代码来实现它:

val initializer = AppInitializer.getInstance(context)
initializer.initializeComponent(MyWorkManagerInitializer::class.java)
复制代码

AppInitializer 是应用启动库提供的连接所有这些部分的类。您需要使用一个 context 对象来创建 AppInitializer 对象,然后可以向其传递一个您为初始化各种不同库创建的 Initializer 引用。在这个示例中,我使用的是 MyWorkManagerInitializer,然后就搞定了。

时间就是一切

我做了几次测试 (使用的是我在 测试应用启动性能 文章中提到的计时方法) 来比较几种不同的启动应用和初始化库的方法。我统计了不带任何库、带 WorkManager (使用默认自动生成的 content provider)、在启动时使用应用启动库自动初始化 WorkManager 以及使用 AppInitializer 延迟初始化 WorkManager 和应用启动库。

需要注意的是,就像我们在 之前的文章 中讨论的,所有的这些时间计算都是基于锁定的 CPU 主频,所以这些时长都要比在没有锁定 CPU 主频的机器上大很多。它们只在相互之间比较的时候有意义,而并不能代表真实的情况。下面是我发现的:

  • 不带 WorkManager: 1244 ms
  • 带 WorkManager 并且通过 content provider 加载: 1311 ms
  • 带 WorkManager 并且通过 App Startup 加载: 1315 ms
  • 带 WorkManager (延迟加载): 1268 ms

最后,我统计了利用 AppInitalizer 手动初始化 WorkManager 的耗时:

  • 利用 AppInitializer 初始化 WorkManager: 51 ms

这个数据给我们带来一些启示。首先,在应用启动的时候加载 WorkManager 会给我的应用平均增加 67 毫秒 (1311–1244) 的启动时间。需要注意的是: 加载这个库的常规方式 (使用 content provider) 使用的时间和使用应用启动库的 (1315 – 1244 = 71 ms) 差不多。这是因为应用启动库在单个库的例子中并不会帮我们节省时间,我们只不过是转移逻辑到另一个代码路径中运行。如果使用应用启动库加载多个库,我们会得到相应的优化效果,但是针对这里的单个库的例子,使用这个方法不会有任何节省时间的优势。

同时延迟初始化 WorkManager 让我可以 "节省" 大约 51 毫秒的时间。

这个差别是否足够明显到您需要担心呢?答案永远是 "看情况而定"。

51 毫秒占了 1.3 秒总时长的不到 4%,而对于一个真实应用来说,通常都会比我这个简单的应用更复杂,这个耗时占总启动时间的百分比会更低。这种情况下这个时长可能不值得担心。但是有时候您可能发现有些库需要太长时间来初始化,更有可能的是,您可能使用了几个自带 content provider 的库,而它们每一个都会增加一点您应用的启动时间。如果您可以将上述大部分或者全部工作推迟到一个更为合适的时间点,并且从启动过程中剥离,或许您会发现应用的启动速度会有显著的提高。

像所有的性能优化项目,您可以做的最重要的事情是分析细节、测量以及决定:

  • 检查您项目合并后的 manifest 文件。您可以看到多少 <provider> 标签?
  • 您能否利用应用启动库从合并的 manifest 文件中移除一些甚至所有这些 content provider,并观察它如何影响启动时间?您能否在实现这个的同时不影响运行时行为呢?(值得注意的是: 您需要保证在应用开始依赖相关库的功能之前,确保初始化它们。)

最后,尽情享受性能测试和优化。我会继续找寻更多分析和优化应用的性能办法,如果发现什么有价值的东西我会发布相关的内容。

文章分类
Android
文章标签