Android内存分析工具 — Memory Profiler

目录

前言

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

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

为帮助防止这些问题,我们可以使用Memory Profiler

  • 实时图表展示应用内存使用量
  • 识别内存泄漏、抖动
  • 提供捕获堆转储、强制GC以及跟踪内存分配的能力

Memory Profiler 概览

图 1. Memory Profiler

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

  1. 强制执行垃圾回收

  2. 堆转储,把内存信息通过文件的方式保存下来,可以进行分析

  3. 记录内存分配情况, 此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示

  4. 放大/缩小时间线

  5. 跳转至实时内存数据

  6. Event 时间线,显示 Activity 状态、用户输入 Event 和屏幕旋转 Event

  7. 内存使用量时间线,其包含以下内容:

    • 一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示

    • 虚线表示分配的对象数,如右侧的 y 轴所示

    • 用于表示每个垃圾回收 Event 的图标

如何计算内存占用

图 2. Memory Profiler 顶部的内存计数图例

内存计数中的类别如下所示:

  • Java:从 Java 或 Kotlin 代码分配的对象内存

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

    即使你的应用中不使用 C++,你也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使你编写的代码采用 Java 或 Kotlin 语言

  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存)

  • Stack: 应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关

  • Code:应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存

  • Other:应用使用的系统不确定如何分类的内存

  • Allocated:您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象

    当连接至运行 Android 7.1 及更低版本的设备时,此分配仅在 Memory Profiler 连接至你运行的应用时才开始计数。 因此,你开始分析之前分配的任何对象都不会被计入。 不过,Android 8.0 附带一个设备内置分析工具,该工具可记录所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示你的应用中待处理的 Java 对象总数

Java 数字可能与你在 Android Monitor 中看到的数字并非完全相同,这是因为应用的 Java 堆是从 Zygote 启动的,而新数字则计入了为它分配的所有物理内存页面。 因此,它可以准确反映你的应用实际使用了多少物理内存

查看内存分配

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

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

默认情况下,左侧的分配列表按类名称排列。 在列表顶部,你可以使用右侧的下拉列表在以下排列方式之间进行切换:

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

捕获堆转储

堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存

要捕获堆转储,在 Memory Profiler 工具栏中点击 Dump Java heap

。 在转储堆期间,Java 内存量可能会暂时增加。因为堆转储与您的应用发生在同一进程中,并需要一些内存来收集数据

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

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

  1. 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 然后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每个实例,如图 5 中所示
  2. Instance View窗格中,点击一个实例。此时下方将出现References,显示该对象的每个引用
  3. References 标签中,如果您发现某个引用可能在泄漏内存,则右键点击它并选择 Go to Instance

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

  • 长时间引用 ActivityContextViewDrawable 和其他对象,可能会保持对 ActivityContext 容器的引用
  • 可以保持 Activity 实例的非静态内部类,如 Runnable
  • 对象保持时间超出所需时间的缓存
图 4. 捕获堆转储需要的持续时间标示在时间线中

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

  • Heap Count:堆中的实例数
  • Shallow Size:此堆中所有实例的总大小(以字节为单位)
  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)

在类列表顶部,你可以使用左侧下拉列表在以下堆转储之间进行切换:

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

Instance View 中,每个实例都包含以下信息:

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

分析内存的技巧

使用 Memory Profiler 时,你可以应用代码施加压力并尝试强制内存泄漏。 在应用中引发内存泄漏的一种方式是,先让其运行一段时间,然后再检查堆。 泄漏在堆中可能逐渐汇聚到分配顶部。 不过,泄漏越小,你越需要运行更长时间的应用才能看到泄漏

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

  • 将设备从纵向旋转为横向,然后在不同的 Activity 状态下反复操作多次。 旋转设备经常会导致应用泄漏 ActivityContextView 对象,因为系统会重新创建 Activity,而如果您的应用在其他地方保持对这些对象之一的引用,系统将无法对其进行垃圾回收
  • 处于不同的 Activity 状态时,在您的应用与另一个应用之间切换(导航到主屏幕,然后返回到您的应用)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。