Android | 记一次内存泄漏排查

1,212 阅读4分钟

前言

昨天在测试 APP 的时候,多次退出然后重新打开一个 Activity,越到后面就能明显感觉到越来越卡顿,因此 合理怀疑产生了非常严重的内存泄漏。那么既然有问题了,就开始一步步排查吧!

这是我第一次对自己的项目做内存泄漏排查,所以还是有必要记录一下的,而且在排查的过程中还学到了不少东西,也相当于是做个笔记了。

确认

首先第一步就是要确认是不是真的发生内存泄漏了,因为 APP 卡顿不一定就是内存泄漏导致的,所以我们要先进行确认。确认的方法很简单,使用 adb 就好,就是下面这条命令:

adb shell dumpsys meminfo 包名

如果在退出你被怀疑发生内存泄漏的 (这个定语稍微有点长嚎)那个 Activity 之后,内存中的 View 数和 Activity 数没有减少,并且再次打开那个 Activity ,内存中的 View 数和 Activity 数越来越多,那么可以肯定:你的 APP 发生内存泄漏了。

以下图片是我的测试结果:

退出再打开 1 次 ↓

图片

退出再打开 2 次 ↓

图片

退出再打开 3 次 ↓

图片

可以看到,每退出再打开那个 Activity 一次,内存中的 Activity 数量就加 1,并且 View 及其它很多对象的数量都在增加...... 这...... 我不卡谁卡?

OK,既然确认了发生内存泄漏了,那就继续往下,去找究竟是哪个小东西导致泄漏的。

查找

这里我使用的是大名鼎鼎的 LeakCanary 作为我的定位工具,没有别的原因,就是因为它用起来真的太太太简单了,就一行代码:

图片

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'

真的非常非常非常友好了!我在网上看到以前的版本还需要在 Application 里注册一下,现在连注册都不需要了,nice~

当我们再次运行 APP 时,LeakCanary 会自动帮我们捕获发生内存泄漏的地方,它会在通知栏给出提示,等它抓取完毕之后,就可以点进去一探究竟了。

在这之前,我还单纯地以为只有一处内存泄漏,可是 LeakCanary 告诉我有两处... 

LeakCanary 会给出下图这种引用链↓ 

图片

图片

从上面两个图可以看到,确确实实有两个地方导致 PushStreamActivity 的实例没有得到释放。

修正

错误找到了,接下来就要改正他↓

第一处引用链比较短,也很简单,是因为我在 PushStreamActivity 类中定义了一个内部类 BackThread,我们知道,Java 中普通内部类和匿名内部类是隐式地持有外部类的引用的,而那个 BackThread 做的是耗时任务,当我退出 PushStreamActivity 时,BackThread 耗时任务还没有做完,导致一直得不到释放,而它又持有外部类 PushStreamActivity 的引用,所以导致 PushStreamActivity 也得不到释放,由此发生了内存泄漏。

这种情况的解决办法是给普通内部类加一个 static 关键字改为静态内部类,因为静态内部类是不持有外部类的引用的。

第二处内存泄漏引用链有点长,我们从下往上看,是因为 PushStreamPresenter 持有 PushStreamActivity 的引用导致的内存泄漏↓ (MVP 模式中Presenter 需要持有 View 的引用)

图片

而 PushStreamPresenter 的引用是被 PushStreamPresenter7持有的,这个PushStreamPresenter7 持有的,这个PushStreamPresenter7 是 PushStreamPresenter 里的一个匿名内部类,应该是某个正在做网络请求的线程。这种情况也好办,只需要在退出 Activity 的时候和 Presenter 解绑就行↓

图片

这样一来,内存泄漏的问题就解决啦~

总结

这大概是我第一次真正意义上的对 APP 进行性能调优,整个过程还是学到挺多东西的,总结如下:

  • 匿名内部类的引用问题。在遇到因 Thread 发生内存泄漏的问题后,我就在想一个问题:匿名类对象实例的引用是被谁持有的?为什么 JVM 在 GC 的时候,不会回收掉未完成的 Thread?

    为此我还在 CSDN 上提了个问题↓

    图片

    我当时想的是,它没有被回收不外乎就两个原因:一是有什么我们不知道的隐式引用指向它;二是它本身就是 GC roots。

    下午的时候我在知乎上找到了满意的答案↓

    图片

    对!Thread 也是 GC roots 之一!学习了!

    并且 LeakCanary 也帮我印证了这一点↓

    图片

  • 加深了对 Java 中普通内部类、匿名内部类及静态内部类的理解,以及前面两者可能带来的危害:可怕的内存泄漏!!!

  • 还有就是对这些个命令呀、工具呀的使用了。