使用内存分析器检查您的应用的内存使用情况

内存分析器是 Android 分析器 中的一个组件,它可以帮助您识别可能导致卡顿、冻结甚至应用崩溃的内存泄漏和内存抖动。它会显示应用内存使用的实时图表,并允许您捕获堆转储、强制执行垃圾回收,以及跟踪内存分配。

要打开内存分析器,请执行以下步骤

  1. 单击**查看 > 工具窗口 > 分析器**(您也可以单击工具栏中的**分析** )。
  2. 从 Android 分析器工具栏中选择要分析的设备和应用进程。如果您通过 USB 连接了设备,但未在列表中看到它,请确保您已启用 USB 调试
  3. 单击**内存**时间轴中的任何位置,打开内存分析器。

或者,您可以使用 dumpsys 从命令行检查应用内存,还可以查看 logcat 中的 GC 事件

为什么应该分析应用内存

Android 提供了一个托管内存环境 - 当它确定您的应用不再使用某些对象时,垃圾回收器会将未使用的内存释放回堆。Android 如何查找未使用的内存一直在不断改进,但在所有 Android 版本中,系统最终必须短暂暂停您的代码。大多数情况下,暂停是不可感知的。但是,如果您的应用分配内存的速度快于系统收集的速度,您的应用可能会在收集器释放足够内存以满足您的分配之前被延迟。延迟会导致您的应用跳过帧并造成明显的缓慢。

即使您的应用没有表现出缓慢,如果它存在内存泄漏,它仍然会保留这些内存,即使它在后台运行也是如此。这种行为会通过强制执行不必要的垃圾回收事件来减慢系统内存性能。最终,系统将被迫杀死您的应用进程以回收内存。然后,当用户返回您的应用时,它必须完全重新启动。

为了帮助防止这些问题,您应该使用内存分析器执行以下操作

  • 在时间轴中查找可能导致性能问题的不可取的内存分配模式。
  • 转储 Java 堆,查看哪些对象正在使用内存。在一段时间内进行多次堆转储可以帮助识别内存泄漏。
  • 在正常和极端用户交互期间记录内存分配,以准确识别您的代码是在短时间内分配过多对象,还是分配了最终泄漏的对象。

有关可减少应用内存使用量的编程实践的信息,请阅读管理应用内存

内存分析器概述

当您首次打开内存分析器时,您将看到应用内存使用的详细时间轴,以及访问工具以强制执行垃圾回收、捕获堆转储和记录内存分配。

图 1. 内存分析器

如图 1 所示,内存分析器的默认视图包括以下内容

  1. 用于强制执行垃圾回收事件的按钮。
  2. 用于捕获堆转储的按钮。

    注意: 用于记录内存分配的按钮仅在连接到运行 Android 7.1(API 级别 25)或更低版本的设备时,才会出现在堆转储按钮的右侧。

  3. 用于指定分析器捕获内存分配频率的下拉菜单。选择适当的选项可能有助于您提高分析时的应用性能
  4. 用于放大/缩小时间轴的按钮。
  5. 用于跳转到实时内存数据的按钮。
  6. 事件时间轴,显示活动状态、用户输入事件和屏幕旋转事件。
  7. 内存使用时间轴,其中包括以下内容
    • 每个内存类别正在使用的内存量的堆叠图,如左侧的 y 轴和顶部的颜色键所示。
    • 虚线表示已分配对象的数目,如右侧的 y 轴所示。
    • 每个垃圾回收事件的图标。

但是,如果您使用的是运行 Android 7.1 或更低版本的设备,并非所有分析数据在默认情况下都是可见的。如果您看到一条消息,内容为“对于所选进程,高级分析不可用”,则您需要启用高级分析才能查看以下内容

  • 事件时间轴
  • 已分配对象的数目
  • 垃圾回收事件

在 Android 8.0 及更高版本上,对于可调试的应用,始终启用高级分析。

内存计数方式

您在内存分析器顶部看到的数字(图 2)是基于您的应用已提交的所有私有内存页,根据 Android 系统。此计数不包括与系统或其他应用共享的页。

图 2. 内存分析器顶部的内存计数图例

内存计数中的类别如下

  • Java:从 Java 或 Kotlin 代码分配的对象的内存。
  • Native:从 C 或 C++ 代码分配的对象的内存。

    即使您在应用中未使用 C++,您也可能会看到一些本地内存在此处使用,因为 Android 框架会使用本地内存来代表您处理各种任务,例如处理图像资产和其他图形 - 即使您编写的代码是使用 Java 或 Kotlin 编写的。

  • 图形:用于图形缓冲区队列的内存,用于将像素显示到屏幕上,包括 GL 表面、GL 纹理等。(注意,这是与 CPU 共享的内存,而不是专用的 GPU 内存。)

  • 堆栈:您的应用程序中本机和 Java 堆栈使用的内存。这通常与您的应用程序运行的线程数量有关。

  • 代码:您的应用程序用于代码和资源的内存,例如 dex 字节码、优化或编译的 dex 代码、.so 库和字体。

  • 其他:您的应用程序使用的系统无法确定的内存类别。

  • 已分配:您的应用程序分配的 Java/Kotlin 对象数量。这并不包括在 C 或 C++ 中分配的对象。

    当连接到运行 Android 7.1 及更低版本的设备时,此分配计数仅从内存分析器连接到正在运行的应用程序时开始。因此,您开始分析之前分配的任何对象都不会被计算在内。但是,Android 8.0 及更高版本包含一个设备上的分析工具,它会跟踪所有分配,因此此数字始终代表您的应用程序在 Android 8.0 及更高版本上所有未完成的 Java 对象的总数。

与之前的 Android Monitor 工具的内存计数相比,新的内存分析器以不同的方式记录您的内存,因此可能看起来您的内存使用量现在更高了。内存分析器监控一些额外的类别,这些类别会增加总数,但如果您只关心 Java 堆内存,那么“Java”数字应该与之前工具中的值类似。尽管 Java 数字可能与您在 Android Monitor 中看到的数字不完全匹配,但新数字会考虑自您的应用程序从 Zygote 分叉以来分配给应用程序 Java 堆的所有物理内存页面。因此,这提供了您应用程序实际使用的物理内存量的准确表示。

查看内存分配

内存分配向您展示了内存中每个 Java 对象和 JNI 引用是如何分配的。具体而言,内存分析器可以显示有关对象分配的以下信息

  • 分配了哪些类型的对象以及它们使用了多少空间。
  • 每个分配的堆栈跟踪,包括在哪个线程中。
  • 对象何时被释放(仅在使用运行 Android 8.0 或更高版本的设备时)。

要记录 Java 和 Kotlin 分配,请选择记录 Java/Kotlin 分配,然后选择记录。如果设备运行的是 Android 8 或更高版本,则内存分析器 UI 将过渡到一个单独的屏幕,显示正在进行的录制。您可以与记录上方的迷你时间轴进行交互(例如,更改选择范围)。要完成录制,请选择停止

Visualization of Java allocations in Memory Profiler

在 Android 7.1 及更低版本上,内存分析器使用传统分配录制,该录制会在您单击停止之前在时间轴上显示录制。

在您选择时间轴上的一个区域(或在您完成与运行 Android 7.1 或更低版本的设备的录制会话后),分配的对象列表将显示出来,按类名分组并按其堆计数排序。

要检查分配记录,请按照以下步骤操作

  1. 浏览列表以查找堆计数异常高的可能泄漏的对象。要帮助查找已知类,请单击类名列标题按字母顺序排序。然后单击类名。实例视图窗格将显示在右侧,显示该类的每个实例,如图 3 所示。
    • 或者,您可以通过单击过滤 或按 Control+F(在 Mac 上按 Command+F)并在搜索字段中输入类或包名称来快速定位对象。如果您从下拉菜单中选择按调用堆栈排列,您也可以按方法名称搜索。如果您要使用正则表达式,请选中正则表达式旁边的框。如果您的搜索查询区分大小写,请选中匹配大小写旁边的框。
  2. 实例视图窗格中,单击一个实例。调用堆栈选项卡将显示在下方,显示该实例在何处以及在哪个线程中被分配。
  3. 调用堆栈选项卡中,右键单击任何一行并选择跳转到源以在编辑器中打开该代码。

图 3. 关于每个分配对象的详细信息将显示在右侧的实例视图

您可以使用分配对象列表上方的两个菜单来选择要检查的堆以及如何组织数据。

从左侧的菜单中,选择要检查的堆

  • 默认堆:系统未指定堆时。
  • 图像堆:系统启动映像,包含在启动时预加载的类。这里的分配保证永远不会移动或消失。
  • zygote 堆:Android 系统中应用程序进程从中分叉的写时复制堆。
  • 应用程序堆:您的应用程序分配内存的主要堆。
  • JNI 堆:显示 Java 本机接口 (JNI) 引用在何处分配和释放的堆。

从右侧的菜单中,选择如何排列分配

  • 按类排列:根据类名对所有分配进行分组。这是默认设置。
  • 按包排列:根据包名称对所有分配进行分组。
  • 按调用堆栈排列:将所有分配分组到相应的调用堆栈中。

在分析过程中提高应用程序性能

为了在分析过程中提高应用程序性能,内存分析器默认情况下会定期对内存分配进行采样。在 API 级别 26 或更高版本的设备上进行测试时,您可以使用分配跟踪下拉菜单更改此行为。可用的选项如下

  • 全部:捕获内存中的所有对象分配。这是 Android Studio 3.2 及更早版本中的默认行为。如果您有一个分配大量对象的应用程序,您可能会在分析过程中观察到应用程序的明显减速。
  • 采样:定期对内存中的对象分配进行采样。这是默认选项,对分析过程中应用程序性能的影响较小。在短时间内分配大量对象的应用程序可能仍然会表现出明显的减速。
  • 关闭:停止跟踪应用程序的内存分配。

查看全局 JNI 引用

Java 本机接口 (JNI) 是一个框架,允许 Java 代码和本机代码互相调用。

JNI 引用由本机代码手动管理,因此本机代码使用的 Java 对象可能会保持过长时间的活动状态。如果在未显式删除之前放弃了 JNI 引用,Java 堆上的某些对象可能会变得不可达。此外,可能会耗尽全局 JNI 引用限制。

要解决此类问题,请使用内存分析器中的JNI 堆视图浏览所有全局 JNI 引用,并按 Java 类型和本机调用堆栈对其进行过滤。使用此信息,您可以找到全局 JNI 引用何时何地创建和删除。

在应用程序运行期间,选择要检查的时间轴的一部分,然后从类列表上方的下拉菜单中选择JNI 堆。然后,您可以像往常一样检查堆中的对象,双击分配调用堆栈选项卡中的对象,查看 JNI 引用在您的代码中分配和释放的位置,如图 4 所示。

图 4. 查看全局 JNI 引用

要检查应用程序 JNI 代码的内存分配,您必须将应用程序部署到运行 Android 8.0 或更高版本的设备。

有关 JNI 的更多信息,请参阅JNI 提示

本机内存分析器

Android Studio 内存分析器包含一个本机内存分析器,用于部署到运行 Android 10 或更高版本的物理和虚拟设备上的应用程序。

本机内存分析器跟踪本机代码中对象的分配/释放,并提供以下信息

  • 分配:在选定时间段内通过 malloc()new 运算符分配的对象数量。
  • 释放:在选定时间段内通过 free()delete 运算符释放的对象数量。
  • 分配大小:在选定时间段内所有分配的总大小(以字节为单位)。
  • 释放大小:在选定时间段内所有释放内存的总大小(以字节为单位)。
  • 总数:分配列中的值减去释放列中的值。
  • 剩余大小:分配大小列中的值减去释放大小列中的值。

Native Memory Profiler

要在运行 Android 10 或更高版本的设备上记录本机分配,请选择记录本机分配,然后选择记录。录制将持续到您单击停止 ,之后内存分析器 UI 将过渡到一个单独的屏幕,显示本机录制。

Record native allocations button

在 Android 9 及更低版本上,记录本机分配选项不可用。

默认情况下,本机内存分析器使用 32 字节的样本大小:每分配 32 字节的内存,就会拍摄内存快照。较小的样本大小会导致更频繁的快照,从而产生更准确的内存使用量数据。较大的样本大小会产生不太准确的数据,但它会在系统上消耗更少的资源,并在录制期间提高性能。

要更改本机内存分析器的样本大小

  1. 选择运行 > 编辑配置
  2. 在左侧窗格中选择您的应用程序模块。
  3. 单击分析选项卡,并在标记为本机内存采样间隔(字节)的字段中输入样本大小。
  4. 再次构建并运行您的应用程序。

捕获堆转储

堆转储显示了在您捕获堆转储时应用中的哪些对象正在使用内存。尤其是在长时间的用户会话之后,堆转储可以通过显示仍处于内存中的对象(您认为这些对象不应该再存在)来帮助识别内存泄漏。

捕获堆转储后,您可以查看以下内容:

  • 您的应用分配了哪些类型的对象,以及每种类型的对象数量。
  • 每个对象使用了多少内存。
  • 在您的代码中,对每个对象的引用在何处保存。
  • 分配对象的调用堆栈。(在 Android 7.1 及更低版本中,调用堆栈当前仅在您在记录分配时捕获堆转储的情况下可与堆转储一起使用。)

要捕获堆转储,请单击 **捕获堆转储**,然后选择 **记录**。在转储堆时,Java 内存量可能会暂时增加。这是正常的,因为堆转储发生在与您的应用相同的进程中,并且需要一些内存来收集数据。

分析器完成捕获堆转储后,内存分析器 UI 将过渡到一个单独的屏幕,显示堆转储。

图 5. 查看堆转储。

如果您需要更精确地确定转储创建的时间,则可以通过调用 dumpHprofData() 在应用代码的关键点处创建堆转储。

在类列表中,您可以看到以下信息:

  • 分配:堆中的分配数量。
  • 原生大小:此对象类型使用的原生内存总量(以字节为单位)。此列仅适用于 Android 7.0 及更高版本。

    您将在这里看到一些在 Java 中分配的对象的内存,因为 Android 对某些框架类使用原生内存,例如 Bitmap

  • 浅层大小:此对象类型使用的 Java 内存总量(以字节为单位)。

  • 保留大小:由于此类的所有实例而保留的内存总大小(以字节为单位)。

您可以使用分配对象列表上方的两个菜单来选择要检查的堆转储以及如何组织数据。

从左侧的菜单中,选择要检查的堆

  • 默认堆:系统未指定堆时。
  • 应用程序堆:您的应用程序分配内存的主要堆。
  • 图像堆:系统启动映像,包含在启动时预加载的类。这里的分配保证永远不会移动或消失。
  • zygote 堆:Android 系统中应用程序进程从中分叉的写时复制堆。

从右侧的菜单中,选择如何排列分配

  • 按类排列:根据类名对所有分配进行分组。这是默认设置。
  • 按包排列:根据包名称对所有分配进行分组。
  • 按调用堆栈排列:将所有分配分组到其相应的调用堆栈中。此选项仅在您在记录分配时捕获堆转储的情况下有效。即使如此,堆中也可能存在一些在您开始记录之前分配的对象,因此这些分配将首先显示,只是按类名列出。

默认情况下,列表按 **保留大小** 列排序。要按不同列的值排序,请单击该列的标题。

单击类名将在右侧打开 **实例视图** 窗口(如图 6 所示)。每个列出的实例都包含以下内容:

  • 深度:从任何 GC 根到所选实例的最短跳跃数。
  • 原生大小:此实例在原生内存中的大小。此列仅适用于 Android 7.0 及更高版本。
  • 浅层大小:此实例在 Java 内存中的大小。
  • 保留大小:此实例支配的内存大小(根据 支配树)。

图 6. 捕获堆转储所需的时间在时间线上显示。

要检查堆,请按照以下步骤操作:

  1. 浏览列表以查找堆计数异常高且可能发生泄漏的对象。要帮助查找已知类,请单击 **类名** 列标题以按字母顺序排序。然后单击类名。**实例视图** 窗格将出现在右侧,显示该类的每个实例,如图 6 所示。
    • 或者,您可以通过单击过滤 或按 Control+F(在 Mac 上按 Command+F)并在搜索字段中输入类或包名称来快速定位对象。如果您从下拉菜单中选择按调用堆栈排列,您也可以按方法名称搜索。如果您要使用正则表达式,请选中正则表达式旁边的框。如果您的搜索查询区分大小写,请选中匹配大小写旁边的框。
  2. 在 **实例视图** 窗格中,单击一个实例。**引用** 选项卡将出现在下方,显示对该对象的每个引用。

    或者,单击实例名称旁边的箭头以查看其所有字段,然后单击字段名称以查看其所有引用。如果要查看字段的实例详细信息,请右键单击该字段,然后选择 **转到实例**。

  3. 在 **引用** 选项卡中,如果您确定一个可能导致内存泄漏的引用,请右键单击它,然后选择 **转到实例**。这将从堆转储中选择相应的实例,显示其自己的实例数据。

在堆转储中,查找由以下任何原因造成的内存泄漏:

  • ActivityContextViewDrawable 和其他可能保存对 ActivityContext 容器的引用的对象的长期引用。
  • 非静态内部类,例如 Runnable,它可以保存 Activity 实例。
  • 缓存,它们保存对象的时长超过必要时长。

将堆转储保存为 HPROF 文件

捕获堆转储后,数据仅在分析器运行时可在内存分析器中查看。退出分析会话后,您将丢失堆转储。因此,如果您希望保存它以供以后查看,请将堆转储导出到 HPROF 文件。在 Android Studio 3.1 及更低版本中,**将捕获导出到文件** 按钮位于时间线下方工具栏的左侧;在 Android Studio 3.2 及更高版本中,在 **会话** 窗格中,每个 **堆转储** 条目右侧都有一个 **导出堆转储** 按钮。在出现的 **导出为** 对话框中,使用 .hprof 文件扩展名保存该文件。

要使用不同的 HPROF 分析器(例如 jhat),您需要将 HPROF 文件从 Android 格式转换为 Java SE HPROF 格式。您可以使用 android_sdk/platform-tools/ 目录中提供的 hprof-conv 工具来执行此操作。使用原始 HPROF 文件和要写入转换后的 HPROF 文件的位置作为两个参数运行 hprof-conv 命令。例如:

hprof-conv heap-original.hprof heap-converted.hprof

导入堆转储文件

要导入 HPROF (.hprof) 文件,请单击 **开始新的分析会话** (在 **会话** 窗格中),选择 **从文件加载**,然后从文件浏览器中选择该文件。

您还可以通过将 HPROF 文件从文件浏览器拖放到编辑器窗口中来导入它。

内存分析器中的泄漏检测

在内存分析器中分析堆转储时,您可以筛选 Android Studio 认为可能表明应用中 ActivityFragment 实例的内存泄漏的分析数据。

筛选器显示的数据类型包括以下内容:

  • Activity 实例已被销毁,但仍被引用。
  • Fragment 实例没有有效的 FragmentManager,但仍被引用。

在某些情况下,例如以下情况,筛选器可能会产生误报:

  • Fragment 已创建,但尚未使用。
  • Fragment 正在被缓存,但不是作为 FragmentTransaction 的一部分。

要使用此功能,请首先 捕获堆转储导入堆转储文件 到 Android Studio 中。要显示可能发生内存泄漏的片段和活动,请在内存分析器的堆转储窗格中选中 **Activity/Fragment 泄漏** 复选框,如图 7 所示。

Profiler: Memory Leak Detection

图 7. 筛选内存泄漏的堆转储。

分析内存的技巧

在使用内存分析器时,您应该对应用代码进行压力测试,并尝试强制发生内存泄漏。在应用中引发内存泄漏的一种方法是在检查堆之前让应用运行一段时间。泄漏可能会逐渐堆积到堆中的分配顶部。但是,泄漏越小,您需要运行应用的时间就越长才能看到它。

您还可以通过以下方式之一触发内存泄漏:

  • 在不同的活动状态下,将设备从纵向旋转到横向,然后再旋转回来多次。旋转设备通常会导致应用泄漏 ActivityContextView 对象,因为系统会重新创建 Activity,如果您的应用在其他地方保存了对这些对象之一的引用,系统将无法进行垃圾回收。
  • 在不同的活动状态下,在您的应用和另一个应用之间切换(导航到主屏幕,然后返回到您的应用)。

提示: 您也可以使用 monkeyrunner 测试框架来执行上述步骤。