Android内存泄露问题的分析、定位与修复(新手向)

目录

可达性分析 & GC Root

内存泄漏的定义

内存泄漏的常见原因

内存泄露的检测与分析工具

示例:AppUpdateActivity泄露

线上平台上查看泄露对象

复现泄露现象

分析AppUpdateActivity入口

开启LeakCanary

触发内存泄漏

分析引用链

appUpdateActivity

view:LinearLayout实例

object[]

arrayDeque

object[]、sparseArray

ViewManager(注意这是class,上面分析过的都是instance’)

object[]、pathClassLoader

引用链分析小结

查看、分析代码

ViewManager——看preLoadedCardViews

AppUpdateActivity——启动/创建view场景

ViewManager——看被哪些中间类调用过

AppUpdateFragment——中间类

参考资料

附录:Profiler上查看泄露对象及其引用链


  1. 可达性分析 & GC Root

现代虚拟机基本都是采用可达性分析算法来判断对象是否存活,可达性算法的原理是以一系列叫做GC Root的根为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。这样通过 GC Root 串成的一条线就叫引用链,直到所有的结点都遍历完毕。

处于引用链中的对象会被认为是存活对象,不会被GC回收。比如下图中的Obj1、Obj2、Obj3。

不在引用链中的对象会被判断为垃圾对象,可以被GC回收。比如下图中的a、b。

故GC Root(垃圾回收根)是指在垃圾回收过程中被视为活动对象集合的根,是GC扫描的起点。

  1. 内存泄漏的定义

如图。当对象a不再需要使用了,本该能够被GC回收时,而现在有一个对象b,它的生命周期比a长,同时它还持有着a的强引用,这导致GC无法回收a,使a仍然停留在堆中,无法释放内存。随着时间的推移,对象a有可能会越积越多,这样可用的内存会越来越少。这种情况下我们会说,a引发了内存泄露。

简单来说,内存泄露就是指,当一个对象不再需要使用时,由于某些原因,它无法被GC回收,导致这部分内存无法被再次利用。

而导致a发生内存泄漏的关键在于两点:

  1. b持有着a的强引用
  2. b的生命周期比a长

结论 1:

对象a没有被任一生命周期比它长的对象b所强引用 = 对象a能够被GC回收 = 对象a能够被GC释放内存 = 对象a不会发生内存泄露。

  1. 内存泄漏的常见原因

引发内存泄露的常见原因主要有:

  1. 集合类:使用完后没有清空
  2. Static修饰的成员变量:引用了短生命周期的对象,使用完后没有解除引用
  3. 非静态内部类/匿名类:默认持有外部类实例的引用

多线程:AsyncTask、实现Runnable接口、继承Thread类。

Handler消息传递:未被处理/正处理的消息 -> Handler实例 -> 外部类

  1. 资源对象使用完后未关闭/停止。如广播BraodcastReceiver、文件流File、数据库游标Cursor、图片资源Bitmap、无限循环播放的动画/视频等。

具体案例与解决方案参见下方链接/保存在本文档路径下的html文件。必看!

《Android性能优化:手把手带你全面了解 内存泄露 & 解决方案》

https://blog.csdn.net/carson_ho/article/details/79407707?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170505373416800227425145%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=170505373416800227425145&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~hot_rank-6-79407707-null-null.142^v99^pc_search_result_base8&utm_term=Android%20%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%B7%A5%E5%85%B7&spm=1018.2226.3001.4187

  1. 内存泄露的检测与分析工具

工具推荐:LeakCanary >> MAT >> Profiler。

AS自带的Profiler,可查看泄露对象及其引用链。但分析引用链的功能太少。

MAT(Eclipse Memory Analyzer Too)分析引用链的工具多一些,最实用的功能是:Path To GC Roots —— exclude all phantom/weak/soft etc. references。

虽然MAT能筛选出关键引用链,但没有进一步定位可能发生泄露的地方。

相比MAT,LeakCanary进一步定位了可能发生泄露的地方,缩小了排查范围。

  1. 示例:AppUpdateActivity泄露

“内存泄露的检测与分析工具”一节的工具就可以检测出哪些对象发生了泄露。

以下检测出内存泄露对象的工具是一个线上平台。能够看出内存泄露对象及其引用链。

    1. 线上平台上查看泄露对象

其引用链为:

* thread android.os.HandlerThread.contextClassLoader (named default_perftest_thread)

* ↳ dalvik.system.PathClassLoader.runtimeInternalObjects

* ↳ array java.lang.Object[].[7521]

* ↳ static ViewManager.preLoadedCardViews

* ↳ android.util.SparseArray.mValues

* ↳ array java.lang.Object[].[1]

* ↳ java.util.ArrayDeque.elements

* ↳ array java.lang.Object[].[0]

* ↳ android.widget.LinearLayout.mContext

* ↳ AppUpdateActivity

可以看到,AppUpdateActivity对象发生了泄露。

线上平台上检测出来的内存泄露问题,如果能在本地复现,数据的查看会更方便。接下来先尝试在本地复现泄露现象,复现成功后再做进一步的分析。复现不了的话,就只能下载线上平台上的堆转储文件hprof来分析了。

    1. 复现泄露现象

  1. 分析AppUpdateActivity入口

AS中打开AppUpdateActivity的代码,分析它的UI界面是怎样的,从哪里可以启动AppUpdateActivity。

经分析,其打开路径为:我的页面-应用更新。

  1. 开启LeakCanary

在AppUpdateActivity所在目录对应build.gradle中,添加依赖:debugImplementation 'com.squareup.leakcanary:leakcanary-android:3.0-alpha-1',使用LeakCanary检测内存泄漏。如下图。

打开AS的Logcat,查看tag:LeakCanary的日志。

  1. 触发内存泄漏

多次尝试以下操作:

  1. 我的页面-应用更新页面-返回我的页面
  2. 我的页面-应用更新页面-返回我的页面-Home键返回桌面
  3. 我的页面-应用更新页面-返回我的页面-Profiler触发强制GC

直至LeakCanary捕获到AppUpdateActivity对象泄露,并且引用链与线上平台上的类似,如下图。

另:Profiler上也能查看泄露对象及其引用链,查看方法详见附录。

    1. 分析引用链

以下分析做如下约定:首字母大写表类,首字母小写表实例。比如,MainActivity表示为一个类;mainActivity表示为一个实例。

先从泄露对象appUpdateActivity开始,逐步往上分析到GC root。

      1. appUpdateActivity

上图显示appUpdateActivity正在发生内存泄露。

因为它的销毁方法onDestroy()已经被调用过,而且它的销毁标识符mDestroyed也已经被设置为true,即它已经执行过销毁程序了。在内存没有泄露的正常情况下,生命周期比它长的所有对象(比如静态变量)此时是不会再强引用它的,这样才能保证GC能够顺利回收它。

但是现在发现,从GC Root开始,通过强引用的方式,竟然还能够搜寻/引用到appUpdateActivity。这会导致appUpdateActivity无法被GC回收,所以它现在仍在堆里面存活,占据着内存,这部分的内存就无法被释放,即发生了内存泄露。

那appUpdateActivity被谁强引用了呢?被view的mContext字段强引用了,即view.mContext=appUpdateActivity。

      1. view:LinearLayout实例

首先需要搞清楚, view的mContext字段强引用着appUpdateActivity会不会引发内存泄漏问题呢?我们需要手动打破这个引用关系吗?

答:不会引发内存泄漏,我们也不需要手动打破这个引用关系。因为appUpdateActivity在执行销毁程序时,会让其下的view先执行销毁程序。如果没有其他生命周期更长的对象强引用着view,那view执行销毁程序后就能被GC回收,随后appUpdateActivity再被回收。

所以现在情况是,appUpdateActivity和view都执行过销毁程序了,但view目前无法被GC回收,因为它被生命周期比view长的对象(object[0])强引用着,所以下图显示view正在发生内存泄漏。

      1. object[]

object[]数据的第0为元素,是对view的强引用。

如果我们能打破这个引用关系,比如手动设置object[0]=null,不让它再强引用着view。这样view就能被GC回收了,view与appUpdateActivity就不会发生内存泄漏了。

所以上图object[0]下方被下划波浪线“~~~”标注着。表示appUpdateActivity内存泄露的原因,可能就是发生在object[0]这个引用上。同时,object[]的泄露情况标注为“未知”,需要我们自己分析判断。

那object[]是什么数组,怎么在代码中找到他?目前信息不足,需继续往上分析。即object[]又被谁强引用着呢?上图可以看到,被arrayDeque.elements强引用着。

      1. arrayDeque

那我们需要手动设置arrayDeque.elements=null,打破其对object[]的引用吗?

不需要。因为arrayDeque.elements=object[]这一引用关系,其实是系统工具类ArrayDeque内部的实现细节,由ArrayDeque自己内部处理就行。(这一段的分析,在后面分析完代码后再回来看,会更清晰,更容易理解)

我们需要做的处理在arrayDeque这一层级完成就行了。比如调用arrayDeque.clear()方法,移除队列中的所有元素,不让它们引用任何对象。

再者,调用arrayDeque.clear()方法后,其实还实现了上述“手动设置object[0]=null”的操作,不再让object[0]强引用着view。

所以上图标注了arrayDeque.elements这一个引用,表示它也是可疑之处。也标注了泄露情况为“未知”。

上面分析到,调用arrayDeque.clear()方法后,“arrayDeque.elements=null”这一操作由ArrayDeque自己内部处理。所以可以推测出arrayDeque.elements=object[]这一引用关系,其实是不会造成内存泄露的。即,arrayDeque.elements的下划波浪线“~~~”可以去除,取消对它的怀疑。

继续引用链的分析,arrayDeque被object[]的第0位元素强引用了。

      1. object[]、sparseArray

object[0]=arrayDeque,如果我们使object[0]=null,就打破了object[0]对arrayDeque的强引用,这样arrayDeque也能被GC回收了。所以object[0]有下划波浪线“~~~”标注着,可能会引发内存泄漏。其泄露状态“未知”。

上图看到,object[]被sparseArray的mValues字段强引用了。所以推测,sparseArray.mValues=object[]这一引用关系,其实也是系统工具类SparseArray的内部实现细节,也是由SparseArray自己内部处理就行,不会引发内存泄漏。即,sparseArray.mValues的下划波浪线“~~~”也可以去除,取消对它的怀疑。

我们要做的处理需要在sparseArray这一层级完成,比如调用sparseArray.clear(),清空sparseArray中的所有元素,就实现了上述的“object[0]=null”操作。

继续引用链的分析,sparseArray被ViewManager的preLoadedCardViews字段强引用了。

      1. ViewManager(注意这是class,上面分析过的都是instance’)

同理,如果我们手动设置ViewManager.preLoadedCardViews=null,能够解除对sparseArray的强引用,让GC能够回收sparseArray。

注意到preLoadedCardViews字段是一个静态变量,它的生命周期长度与Application一样。

重点来了!也就是说,preLoadedCardViews的生命周期长度比appUpdateActivity的要长很多,而preLoadedCardViews又对appUpdateActivity间接的强引用了。这根本性地导致了appUpdateActivity在执行过销毁程序后,GC无法回收它,引发了appUpdateActivity的内存泄露。

所以上图ViewManager.preLoadedCardViews有下划波浪线“~~~”标注着,表示它可能引发了内存泄漏。

注意到,ViewManager的泄露状态为“NO”。因为引用链中的class,GC是不会回收它的,它占据的内存是不会被释放的,所以也就不存在发生内存泄露之说了。或者说ViewManager是不会引发内存泄漏的。

重点来了!既然ViewManager的内存是不会被释放的,那指向ViewManager的任何一个引用,它都不会造成ViewManager发生内存泄漏,更不会造成appUpdateActivity发生内存泄露。即,造成appUpdateActivity发生内存泄露的引用,只可能是上面分析过的那些引用:ViewManager.preLoadedCardViews、SparseArray.mValues[0]、ArrayDeque.elements[0]。

但我们也看一下剩余的引用链。

ViewManager被object[]的第3236位元素引用着。

      1. object[]、pathClassLoader

对于当前引用链来说,既然ViewManager没有发生内存泄漏,那持有其引用的object[]自然也不会发生内存泄露了,所以其泄露状态显示“NO”。同理,pathClassLoader、connectivityThread,也是如此。

类似的,推测pathClassLoader.runtimeInternalObjects=object[]这一引用关系,也是系统工具类PathClassLoader的内部实现细节。

另外,处于引用链中的classLoader、处于活动状态的thread实例,GC也不会回收,所以pathClassLoader、connectivityThread自身也不会发生内存泄漏。

      1. 引用链分析小结

当前引用链中,能造成appUpdateActivity发生内存泄露的引用,只有这三个:ViewManager.preLoadedCardViews、SparseArray.mValues[0]、ArrayDeque.elements[0]。

而SparseArray、ArrayDeque属于系统类,ViewManager则是自己编写的类,所以问题应该出在ViewManager.preLoadedCardViews这一引用上。

    1. 查看、分析代码

起点是AppUpdateActivity,终点是ViewManager.preLoadedCardViews。

现在需要查看代码,分析appUpdateActivity是如何传递/被引用到preLoadedCardViews上的。

      1. ViewManager——看preLoadedCardViews

上图看到preLoadedCardViews字段的类型为SparseArray<ArrayDeque<View>>,与引用链中从view到sparseArray的部分都对应上了。

查找给preLoadedCardViews赋值的代码。推测是在给preLoadedCardViews赋值的时候,传入了appUpdateActivity实例。代码如下图。

果然,输入参数是context,appUpdateActivity应该就是在这里传进来的了。

在AppUpdateActivity代码中搜索,看看有没有调用过ViewManager类的方法,发现并没有。推测AppUpdateActivity与ViewManager之间还有其他中间类,appUpdateActivity先传给这些中间类,中间类再调用ViewManager类的方法,把appUpdateActivity传到ViewManager.preLoadedCardViews里面。

所以现在问题变成找出这些中间类,找出完整的调用链。

先分析ViewManager类。注意到preLoadedCardViews是存储多个view的容器,这些view在Activity启动时预先创建加载好并缓存起来。在需要创建view时,先看看preLoadedCardViews缓存里有没有符合条件的view。有的话,就从中取出;没有的话,就新建一个。

      1. AppUpdateActivity——启动/创建view场景

所以AppUpdateActivity中,使用到ViewManager.preLoadedCardViews的场景,推测应该是在其启动、创建view的时候调用中间类的。

注意到AppUpdateActivity onCreate()时,新建了一个AppUpdateFragment。这个有可能是中间类的其中之一。

再回去ViewManager中看看,实现preLoadedCardViews赋值的preLoadCardInternal(Context context, int cardCode, int count)这一方法都被哪些中间类调用了。

      1. ViewManager——看被哪些中间类调用过

preLoadCardInternal(Context context, int cardCode, int count)方法是private的,在ViewManager中被buildPreLoadCache(Context context)、preLoadRecomenedCard(Context context)、preLoadDetailContentCard(Context context)这三个public方法调用过。

分别查看这三个方法被哪些类调用过。

如上图,在preLoadRecomenedCard(Context context)中,发现了AppUpdateFragment。

      1. AppUpdateFragment——中间类

点进去可以看到,AppUpdateFragment通过getContext(),将appUpdateActivity传进去了。

至此找到完整调用链。

那AppUpdateFragment预加载了卡片view后,是不是忘记清空存储预加载view的容器,导致没有及时解除对appUpdateActivity的引用?

同时,注意到ViewManager类中有一个方法public void clearPreLoadCache(),就是用于清空preLoadedCardViews和arrayDeque的。下图。

所以,在AppUpdateFragment onDestroy()中,补充上调用这个方法就行了。下图

补上代码后,运行app,执行“复现泄露现象”小节的操作,再到Profiler里面查看,AppUpdateActivity实例的所有引用链中都找不到这条引用链了,说明修复成功,preLoadedCardViews不会再造成AppUpdateActivity发生内存泄露了。

  1. 参考资料

下面列举的这些资料是都是精心筛选、去重过的,非常建议去学习。

Leakcanary入门:Getting Started - LeakCanary

使用 Memory Profiler 检查应用程序的内存使用情况:

https://developer.android.com/studio/profile/memory-profiler#capture-heap-dump

Android性能优化:手把手带你全面了解 内存泄露 & 解决方案:

https://blog.csdn.net/carson_ho/article/details/79407707?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170505373416800227425145%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=170505373416800227425145&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~hot_rank-6-79407707-null-null.142^v99^pc_search_result_base8&utm_term=Android%20%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%B7%A5%E5%85%B7&spm=1018.2226.3001.4187

Android异步通信:详解 Handler 内存泄露的原因:

Android异步通信:详解 Handler 内存泄露的原因 - 简书

  1. 附录:Profiler上查看泄露对象及其引用链

Profiler上捕获堆转储:

过滤:

点击AppUpdateActivity实例,查看所有引用链:

找出与线上平台上的类似的引用链,有时可以通过上图的“Show nearest GC root only”选项过滤出来。与线上平台上的类似的引用链如下图。

如果内存泄露问题被修复成功了,Profiler上可以看到,AppUpdateActivity实例的所有引用链中都将找不到如上图所示的引用链。

觉得讲解得清晰,有收获的,麻烦点个赞呀。你的点赞是我更新的动力!♥

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值