返回博客首页
← 所有文章

使用 Android Studio 和 Xcode 查找内存泄漏

2019 年 1 月 8 日 — 作者:Dimitar Todorov

内存消耗是应用程序性能的关键方面之一。测试内存泄漏并防止它们是每个 QA 和应用程序开发人员应执行的主要任务之一。内存泄漏会导致应用程序性能下降,甚至导致应用程序崩溃,从而降低应用程序用户的满意度,甚至导致您失去用户。在这篇文章中,我将向您展示如何追捕它们,找到大多数内存泄漏的位置,以及如何解决它们。

先决条件

此任务的先决条件是几个工具,这些工具帮助我们深入了解运行时的应用程序并分析其行为。我将向您展示如何使用 Xcode Instruments 和 Android Studio Profiler 分析原生 iOS 和原生 Android 应用程序。您可以分别从 macOS App Store 和 developer.android.com 直接下载它们。这些工具用于查看资源,例如内存、线程和进程、方法计时和网络使用情况。

原生内存泄漏追踪

Android

构建 NativeScript 应用程序后,application/platforms 文件夹中会有两个原生应用程序。从 Android 开始,我们需要打开 Android Studio 并选择“分析或调试 APK”选项,然后打开应用程序的 .apk 文件。

android studio

当工作室准备就绪后,我们必须手动设置一项配置。该项目的 Android SDK 应该被配置 - 打开文件菜单并选择项目结构。在打开的窗口中,在项目 SDK 下拉菜单中选择Android SDK,然后转到模块,再次在模块 SDK 下拉菜单中选择Android SDK。将更改应用于项目并准备启动。

android studio setup

在 Android Studio 的功能区中有一个“分析”图标,点击该图标将打开一个显示当前可用设备的窗口,选择一个,点击确定按钮,开始追踪。请记住,应用程序标识符应该唯一。如果该应用程序已经安装在设备上,您应该使用adb install -r命令。

在窗口中,您可以看到 CPU、内存和网络的指标,点击内存指标将扩展内容并显示更详细的图表,我们可以在其中监控正在运行的应用程序的内存使用情况。

android studio profile

主要的内存消耗组件是那些使用数据的组件 - 列表、画廊、网格、图表等。内存泄漏追踪应该针对此类组件。在我们的示例中,我们有一个Image区域组件,它显示来自画廊的图像。每次选择图像时,使用的内存大小预计会增加。但是,选择另一个图像或再次显示同一图像不应该增加使用的内存。开发人员应该适当地选择一项技术,并应该负责释放未使用的内存和对象。

在我们的示例中,我们看到没有泄漏,在依次选择图像之后,第一个图像的内存被释放。我们看到了内存使用情况系列的上升和下降。

android studio profiling results

iOS

让我们尝试对 iOS 使用相同的示例,看看会发生什么。

此处的过程与 Android 相同,只是与 iOS 应用程序开发和调试过程有关的一些细节有所不同。

ios simulator

同样,我们首先在 XCode 中打开原生应用程序的项目或工作区(位于platforms/ios文件夹中)。然后,转到产品 -> 分析菜单并选择分析选项。应用程序构建并部署到设备后,会显示一个带有选项的窗口。对于此测试,我们将选择分配选项。当分析仪器显示时,我们可以简单地按下窗口左上角的红色记录按钮并开始观察图表。最初,该应用程序使用 16-17 MB 的内存。当我们开始在应用程序中选择图像时,我们看到图表略微上升,但是当我们开始选择第二张、第三张等图像时,内存的消耗呈线性增长,这与我们预期的结果不符。

这看起来像是我们应用程序中的内存泄漏。

xcode memory leak

然而,经过进一步的调查,事实证明,在某些时刻,虽然很少见,但总的内存使用情况会下降,然后在选择图像时再次开始上升。因此,这并不完全是泄漏,因为最终,似乎ARC(自动引用计数,iOS 中垃圾收集的替代方案)机制设法通过释放不需要的对象来释放内存。问题仅仅是高内存使用量,这在 ARC 未释放这些大型对象的情况下可能对应用程序执行造成致命影响。

但是为什么会发生这种情况?是什么阻止了 ARC 释放内存?让我们一步一步地解释。

  • 选择图像时,nativescript-imagepicker插件 API 将图像的 JS 对象表示形式返回到应用程序。
  • 这些 JS 对象中的每一个都有其原生对应物,在本例中 - 图像(大小为大型对象),使用大量的内存。
  • JS 对象本身非常小,以至于 JavaScript VM 中的内存使用情况永远不会达到触发垃圾收集的程度。
  • 因此,许多小的 JS 对象会导致 JS 世界中原生端的内存使用量很高。只有当 JS 中的内存压力升高时,才会触发 JS 中的 GC,所有原生对应物都会被释放。

那么我们如何解决这个问题?也许您已经猜到了 - 在 JavaScript 中触发垃圾收集!虽然这听起来不像最佳解决方案,但它似乎是目前唯一的选择。让我们看看我们的图形是什么样子的。

xcode memory leak fixed

确实有效 - 原生对象会在垃圾回收的 JS 对象释放其引用后立即被释放。

对于此类任务,一些改进是检查持有数据的组件(例如图表、网格、日历、数据表单、下拉列表、菜单等)。查找加载的额外资源。查找“下拉刷新”功能,该功能刷新应用程序的视图,在滚动时按需加载项目,严格谨慎地处理应用程序页面之间的导航。内存泄漏可能隐藏在任何地方,因此我们应该做好追捕它们的准备。一旦从用户那里隐藏的东西,就应该删除,并应正确释放其内存。

虚拟内存泄漏

虚拟内存泄漏是指在应用程序的 UI 部分发生的那些泄漏。在页面之间导航时,会将 UI 组件(如标签、按钮等)进行多次复制。视图没有从列表中删除,在刷新后在网格中进行多次复制,等等,这些都是内存方面泄漏的示例。让我们看一个使用带有“下拉刷新”功能的ListView的示例。

NativeScript CLI 有一个内置的功能来调试 iOS 和 Android 应用程序,并在 Google Chrome 的开发者工具窗口中检查 UI 元素。

今天我们将使用 主从应用程序模板中的ListView组件。

nativescript master detail template

从模板中创建一个应用程序。

tns create masterDetailApp --tns-template-master-detail

...然后运行tns debug android命令,等待应用程序构建。然后打开 Chrome 浏览器,访问 CLI 在控制台中打印的链接。

在浏览器的开发工具的内存选项卡中,我们看到一个按钮,用于创建 JavaScript 堆的快照。拍摄应用程序的初始快照作为参考,并查看列出的元素。通过在搜索栏中键入“image”来过滤元素。正如我们所看到的,该模板从ListView加载了四张图像。

chrome devtools shows four images

我们还可以看到它们的 ID。通过触发“下拉刷新”,我们期望ListView的源代码被刷新,并且项目的数量保持不变,但是如果我们在拍摄新的快照后再次过滤图像,我们发现图像的数量翻了一番,现在有八个!

chrome devtools shows eight images

显然,ListView组件存在一个错误 - 它的项目/视图应该在不再使用时(即对用户可见时)由垃圾收集器删除。

让我们看看我们是如何发现该泄漏的。第一个快照中的四个元素不应该出现在第二个快照中,但是如果我们查看树,它们仍然在那里。当我们检查它们时,我们发现renderElement保留器中RadListView的第一次出现是在_itemTemplate中,它是由ViewContainerRef创建的。

chrome devtools inspection

在 Angular 中创建视图时,一个常见的场景是使用ViewContainerRef来完成。它最常被开发人员使用,并且是创建嵌入视图的常用方法之一。它隐藏了漏洞,因为容器应该在使用完毕后被清除。如果未调用clear方法,即使触发了垃圾收集器,所有视图也将保持未清除状态。原因是ViewContainerRef本身持有对创建的嵌入视图的引用,因此它们无法被标记为可供垃圾收集。

现在我们知道是什么创建了视图,我们应该找到一种方法来清理它们。在 Angular 文档中搜索,我们发现ViewContainerRef有一个clear()方法,它会销毁该容器中的所有视图。在与开发人员讨论和调试后,引入了修复程序,并且我们模板中的泄漏被删除了!

结论

应用程序工作流程的性能分析是测试过程的关键部分。确保应用程序性能良好应该是每个应用程序开发团队的目标。内存使用是应用程序生命周期的重要组成部分,过度使用内存可能会导致崩溃、数据丢失和客户不满意(这不是我们想要的结果!)。执行内存分析非常容易、快捷、愉快,而且结果可以并且将导致性能更好的应用程序和更满意的客户。