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时,您将看到应用程序的内存使用的详细时间轴,和可以使用的内存工具包括强制垃圾回收、抓取堆信息和记录内存分配。

memory-profiler-callouts_2x.png

如上图所示,Memory Profiler的默认视图包括以下内容:

  1. 强制垃圾回收事件的按钮。
  2. 抓取堆信息的按钮。注意:仅当连接到运行Android7.1(API级别25)或更低版本的设备时,堆信息按钮右侧才会显示一个用于记录内存分配的按钮。
  3. 用于设置Profiler捕获内存分配的频率的下拉菜单。选择适当的选项可以帮助您在分析时提高应用程序性能。
  4. 用于放大/缩小时间轴的按钮。
  5. 一个跳转到实时内存数据的按钮。
  6. 事件时间轴,显示活动状态、用户输入事件和屏幕旋转事件。
  7. 内存使用时间轴,包括以下内容:

    • 由每一个内存类别使用多少内存的堆叠图,如左边的Y轴和顶部的颜色键所指示的。
    • 虚线表示分配的对象的数目,如右边的y轴所示。
    • 每个垃圾回收事件的图标。

但是,如果您使用的是运行Android 7.1或更低版本的设备,默认情况下并非所有分析数据都可见。如果您看到一条消息,上面写着“Advanced profiling is unavailable for the selected process”,则需要启用高级分析才能看到以下内容:

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

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

如何计算内存

你在Memory Profiler顶部看到的数字基于您的应用通过Android 系统提交的所有私有内存页面。此计数不包括与系统或其他应用程序共享的页面。

memory-profiler-counts_2x.png

内存计数中的类别如下:

  • Java:从Java或Kotlin代码中分配对象的内存。
  • Native:从C或C++代码分配对象的内存。即使您在应用程序中不使用C++,也可能会看到一些本地内存,因为Android Framework代表你使用Native内存来处理各种任务,例如处理图像资源和其他图形,即使您编写的代码是Java或KOTLIN。
  • Graphics:用于在屏幕上显示(包括GL曲面、GL纹理等)的图形缓冲队列的内存。(请注意,这是与CPU共享的内存,而不是专用的GPU内存。)
  • Stack:应用程序中Native堆栈和Java堆栈使用的内存。这通常与应用程序正在运行的线程数有关。
  • Code:应用程序用于代码和资源的内存,例如dex字节码、优化或编译的dex代码、.so库和字体。
  • Others:系统不确定如何分类的应用程序使用的内存。
  • Allocated:应用程序分配的Java/Kotlin对象数,不计算在C或C++中分配的对象。当连接到运行Android7.1及更低版本的设备时,此分配计数仅在Memory Profiler连接到正在运行的应用程序时开始。因此,在开始分析之前分配的任何对象都不会被考虑在内。但是,Android 8.0及更高版本包含一个设备内置分析工具,该工具可跟踪所有分配。因此在android8.0及更高版本上,此数字始终表示应用程序中Java对象总数。

与之前Android Monitor中内存工具的计数相比,新的Memory Profiler以不同的方式记录您的内存。因而,内存使用率看起来会更高了。Memory Profiler监视一些额外的类别,这些类别增加了总的内存。但是如果您只关心Java堆内存,那么“Java”数值应该与前一个工具中的值类似。但是Java数值可能与您在Android Monitor中看到的不完全匹配,新数值统计了自从Zygote派生以来应用程序的Java堆分配的所有物理页面。因此,它提供了应用程序实际使用的物理内存量的精确表示。

注意:使用搭载 Android 8.0(API 级别 26)及更高版本的设备时,Memory Profiler 还会显示应用中的一些误报的原生内存使用量,而这些内存实际上是分析工具使用的。对于大约 100000 个对象,最多会使报告的内存使用量增加 10MB。在 IDE 的未来版本中,这些数字将从您的数据中过滤掉。

查看内存分配

内存分配向您展示了内存中的每个Java对象和JNI引用是如何分配。具体来说,Memory Profiler可以向您显示以下有关对象分配的信息:

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

如果您的设备运行的是Android 8.0或更高版本,您可以随时查看对象分配,如下所示:在时间轴中拖动以选择要查看分配的区域。不需要开始录制会话,因为Android8.0及更高版本包含一个设备内置分析工具,可以不断跟踪应用程序的分配。详细内容可以参考视频:高版本查看内存分配

如果您的设备运行的是Android 7.1或更低版本,请单击Memory Profiler工具栏中的Record memory allocations图标。录制时,Memory Profiler会跟踪应用程序中发生的所有分配。完成后,单击Stop recording图标以查看分配。详细内容可以参考视频:低版本查看内存分配

选择时间线的某个区域后(或在使用运行Android7.1或更低版本的设备完成录制会话时),已分配对象的列表将显示在时间线下方,按类名分组并按堆计数排序。

注意:在 Android 7.1 及更低版本上,您最多可以记录 65535 个分配。如果您的记录会话超出此限制,则记录中仅保存最新的 65535 个分配。(在 Android 8.0 及更高版本上,则没有实际的限制。)

要检查分配记录,请执行以下步骤:

  1. 浏览列表以查找堆计数异常大且可能泄漏的对象。若要查找已知类,请单击Class Name列标题按字母顺序排序,然后单击类名。Instance View窗格将出现在右侧,显示该类的每个实例,如下图所示。或者,可以通过单击Filter 图标或按 Ctrl+F 键,并在搜索字段中输入类或包名称来快速定位对象。如果从下拉菜单中选择Arrange by callstack,还可以按方法名进行搜索。如果要使用正则表达式,请选中Regex旁边的复选框。如果搜索查询区分大小写,请选中Match case旁边的复选框。
  2. Instance View窗格中,单击实例。Call Stack标签将出现在下面,显示该实例的分配位置和线程。
  3. Call Stack标签中,右键单击任意行,然后选择Jump to Source,就可以在编辑器中打开该代码。

memory-profiler-allocations-detail_2x.png

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

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

  • default heap:系统未指定堆时。
  • image heap:系统启动镜像,包含在启动期间预加载的类。这里的分配保证不会移动或消失。
  • zygote heap:当应用程序进程从Android系统中派生出来时的写时拷贝堆。
  • app heap:应用程序分配内存的主堆。
  • JNI heap:这个堆显示了Java Native接口(JNI)引用的分配和释放的位置。

从右侧的菜单中,选择如何组织分配:

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

在分析时提高应用程序性能

为了提高分析时的应用程序性能,默认情况下,内存探查器定期对内存分配进行采样。在运行API级别26或更高级别的设备上测试时,可以使用Allocation Tracking下拉列表更改此行为。可用选项如下:

  • Full:捕获内存中的所有对象分配。这是Android Studio 3.2和更早版本中的默认行为。如果您的应用程序分配了很多对象,那么在分析时,您可能会看到应用程序的速度明显减慢。
  • Sampled:定期采样内存中的对象分配。这是默认选项,在分析时对应用程序性能的影响较小。在短时间内分配大量对象的应用程序可能仍然会显示出明显的减速。
  • Off:停止跟踪应用程序的内存分配。
注意:默认情况下,Android Studio在执行CPU录制时停止跟踪实时分配,并在CPU录制完成后将其重新打开。您可以在“CPU记录配置”对话框中更改此行为。

查看全局JNI引用

Java Native Interface(JNI)是一个允许Java代码和Native代码相互调用的框架。JNI引用是由Native代码进行管理的,因此Native代码使用的Java对象可能会存活很长时间。如果在没有显式删除JNI引用的情况下丢弃JNI引用,Java堆上的某些对象可能会变得不可访问。此外,还可能会耗尽全局JNI引用的限制。要解决此类问题,请使用Memory Profiler中的JNI heap来浏览所有全局JNI引用,并按Java类型和Native调用堆栈筛选它们。有了这些信息,您可以找到何时何地创建和删除全局JNI引用。

当应用程序运行时,选择要检查的时间轴的一部分,然后从类列表上方的下拉菜单中选择JNI heap。接下来,您就可以像往常一样检查堆中的对象,并双击Allocation Call Stack选项卡中的对象,查看在代码中JNI引用的分配和释放的位置,如下图所示。
memory-profiler-jni-heap_2x.png

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

抓取堆信息

堆信息能显示在抓取堆信息时应用程序中的哪些对象正在使用内存。特别是在长时间的用户会话之后,通过分析堆信息中是否存在您认为不应该存在的对象,可以用来帮助识别内存泄漏。抓取堆信息后,你可以查看以下内容:

  • 你的应用程序分配了哪些类型的对象,以及每种对象的数量。
  • 每个对象正在使用的内存量。
  • 在代码中保存对每个对象的引用的位置。
  • 分配对象的调用堆栈。(调用堆栈当前仅在Android 7.1及更低版本中提供,只有在分配期间抓取堆信息才能显示。)

要抓取堆信息,请单击Memory Profiler工具栏中的Dump Java heap图标。在抓取期间,Java内存量可能会临时增加。这是正常的,因为堆抓取发生在与应用程序相同的进程中,需要一些内存来收集数据。堆信息在内存时间轴的下方,显示堆中所有类的类型,如下图所示。

memory-profiler-dump_2x.png

如果需要更精确地了解堆的抓取时间,可以通过调用dumpHprofData()在应用程序代码的关键点抓取堆信息。

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

  • Allocations:堆中的分配数。
  • Native Size:此对象类型使用的Native内存总量(字节)。此列仅适用于Android 7.0及更高版本。这里您将看到Java中分配的一些对象的内存,因为Android在一些framework类(如位图)使用Native内存。
  • 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列排序。若要按其他列中的值排序,请单击该列的标题。

单击类名打开右侧的IInstance View窗口(如下图所示),每个列出的实例包括以下内容:

  • Depth:从任意GC根到所选实例的最短跃点数。
  • Native Size:Native内存中此实例的大小。此列仅适用于Android 7.0及更高版本。
  • Shallow Size:这个实例在Java内存中的大小。
  • Retained Size:此实例支配的内存大小(根据支配树)。
注意:默认情况下,堆信息不会向您显示每个已分配对象的堆栈轨迹。要获取堆栈轨迹,在点击 Dump Java heap 之前,您必须先开始记录内存分配。然后,您可以在 Instance View 中选择一个实例,并查看 References 标签旁边的 Call Stack 标签,如下图所示。不过,在您开始记录分配之前,可能已分配一些对象,因此不会显示这些对象的调用堆栈。在包含调用堆栈的实例在图标上会有一个“堆栈”标志表示。(遗憾的是,由于堆栈轨迹需要您执行分配记录,因此您目前无法在 Android 8.0 上查看堆信息的堆栈轨迹。)

memory-profiler-dump-stacktrace_2x.png

要检查堆信息,请执行以下步骤:

  1. 浏览列表以查找堆计数异常大且可能泄漏的对象。若要查找已知类,请单击Class Name列标题按字母顺序排序。然后单击类名。Instance View窗格会出现在右侧,显示该类的每个实例,如上图所示。或者,可以通过单击Filter图标或按Control+F,并在搜索字段中输入类或包名称来快速定位对象。如果从下拉菜单中选择Arrange by callstack,还可以按方法名进行搜索。如果要使用正则表达式,请选中Regex旁边的复选框。如果搜索查询区分大小写,请选中Match case旁边的复选框。
  2. Instance View窗格中,单击实例。References标签将出现在下面,显示对该对象的每个引用。或者,单击实例名称旁边的箭头以查看其所有字段,然后单击字段名称以查看其所有引用。如果要查看某个字段的实例详细信息,请右键单击该字段并选择Go to Instance
  3. References标签中,如果标识了可能泄漏内存的引用,请右键单击该引用并选择Go to Instance。这将从堆信息中选择相应的实例,从而显示其自己的实例数据。

在堆信息中,注意以下情况可能导致的内存泄漏:

  • 对Activity、Context、View、Drawable和其他对象的长时间引用可能包含对Activity或Context的引用。
  • 可以保存Activity的非静态内部类,如Runnable。
  • 保存对象的时间超过所需时长的缓存。

将堆信息另存为HPROF文件

捕获堆信息后,只有在Profiler运行时,数据才能在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命令。例如:

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

导入堆信息文件

要导入HPROF(.hprof)文件,请单击Sessions窗格中的Start a new profiling session图标,选择Load from file,然后从文件浏览器中选择该文件。也可以通过将HPROF文件从文件浏览器拖动到编辑器窗口中来导入该文件。

分析内存的技巧

在使用Memory Profiler时,您应该给应用程序代码增加压力,试图去暴露内存泄漏。引发应用程序内存泄漏的一种方法是在检查堆之前让它运行一段时间,泄漏可能会逐渐汇集到堆中分配的顶部。但是当泄漏越小时,应用程序就需要运行越长时间,再进行泄漏检查。

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

  • 在不同的Activity状态下,将设备从纵向旋转到横向,再旋转到纵向,如此进行多次旋转。旋转设备通常会导致应用程序泄漏Activity、Context 或 View对象,因为系统会重新创建该Activity,如果应用程序在其他地方保存了一个对象的引用,则系统无法对其进行垃圾回收。
  • 在处于不同Activity状态时在应用程序之间进行切换(导航到主屏幕,然后返回您的应用程序)。

参考文档:

Android Developers: memory profiler


戈壁老王
143 声望64 粉丝

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,