返回博客首页
← 所有文章

Android 性能 - 原因和方法

2016 年 10 月 6 日 — 作者:Georgi Atanasov

我们经常收到关于 NativeScript Android 应用性能和 APK 大小的反馈,特别是使用 Angular 2 构建的应用。在这篇文章中,我将详细介绍 Android 运行时、核心模块和 Angular 2 集成项目背后的技术挑战,并分享一些可以显著提高性能的简单步骤。

加载(启动)时间

在 NativeScript Android 应用加载时会执行几个任务。

  1. 加载 NativeScript 本机库(*.so)并初始化 V8 JS VM。
  2. 提取 元数据 文件,读取它们并构建它们的内存表示。
  3. 从 APK 归档文件将所有 JavaScript 文件提取到文件系统中,以便之后在需要时更快地访问。目前,如果没有使用 Webpack 打包,一个空白的 Angular 应用大约有 3500 个 JS 文件。文件提取任务的执行复杂度与文件数量成线性关系 - 包含的 JS 文件越多,执行的 I/O 操作就越多。
  4. 需要主应用 JS 文件。require 操作包括脚本解析、编译、执行和在本地进行缓存。初始 require 图被填充并缓存。

注意: 文件提取仅在全新安装后首次运行应用程序时进行。在后续运行时,文件将直接从文件系统中读取。

前两个步骤的执行时间是固定的,没有优化的必要。但是,步骤 3 和 4 是导致加载时间增加的主要原因。如何优化这些步骤?请了解 android-snapshot 插件

此 snapshot 插件优化了:

  • 它提供所有框架 JS 文件(包括 Angular 和核心模块)的预编译二进制表示形式。
  • APK 中只包含一个二进制文件,而不是 3500 个独立文件。只有应用程序逻辑(即位于“app”文件夹中的内容)以单独的 JS 文件形式打包。这节省了大量步骤 3 的提取时间。
  • 预编译的 JS 源代码节省了大量步骤 4 的时间 - 所有 Angular 和核心模块都在 V8 堆中预加载。

唯一的权衡是,由于快照文件依赖于 CPU,应用程序包大小会增加 5 MB。不过,我们有计划进一步改进这一点(有关更多信息,请参阅“包大小”部分)。

如果你查看 snapshot 存储库的 Readme 文件,你会看到我们测试的近似数字。启用快照后,空白 Anular 2 应用程序的加载时间几乎缩短了一倍 - 从 4 秒缩短到 2 秒!

同步两个垃圾收集器

V8 运行自己的垃圾收集器,Android(Dalvik VM)也是如此。由于 NativeScript 的架构范式,特别是通过 JavaScript 进行的 100% 本机访问,有时会在 JavaScript 中代理本机 Android 对象。因此,Android 运行时使用复杂的机制来使代理的本机实例保持活动状态,直到 JavaScript 代理对象被收集为止。问题是,两个垃圾收集器存在于不同的世界中,并且以自己的速度运行。Android 运行时尝试在 JavaScript 端分配大型本机对象(如 Image)时施加相同的内存压力,但这只是一个提示。有时,Dalvik GC 需要清除一些大型对象(如 Bitmaps)来释放堆内存,但由于这些对象在 JavaScript 中被代理,并且代理仍然处于活动状态,因此什么也不会发生,Dalvik 堆不会被清除。大多数情况下,JS 代理到那时可以被收集,但 V8 的 GC 尚未开始,而 Dalvik 的 GC 已经开始运行。最终会导致内存泄漏和 OutOfMemory Android 异常,尤其是在使用大型本机对象(如 Bitmaps 或流)时。有关更详细的技术细节的良好阅读材料和示例 可以在这里找到

综上所述 - 有时我们需要一种机制来同步两个垃圾收集器,以确保正确回收内存。V8 在一个标志后面公开了一个 `gc()` 调用,该标志默认情况下在 NativeScript Android 应用程序中启用。NativeScript 核心模块利用此行为,在导航时调用 V8 的 `gc`。为什么在导航时调用?因为通常在导航操作之后,上一页面的 JavaScript 可视化树变得可以被 V8 的垃圾收集器访问。正如我已经提到的,收集代理将使相应的本机对象可以被 Dalvik 的垃圾收集器访问。

所有这些听起来像是完美的解决方案,但不幸的是,它会导致一些性能问题,因为 V8 的 GC 无条件地强制执行,因为它是在主 UI 线程上运行的阻塞操作。我们试图仅在 主应用程序循环空闲 时调用 V8 的 gc,但似乎这种启发式方法在导航期间没有按预期工作。

在导航期间显式调用 JavaScript GC 是 NativeScript 核心模块中主要的性能瓶颈之一。如果你今天遇到了这个问题,你可以 简单地注释掉 这行代码,看看效果如何。我们目前正在研究一个更通用的解决方案,该解决方案将在 Android 运行时中直接处理。虽然它也使用了一些启发式方法,但它看起来很有希望,并且似乎在迄今为止隔离的 99% 的内存泄漏场景中都能正常工作。

应用程序包大小

正如我在 这篇博文 中解释的那样,NativeScript Android 运行时提供三个独立的版本,用于如今可用的三种主要 CPU 架构。这使得一个空白的 Hello World 应用程序的大小约为 12 MB。如果 APK 大小对于您的客户至关重要,那么您可以为应用程序需要运行的每种 CPU 架构生成单独的 APK。有关更多信息,您可以参考 ABI 分割部分,该部分位于 适用于 Android 的发布帮助文章 中。

我如何立即提高性能?

以下是您可以采取的措施来提高 NativeScript Android 应用程序的性能

未来的改进

我们计划进一步改进 NativeScript Android 应用程序整体性能的上述三个主要方面。

  • 默认情况下为 Android 应用程序启用 snapshot 插件。
  • 改进快照生成。目前,插件附带了所有 Angular 和核心模块代码的预编译版本。相反,我们可以在客户机上首先使用 Webpack,跳过所有不必要的代码,然后对捆绑的结果运行 snapshot 工具。这样,我们就可以只发布当前二进制文件的一小部分。
  • 在 Android 运行时内部处理两个垃圾收集器的同步,而不是从 JavaScript 手动调用 `gc`。
  • 改进默认 APK 大小(查看此 GitHub 问题 以获取更多详细信息)。如果启用了 ABI 分割,则也将其应用于 snapshot 插件。

结论

虽然 NativeScript 应用程序是 100% 本地的,并且在大多数情况下运行速度快且流畅,但有时应用程序可能需要额外的微调才能变得更快。NativeScript 工程团队正在积极努力,争取使所有上述改进默认情况下启用。一如既往,我们欢迎并感谢您的反馈 - 请在 GitHub NativeScriptAndroid 运行时 存储库中分享您的反馈。