利用 Android Profiler 评估应用性能-Memory Profile

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

要打开 Memory Profiler,请按以下步骤操作:

  1. 依次点击 View > Tool Windows > Profiler(您也可以点击工具栏中的 Profile 图标 )。
  2. 从 Android Profiler 工具栏中选择要分析的设备和应用进程。如果您已通过 USB 连接设备但系统未列出该设备,请确保您已启用 USB 调试。
  3. 点击 MEMORY 时间轴上的任意位置以打开 Memory Profiler。

为什么应分析您的应用内存

Android 提供了托管内存环境 - 当它确定您的应用不再使用某些对象时,垃圾回收器会将未使用的内存释放回堆中。虽然 Android 查找未使用内存的方式在不断改进,但对于所有 Android 版本,系统都必须在某个时间点短暂地暂停您的代码。大多数情况下,这些暂停难以察觉。不过,如果您的应用分配内存的速度比系统回收内存的速度快,则当回收器释放足够的内存以满足您的分配需要时,您的应用可能会延迟。此延迟可能会导致您的应用跳帧,并使系统明显变慢。

尽管您的应用不会表现出变慢,但如果存在内存泄露,则即使应用在后台运行也会保留该内存。此行为会强制执行不必要的垃圾回收事件,因而拖慢系统其余部分的内存性能。最后,系统被迫终止您的应用进程以回收内存。然后,当用户返回您的应用时,它必须完全重启。

为帮助防止这些问题,您应使用 Memory Profiler 执行以下操作:

  • 在时间轴上查找可能会导致性能问题的不理想的内存分配模式。
  • 转储 Java 堆以查看在任何给定时间哪些对象耗尽了内存。在很长一段时间内进行多次堆转储有助于识别内存泄露。
  • 记录正常用户交互和极端用户交互期间的内存分配,以准确识别您的代码在何处短时间内分配了过多对象,或分配了泄露的对象。

Memory Profiler 概览

当您首次打开 Memory Profiler 时,您将看到一条表示应用内存使用量的详细时间轴,并可使用各种工具来强制执行垃圾回收、捕获堆转储以及记录内存分配。

如图 1 所示,Memory Profiler 的默认视图包括以下各项:

选择时间轴的某个区域后(或者使用搭载 Android 7.1 或更低版本的设备完成记录会话后),已分配对象的列表将显示在时间轴下方,按类名称进行分组,并按其堆计数排序。

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

  1. 浏览列表以查找堆计数异常大且可能存在泄露的对象。为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。然后,点击一个类名称。此时右侧将出现 Instance View 窗格,显示该类的每个实例。此外,您也可以快速找到对象,方法是点击 Filter 图标 ,或按 Ctrl+F 键(在 Mac 上,按 Command+F 键),然后在搜索字段中输入类或软件包名称。如果从下拉菜单中选择 Arrange by callstack,还可以按方法名称搜索。如果要使用正则表达式,请勾选 Regex 旁边的复选框。如果您的搜索查询区分大小写,请勾选 Match case 旁边的复选框。
  2. Instance View 窗格中,点击一个实例。此时下方将出现 Call Stack 标签页,显示该实例被分配到何处以及在哪个线程中。
  3. Call Stack 标签页中,右键点击任意行并选择 Jump to Source,以在编辑器中打开该代码。

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

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

  • default heap:当系统未指定堆时。
  • image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失。
  • zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。
  • app heap:您的应用在其中分配内存的主堆。
  • JNI heap:显示 Java 原生接口 (JNI) 引用被分配和释放到什么位置的堆。

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

  • Arrange by class
    :根据类名称对所有分配进行分组。这是默认选项。
  • Arrange by package:根据软件包名称对所有分配进行分组。
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈。

在分析时提高应用性能

为了在分析时提高应用性能,Memory Profiler 在默认情况下会定期对内存分配进行采样。在运行 API 级别 26 或更高级别的设备上进行测试时,您可以使用 Allocation Tracking 下拉菜单来更改此行为。可用选项如下:

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

查看全局 JNI 引用

Java 原生接口 (JNI) 是一个允许 Java 代码和原生代码相互调用的框架。

JNI 引用由原生代码进行管理,因此原生代码使用的 Java 对象可能会保持活动状态太长时间。如果丢弃了 JNI 引用而未先明确将其删除,Java 堆上的某些对象可能会变得无法访问。此外,还可能会达到全局 JNI 引用限制。

要排查此类问题,请使用 Memory Profiler 中的 JNI heap 视图来浏览所有全局 JNI 引用,并按 Java 类型和原生调用堆栈对其进行过滤。借助此信息,您可以了解创建和删除全局 JNI 引用的时间和位置。

在您的应用运行时,选择您要检查的一部分时间轴,然后从类列表上方的下拉菜单中选择 JNI heap。 您随后可以像往常一样检查堆中的对象,还可以双击 Allocation Call Stack 标签页中的对象,以查看在代码中将 JNI 引用分配和释放到了什么位置,如图 4 所示。

捕获堆转储

堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存。特别是在长时间的用户会话后,堆转储会显示您认为不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄露。

捕获堆转储后,您可以查看以下信息:

  • 您的应用分配了哪些类型的对象,以及每种对象有多少。
  • 每个对象当前使用多少内存。
  • 在代码中的什么位置保持着对每个对象的引用。
  • 对象所分配到的调用堆栈。(目前,对于 Android 7.1 及更低版本,只有在记录分配期间捕获堆转储时,才会显示调用堆栈的堆转储。)

要捕获堆转储,请点击 Memory Profiler 工具栏中的 Dump Java heap 图标 。 在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与您的应用发生在同一进程中,并需要一些内存来收集数据。

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

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

  • Allocations:堆中的分配数。
  • Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列。您会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存。
  • Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)。
  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。

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

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

  • default heap:当系统未指定堆时。
  • app heap:您的应用在其中分配内存的主堆。
  • image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失。
  • zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。

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

  • Arrange by class:根据类名称对所有分配进行分组。这是默认选项。
  • Arrange by package:根据软件包名称对所有分配进行分组。
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈。只有在记录分配期间捕获堆转储时,此选项才有效。即便如此,堆中也很可能有在您开始记录之前分配的对象,所以会先显示这些分配,直接按类名称列出它们。

默认情况下,此列表按 Retained Size 列排序。要按其他列中的值排序,请点击该列的标题。

点击一个类名称可在右侧打开 Instance View 窗口(如图 6 所示)。列出的每个实例都包含以下信息:

  • Depth:从任意 GC 根到选定实例的最短跳数。
  • Native Size:原生内存中此实例的大小。 只有在使用 Android 7.0 及更高版本时,才会看到此列。
  • Shallow Size:Java 内存中此实例的大小。
  • Retained Size:此实例所支配内存的大小(根据支配项树)。

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

  1. 浏览列表以查找堆计数异常大且可能存在泄露的对象。为帮助查找已知类,点击
    Class Name 列标题以按字母顺序排序。然后,点击一个类名称。此时右侧将出现 Instance View 窗格,显示该类的每个实例。此外,您也可以快速找到对象,方法是点击 Filter 图标 ,或按 Ctrl+F 键(在 Mac 上,按 Command+F 键),然后在搜索字段中输入类或软件包名称。如果从下拉菜单中选择 Arrange by callstack,还可以按方法名称搜索。如果要使用正则表达式,请勾选 Regex 旁边的复选框。如果您的搜索查询区分大小写,请勾选 Match case 旁边的复选框。
  2. Instance View 窗格中,点击一个实例。此时下方将出现 References 标签页,显示对该对象的每个引用。或者,点击实例名称旁边的箭头以查看其所有字段,然后点击一个字段名称以查看其所有引用。如果您要查看某个字段的实例详细信息,请右键点击该字段并选择 Go to Instance
  3. References 标签中,如果您发现某个引用可能在泄露内存,请右键点击它并选择 Go to Instance。这样会从堆转储中选择相应的实例,从而向您显示它自己的实例数据。

在您的堆转储中,请注意由下列任意情况引起的内存泄露:

  • 长时间引用 Activity、Context、View、Drawable 和其他对象,可能会保持对 或 Context 容器的引用。
  • 可以保持 Activity 实例的非静态内部类,如 Runnable。
  • 对象保持时间比所需时间长的缓存。

将堆转储另存为 HPROF 文件

捕获堆转储后,只有在 Memory Profiler 正在运行时,才能在该分析器中查看数据。当您退出分析会话时,会丢失堆转储。因此,如果您要保存堆转储以供日后查看,请将其导出到 HPROF 文件。在 Android Studio 3.1 及更低版本中,Export capture to file 按钮 位于时间轴下方工具栏的左侧;在 Android Studio 3.2 及更高版本中,Sessions 窗格中每个 Heap Dump 条目的右侧都有一个 Export Heap Dump 按钮。在随即显示的 Export As 对话框中,使用 .hprof 文件扩展名保存文件。

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

<code>hprof-conv heap-original.hprof heap-converted.hprof
/<code>

导入堆转储文件

要导入一个 HPROF (.hprof) 文件,请点击 Sessions 窗格中的 Start a new profiling session 图标,选择 Load from file,然后从文件浏览器中选择该文件。

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


分享到:


相關文章: