目录
前言
Android 存在内存回收机制,当它确定应用不再使用某些对象时,垃圾回收器会将未使用的内存释放回堆中。 虽然 Android 查找未使用内存的方式在不断改进,但对于所有 Android 版本,系统都必须在某个时间点短暂地暂停你写的代码。 大多数情况下,这些暂停难以察觉。 但是,如果你的应用分配内存的速度比系统回收内存的速度快,那么当释放足够的内存以满足应用的分配需要时,应用就可能出现延迟。 这样可能会导致应用跳帧,并使系统明显变慢
如果存在内存泄漏,则即使应用在后台运行也会保留该内存。 此行为会强制执行不必要的垃圾回收事件,因而拖慢系统的内存性能。 最后,系统被迫终止你的应用进程以回收内存。 然后,当用户返回你的应用时,就必须完全重启
为帮助防止这些问题,我们可以使用Memory Profiler
- 实时图表展示应用内存使用量
- 识别内存泄漏、抖动
- 提供捕获堆转储、强制GC以及跟踪内存分配的能力
Memory Profiler 概览
如图 1 所示,Memory Profiler 的默认视图包括以下各项:
强制执行垃圾回收
堆转储,把内存信息通过文件的方式保存下来,可以进行分析
记录内存分配情况, 此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示
放大/缩小时间线
跳转至实时内存数据
Event 时间线,显示 Activity 状态、用户输入 Event 和屏幕旋转 Event
-
内存使用量时间线,其包含以下内容:
一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示
虚线表示分配的对象数,如右侧的 y 轴所示
用于表示每个垃圾回收 Event 的图标
如何计算内存占用
内存计数中的类别如下所示:
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 启动的,而新数字则计入了为它分配的所有物理内存页面。 因此,它可以准确反映你的应用实际使用了多少物理内存
查看内存分配
要检查内存分配记录,可以按以下步骤操作:
- 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 点击 Class Name 列标题以按字母顺序排序。 然后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每个实例,如图 3 中所示
- 在 Instance View 窗格中,点击一个实例。 此时下方将出现 Call Stack 标签,显示该实例被分配到何处以及哪个线程中
- 在 Call Stack 标签中,点击任意行以在编辑器中跳转到该代码
默认情况下,左侧的分配列表按类名称排列。 在列表顶部,你可以使用右侧的下拉列表在以下排列方式之间进行切换:
- Arrange by class:基于类名称对所有分配进行分组
- Arrange by package:基于软件包名称对所有分配进行分组
- Arrange by callstack:将所有分配分组到其对应的调用堆栈
捕获堆转储
堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存
要捕获堆转储,在 Memory Profiler 工具栏中点击 Dump Java heap
注:如果您需要更精确地了解转储的创建时间,可以通过调用
Debug.dumpHprofData()
在应用代码的关键点创建堆转储
要检查堆信息,请按以下步骤操作:
- 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 然后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每个实例,如图 5 中所示
- 在Instance View窗格中,点击一个实例。此时下方将出现References,显示该对象的每个引用
- 在 References 标签中,如果您发现某个引用可能在泄漏内存,则右键点击它并选择 Go to Instance
在堆转储中,请注意由下列任意情况引起的内存泄漏:
- 长时间引用
Activity
、Context
、View
、Drawable
和其他对象,可能会保持对Activity
或Context
容器的引用 - 可以保持
Activity
实例的非静态内部类,如Runnable
- 对象保持时间超出所需时间的缓存
在类列表中,你可以查看以下信息:
- 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 状态下反复操作多次。 旋转设备经常会导致应用泄漏
Activity
、Context
或View
对象,因为系统会重新创建Activity
,而如果您的应用在其他地方保持对这些对象之一的引用,系统将无法对其进行垃圾回收 - 处于不同的 Activity 状态时,在您的应用与另一个应用之间切换(导航到主屏幕,然后返回到您的应用)