眼瞅着还有一个月就过年,项目也没有那么忙了,技术老大要求做一做性能优化方面的工作。
而我的任务就是把项目中的内存泄漏撸一遍,然后安排对应的人处理。

说到内存泄漏,我也算是老手了,其实在 2016 年我就写个内存泄漏方面的文章:

Android 如何有效的解决内存泄漏的问题

把内存泄漏的地方找出来

说干就干,首要的任务就是把内存泄漏的代码揪出来,我选择 leakcanary
github 地址:https://github.com/square/leakcanary/

关于 leakcanary 的介绍,集成步骤,我就不在这里展开讲述了,网上有很多文章,你们自己搜索一下。

我只啰嗦一点:

  • 1、 leakcanary 2.0之后的版本集成不需要初始化

集成完成后,把项目跑起来,过一会就会报出来很多内存泄漏的日志。
在 Android Studio logcat 过滤 LeakCanary 就会看到如下:
Android 如何做一次内存泄漏大排查_内存泄漏
同时在手机桌面也会有一个 小鸟图标,点击会看到可视化页面
Android 如何做一次内存泄漏大排查_LeakCanary_02
这里就可以看到内存泄漏的链条,分析的方式是 从下往上 的顺序,比如针对本图:

首先 MainActivity 实例发生内存泄漏 --> 再往上可以看到 MainActivity 泄漏 原因是 MainActivity 里的一个 Lambda 表达式引起的 --> --> 再往上 看到 MutableLiveData 引起的 --> -->

看到这个分析链条,我们就很清楚了,大概率是 MutableLiveData 对象引起的,再结合实际的项目代码,最后发现果然是因为 MainActivity 里的 MutableLiveData 对象没有释放。

特别要注意的是:

纵然 leakcanary 工具很牛逼,但是要想清晰的定位,然后修复内存泄漏,还是要结合实际的项目代码的。

到这里我们基本就完成了 把内存泄漏揪出来 的问题。

leakcanary 状态

在上面一部分,我贴了两个图,图中的有很清晰的对象引用链条。leakcanary 对每个对象都标明了泄漏的状态。

  • Leaking: YES 确定已经泄漏
  • Leaking: UNKNOWN 不确定是否泄漏
  • Leaking: NO 没有泄漏

我们在分析 对象引用链条的时候,要特别注意 UNKNOWN 状态,这个状态即有可能是泄漏了,也有可能是没有泄漏,这就需要我们程序要认真的分析项目代码,然后给出结论

hprof

leakcanary 在运行的时候,发现内存泄漏了,会把 Java堆快照转储到Android HPROF文件中,方便开发者分析。

如何获取 HPROF

方式一:通过 logcat 获取

在 Android Studio 的 logcat 中会输出 hprof 文件地址:

Android 如何做一次内存泄漏大排查_内存泄漏_03
我们拿到 hprof 文件链接后,就可以通过 adb pull 命令导到电脑桌面

adb pull /storage/emulated/0/Download/leakcanary-com.cootek.crazyreader/2021-01-08_11-21-08_472.hprof ~/DeskTop

方式二:通过客户端可视化页面

Android 如何做一次内存泄漏大排查_LeakCanary_04
点击 Share Heap Dump file 可以通过分享出去。

正文到这里其实也就结束了。在做内存泄漏排查的时候用到了 AndroidStudio Profiler 工具,里面有很多新的概念和内存指标,下面的内容就是在探究 Profiler 工具如何使用以及各种内存指标所代表的含义

如何打开 hprof 文件

方式一: Android studio Profiler 功能打开
Android 如何做一次内存泄漏大排查_Android 内存泄漏_05
这个工具显示了如下信息:

Class name Total Count 该类的实例总数 Heap Count 所选择的堆中该类的实例的数量 Sizeof 单个实例所占空间大小(如果每个实例所占空间大小不一样则显示0) Shallow Size 堆里所有实例大小总和(Heap Count * Sizeof) Retained Size 该类所有实例所支配的内存大小 Instance 具体的实例 Reference Tree 所选实例的引用,以及指向该引用的引用。 Depth GC根节点到所选实例的最短路径的深度 Shallow Size 所选实例的大小 Dominating Size 所选实例所支配的内存大小

用HPROF分析工具,可以检测到泄漏的 Activity
Android 如何做一次内存泄漏大排查_LeakCanary_06
通过这个可以看到 本次分析有 3 出泄漏的地方,点击 第一个 ReaderActivity
Android 如何做一次内存泄漏大排查_zhaoyanjun_07
可以看到详细的泄漏实例,Depth 为 13 ,代表 GC根节点到所选ReaderActivity实例的最短路径的深度是 13 。

Heap Dump

Heap Dump 是什么?

Heap Dump 也叫堆转储文件,是一个 Java 进程在某个时间点上的内存快照。 Heap Dump 是有着多种类型的。不过总体上 heap dump 在触发快照的时候都保存了 java 对象和类的信息。通常在写 heap dump 文件前会触发一次 FullGC ,所以 heap dump 文件中保存的是 FullGC 后留下的对象信息。

简单说就是:

heap dump 文件是一个二进制文件,它保存了某一时刻 JVM 堆中对象使用情况。 HeapDump 文件是指定时刻的 Java 堆栈的快照,是一种镜像文件。

Heap Dump里面有什么?

一般在 Heap Dump 文件中可以获取到(这仍然取决于heap dump文件的类型)如下信息:

  • 对象信息:类、成员变量、引用值;
  • 类信息:类加载器、名称、超类、静态成员;
  • Garbage Collections Roots:JVM可达的对象;
  • 线程栈以及本地变量:获取快照时的线程栈信息,以及局部变量的详细信息

也就是说我们可以对上面这些内容进行分析。通常可以基于 Heap Dump 分析如下类型的问题:

  • 找出内存泄漏的原因;
  • 找出重复引用的jar或类;
  • 分析集合的使用;
  • 分析类加载器。

总而言之我们对 Heap Dump 的分析就是对应用的内存使用进行分析,从而更加合理地使用内存。

如何做一次 Heap Dump

在前面讲到的, hprof 文件都是 Leakcanary 工具帮我们做的,那我们自己想要自己做一次 Heap Dump ,生成 hprof 文件又该怎么做呢?

其实 AndroidStudio 有现成的工具,只要动动手机就行了。

AndroidStudio --> Profiler --> 点击 + 号 --> 选择设备 --> 选择进程 --> 点击 MEMORY --> 点击 向下的箭头

Android 如何做一次内存泄漏大排查_Android 内存泄漏_08
由于生成的 hprof 文件比较大,所以解析出来比较慢,要耐心等待。

至此,我们就完成手动 Heap Dump 操作,并且生成 hprof 文件 。我们也可以点击保存按钮,把 hprof 文件保存到桌面,或者发给其他人。

如何代码触发 Heap Dump

代码其实很简单:

try {
     //指定Hprof文件的名字
     var path: String =
                externalCacheDir?.absolutePath + File.separator + System.currentTimeMillis() + ".hprof"
     Debug.dumpHprofData(path)
} catch (e: Exception) {

生成的文件在 Android/data/app包名/cache/ 目录下:
Android 如何做一次内存泄漏大排查_Android 内存泄漏_11

如何手动触发 GC

AndroidStudio --> Profiler --> 点击 + 号 --> 选择设备 --> 选择进程 --> 点击 MEMORY --> 点击 像垃圾桶 的图标

Android 如何做一次内存泄漏大排查_zhaoyanjun_12
其实最快速的是点击 右键
Android 如何做一次内存泄漏大排查_Android 内存泄漏_13

Android Profiler指标

官方文档:https://developer.android.com/studio/profile/memory-profiler

Android 如何做一次内存泄漏大排查_LeakCanary_14
内存计数中的类别如下:

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

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

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

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

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

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

Others:您的应用使用的系统不确定如何分类的内存。

Allocated:您的应用分配的 Java/Kotlin对象数。此数字没有计入 CC++ 中分配的对象。

如果连接到搭载 Android 7.1 及更低版本的设备,只有在内存性能分析器连接到您运行的应用时,才开始此分配计数。因此,您开始分析之前分配的任何对象都不会被计入。但是,Android 8.0 及更高版本附带一个设备内置性能剖析工具,该工具可跟踪所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。

Heap Dump指标分析

只有看懂了每个指标,才能更好的分析内存,下面我们分析一下 heap Dump 指标
Android 如何做一次内存泄漏大排查_内存泄漏_16

对象跟踪策略

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

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

记录某一段时间的内存分配情况

Heap Dump 是一个很好用的工具,能够分析内存中所有的对象,但是也是有弊端的,Heap Dump 是全量分析,如果你想分析某一段时间内的内存增量分配情况,该怎么做呢?点击 Record 按钮.

Android 如何做一次内存泄漏大排查_LeakCanary_18
下面用一个 gif 看看

Memory Analyzer(MAT)

Memory Analyzer 工具,简称:MAT

MAT 是 Eclipse 下的一个软件,专门用来分析 Java内存堆。

官方下载地址:https://www.eclipse.org/mat/

安装完成后,图标如下
Android 如何做一次内存泄漏大排查_LeakCanary_20
不得不说,这个工具长得很丑。

MAT可以打开 .hprof , 但是从 AndroidStudio 里面的导出的 .hprof 文件,MAT 是不支持查看的,所以需要转化一下,Android SDK 自带了转化工具。

您可以使用 android_sdk/platform-tools/ 目录中提供的 hprof-conv工具执行此操作。运行包含两个参数(即原始 HPROF 文件和转换后 HPROF 文件的写入位置)的hprof-conv 命令。例如:

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

经过转化过的 .hprof 文件,MAT 就可以打开了。

快过年了,祝大家 2021 事事顺心,万事大吉。新年快乐鸭 !!