返回博客首页
← 所有文章

使用 Webpack + V8 堆快照提升 Android 应用启动速度

2017 年 7 月 5 日 — 作者:Ivan Buhov

启动时间是现代移动应用的关键成功因素。因此,我们一直在努力最小化 NativeScript 应用在初始化阶段花费的时间。随着我们最新发布的nativescript-dev-webpack 插件,我们通过引入 V8 堆快照支持,在优化 Android 上的启动时间方面取得了重大进展。

我们在 Nexus 5 设备上测量了一个空 Angular 应用的启动时间提升了 30%。在快照的捆绑包中添加更多代码可以带来更好的结果。我们的 SDK 示例应用在启用快照后启动速度提高了 40%。在旧设备上,这个百分比甚至更高。我们很高兴向您提供此功能,并希望在您衡量应用程序启动时间提升时收到您的反馈。

V8 堆快照

V8 JavaScript 引擎有一个名为自定义启动快照 的宝贵功能 - 为所有 V8 嵌入程序(包括 {N} Android 运行时)提供了一种机会,可以使用之前准备好的堆快照来初始化 JavaScript 上下文。换句话说,我们不再需要在每次启动时获取、解析和执行脚本,而可以只执行一次脚本,并将 V8 堆状态序列化为二进制 Blob 文件。然后,生成的 Blob 会包含在 APK 捆绑包中,并在启动时由 Android 运行时加载。因此,应用的启动速度明显更快,而不会牺牲任何功能。

如何使用它

V8 堆快照支持由最新nativescript-dev-webpack 包中包含的 NativeScriptSnapshot Webpack 插件提供。如果您还没有使用 Webpack 捆绑,本文 可以帮助您设置初始配置。如果您已经拥有现有的 Webpack 配置,请确保在安装最新nativescript-dev-webpack 包后更新它。

node ./node_modules/.bin/update-ns-webpack

如果您已经安装了nativescript-dev-android-snapshot 包,可以安全地将其删除。它已经过时,很快将被弃用。

npm uninstall nativescript-dev-android-snapshot

现在,您应该在 webpack.config.js 中拥有以下代码行。

...
    if (env.snapshot) {
        plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({
            chunk: "vendor",
            projectRoot: __dirname,
            webpackConfig: config,
            targetArchs: ["arm", "arm64", "ia32"],
            tnsJavaClassesOptions: { packages: ["tns-core-modules" ] },
            useLibs: false
        }));
    }
...

接下来,我们需要做的就是在 package.json 文件中向 Android 捆绑命令传递一个 --snapshot 标志,并运行该命令。

To enable snapshot generation pass --snapshot flag to the android bundling commands in package.json

您可以在捆绑步骤产生的日志中看到传递给底层快照生成器的确切参数。

Snapshot generator arguments

由于无法在 Windows 上构建运行在 Windows 上的 mksnapshot 工具,因此快照生成功能仅限于 macOS 和 Linux 平台。目前,--snapshot 标志在 Windows 上是无效的。

工作原理

在幕后,我们的快照生成器使用 V8 团队开发的所谓 mksnapshot 工具,从任意脚本生成快照 Blob。这是通过在完全空的 V8 实例中执行脚本来完成的,然后将堆状态保存到二进制 Blob 文件中。

Snapshot generation process diagram

因此,执行脚本的 V8 上下文除了 JS 引擎附带的 API 外,没有其他注入的 API。但是,Android 运行时提供的 V8 上下文(在应用程序启动时加载生成的 Blob)包含丰富的额外注入的 API。

  • 公开给 JS 世界的本地 Java API(java.lang.Object 等)。
  • CommonJS 模块规范 指定的 API(requiremoduleexports 等)。
  • 特定于运行时的 API(global.__runtimeVersionglobal.__extends 等)。

这些 API 由 Android 运行时公开,这使得它们在快照上下文中不可用。尝试在快照的脚本中调用它们将抛出错误。另一个限制是 V8 上下文只能使用单个预生成的堆快照初始化。换句话说,只能快照一个脚本文件。

为了克服快照上下文中缺少 CommonJS 模块概念,并使我们快照的单个脚本尽可能大,最好的方法是使用支持 CommonJS 模块的 JavaScript 捆绑器。由于 Webpack 在社区中得到广泛使用,因此选择并不难。

不幸的是,Webpack 无法克服快照上下文中 Java 和特定于运行时的 API 的不可用性。因此,如果在快照生成期间触及了由 Android 运行时注入的 API,您可能会收到引用错误。

snapshot-generator-error

包含在快照的捆绑包中的模块仍然可以包含本地 API 调用,前提是它们不会在模块加载时立即执行。例如,以下模块

require("application");

var time = new android.text.format.Time();

无法快照,因为它触及了不可用的 android.text.format.Time API。但是,在下面这个模块中

require("application");

function getTime() {
    return new android.text.format.Time();
}

本地 API 访问不会在模块执行时执行。鉴于 getTime() 在 Android 运行时提供的完整功能的 V8 上下文中稍后被调用,我们可以安全地将该模块包含在快照的捆绑包中。

如果快照步骤由于引用未定义的 API 而失败,请尝试以下几种解决方案。

  • 如果可以更改包含禁止的 API 调用的模块,请将有问题的代码包装在一个函数中,该函数在应用在设备上运行后调用。
  • 将模块保留在捆绑包中,但确保所有不可快照模块的 require 调用在应用在设备上运行后执行。
    require("application");
    var m = require("non-snapshotable-module");
    
    function doSomething() {
        return m.someMethod();
    }

    如果上述代码在加载不可快照模块时实际上需要它,则上述代码更有可能成功快照。

    require("application");
    
    function doSomething() {
        return require("non-snapshotable-module").someMethod();
    }

    如果 doSomething() 函数从未在快照上下文中调用,则不可快照的模块不会被执行,并且 Blob 生成将成功。

  • 从快照的捆绑包中排除包含禁止的 API 调用的模块。

关于 CPU 架构我们需要了解的一切

与 Android 运行时一起提供的 V8 库包含 3 个 CPU 架构切片 - ia32(用于模拟器)、armarm64(用于设备)。由于 V8 堆二进制格式不是与架构无关的,因此我们需要为每个 CPU 架构提供不同的 Blob 文件。为所有架构生成快照可以保证在应用程序启动时始终能找到正确的 Blob 文件。但是,当我们的构建目标是所有支持的架构的子集时,最好只为子集中的架构启用堆生成。例如,在为设备构建时,我们可以排除 ia32。此外,arm64 也可以从列表中删除,因为如果没有明确指定,即使在 arm64 设备上也会使用 arm 二进制文件,这使得 arm64 Blob 文件毫无用处。

包含不必要的架构不会影响应用程序性能,但会增加应用程序包的大小。可以通过修改 NativeScriptSnapshot Webpack 插件的 targetArchs 选项来配置目标架构。查看文档 以了解有关如何配置快照生成过程的更多详细信息。

V8 堆快照 - 过去和未来

您可能知道,V8 堆快照支持目前可以通过nativescript-dev-android-snapshot 插件使用。从 v3.1.1 开始,它不再是默认模板的一部分,并且很快将被弃用,取而代之的是nativescript-dev-webpack 与本地生成的快照集成。以下列出了我们在旧方法中遇到的缺点。

  1. 仅使用预定义的快照包集:tns-core-modulestns-core-modules + nativescript-angular

  2. 无法控制快照中包含的内容。

    • 无法包含客户端代码。

    • 无法包含其他包/插件。

  3. 在 Angular 应用的情况下,不支持 Angular 预编译 + 快照。

  4. 您被迫在 Webpack 和快照之间进行选择。无法同时拥有两者。

  5. 在许多情况下,Webpack 比快照效果更好,例如在 Angular 应用中,Webpack + Angular 预编译会导致比使用快照更好的启动时间。

  6. 难以调试和排查错误,因为快照生成不会在您的机器上发生。

所有这些缺点促使我们在代码捆绑后,将 V8 堆快照生成转移到开发人员的本地机器上。这也统一了我们的性能优化路径,让您有机会使用所有可用的技术(Webpack、AoT 编译、uglify、快照等),而不是因为现有的不兼容性而选择子集。

提供您的反馈

我们很乐意听到您对本地快照生成功能的反馈。不要忘记在启用它后与社区分享您的启动时间测量结果。您可以在nativescript-dev-webpack 存储库 中记录问题,如果您真的想参与进来,甚至可以提交拉取请求。