Android内存优化工具:Memory Profiler

一、简介

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

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

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

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

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

  • 在时间线中查找可能会导致性能问题的不理想的内存分配模式。
  • 转储 Java 堆以查看在任何给定时间哪些对象耗尽了使用内存。 长时间进行多个堆转储可帮助识别内存泄漏。
  • 记录正常用户交互和极端用户交互期间的内存分配以准确识别您的代码在何处短时间分配了过多对象,或分配了泄漏的对象。
  • 三、Memory Profiler 概览

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

  • Java:从 Java 或 Kotlin 代码分配的对象内存。
  • Native:从 C 或 C++ 代码分配的对象内存。
    即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架也会使用原生内存处理各种任务。
  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Stack: 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。
  • Other:您的应用使用的系统不确定如何分类的内存。
  • Allocated:您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。
  • 四、查看内存分配

    内存分配显示内存中每个对象是如何分配的。 具体而言,Memory Profiler 可为您显示有关对象分配的以下信息:

  • 分配哪些类型的对象以及它们使用多少空间。
  • 每个分配的堆叠追踪,包括在哪个线程中。
  • 对象在何时被取消分配(仅当使用运行 Android 8.0 或更高版本的设备时)。
  • 如果您的设备运行 Android 8.0 或更高版本,您可以随时按照下述方法查看您的对象分配: 只需点击并按住时间线,并拖动选择您想要查看分配的区域(如下图所示)。 不需要开始记录会话,因为 Android 8.0 及更高版本附带设备内置分析工具,可持续跟踪您的应用分配。

  • 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 然后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每个实例,如图中所示。
  • 在 Instance View 窗格中,点击一个实例。 此时下方将出现 Call Stack 标签,显示该实例被分配到何处以及哪个线程中。
  • 在 Call Stack 标签中,点击任意行以在编辑器中跳转到该代码。

  • Arrange by class:基于类名称对所有分配进行分组。
  • Arrange by package:基于软件包名称对所有分配进行分组。
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈。
  • 五、捕获堆转储

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

  • 您的应用已分配哪些类型的对象,以及每个类型分配多少。
  • 每个对象正在使用多少内存。
  • 在代码中的何处仍在引用每个对象。
  • 对象所分配到的调用堆栈。 (目前,如果您在记录分配时捕获堆转储,则只有在 Android 7.1 及更低版本中,堆转储才能使用调用堆栈。)

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

    堆转储显示在内存时间线下,显示堆中的所有类类型,如上图所示。

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

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

  • 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 然后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每个实例,如图 5 中所示。
  • Instance View 窗格中,点击一个实例。此时下方将出现 References ,显示该对象的每个引用。
    或者,点击实例名称旁的箭头以查看其所有字段,然后点击一个字段名称查看其所有引用。 如果您要查看某个字段的实例详情,右键点击该字段并选择 Go to Instance
  • References 标签中,如果您发现某个引用可能在泄漏内存,则右键点击它并选择 Go to Instance 。 这将从堆转储中选择对应的实例,显示您自己的实例数据。
  • 默认情况下,堆转储 不会 向您显示每个已分配对象的堆叠追踪。 要获取堆叠追踪,在点击 Dump Java heap 之前,您必须先开始 记录内存分配 。 然后,您可以在 Instance View 中选择一个实例,并查看 Call Stack 标签以及 References 标签,如图 5 所示。不过,在您开始记录分配之前,可能已分配一些对象,因此,调用堆栈不能用于这些对象。 包含调用堆栈的实例在图标

    上用一个“堆栈”标志表示。 (遗憾的是,由于堆叠追踪需要您执行分配记录,因此,您目前无法在 Android 8.0 上查看堆转储的堆叠追踪。)

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

  • 长时间引用 Activity Context View Drawable 和其他对象,可能会保持对 Activity Context 容器的引用。
  • 可以保持 Activity 实例的非静态内部类,如 Runnable
  • 对象保持时间超出所需时间的缓存。
  • Heap Count:堆中的实例数。
  • Shallow Size:此堆中所有实例的总大小(以字节为单位)。
  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。
  • 在类列表顶部,您可以使用左侧下拉列表在以下堆转储之间进行切换:

  • Default heap:系统未指定堆时。
  • App heap:您的应用在其中分配内存的主堆。
  • Image heap:系统启动映像,包含启动期间预加载的类。 此处的分配保证绝不会移动或消失。
  • Zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。
  • 默认情况下,此堆中的对象列表按类名称排列。 您可以使用其他下拉列表在以下排列方式之间进行切换:

  • Arrange by class:基于类名称对所有分配进行分组。
  • Arrange by package:基于软件包名称对所有分配进行分组。
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈。 此选项仅在记录分配期间捕获堆转储时才有效。 即使如此,堆中的对象也很可能是在您开始记录之前分配的,因此这些分配会首先显示,且只按类名称列出。
  • 默认情况下,此列表按 Retained Size 列排序。 您可以点击任意列标题以更改列表的排序方式。
    在 Instance View 中,每个实例都包含以下信息:

  • Depth:从任意 GC 根到所选实例的最短 hop 数。
  • Shallow Size:此实例的大小。
  • Retained Size:此实例支配的内存大小