最近项目开发中,开发人员和测试人员均反应在android5.0以下手机上LeakCanary频繁监控到内存泄漏,如下图所示,但凡用到Dialog或DialogFragment地方均出现了内存泄漏。

  如上图所示,存在一个Message实例的obj成员变量,间接引用着Activity的实例,导致Activity无法正常退出。通过Android Monitors内存快照分析,确实有Message实例持有对LoadingDialogFragment的引用,进而导致Activity也无法正常销毁,出现内存泄漏(如下图)。

  参考一个内存泄漏引发的血案一文,了解到问题发生原因:局部变量的生命周期在Dalvik VM跟ART/JVM中有区别。在DVM中,假如线程死循环或者阻塞,那么线程栈帧中的局部变量假如没有被置为null,那么就不会被回收。 在 VM 中,每一个栈帧都是本地变量的集合,而垃圾回收器是保守的:只要存在一个存活的引用,就不会回收它。在每次循环结束后,本地变量不再可访问,然而本地变量仍持有对 Message 的引用,interpreter/JIT 理论上应该在本地变量不可访问时将其引用置为 null,然而它们并没有这样做,引用仍然存活,而且不会被置为 null,使得它不会被回收。

  1、例如HandlerThread中,Looper会不停的从阻塞队列MessageQueue中取Message进行处理。当没有可消费Message对象时,就会开始阻塞,而此时最后一个被取出的Message就会被本地变量引用,一直不会释放引用,哪怕Message已经被recycler(仅仅是清理了内容并放回消息队列)。其实到这一步,只是一个空壳的Message被泄漏,无法回收,毕竟Message实例的内容还是被清理了(demo中的SecondActivity模拟了没有recycler时的泄漏情况,适用于自己实现类似HandlerThread时需要注意的情况)。

  2、在Dialog源码中,我们可以看到如下代码片段,包括setOnCancelListener、setOnDismissListener在内的方法,其实都是将设置进来的listener对象(listener对象包含对Activity的引用)放到一个从消息队列中拿到的Message实例中,将listener赋给了Message实例的obj变量。例如mShowMessage,mShowMessage会一直保存这个Message实例,不会再放回消息队列中,因为在sendShowMessage时,Dialog是从消息队列中再次obtain一个Message实例,复制mShowMessage内容进行发送。当然前面这些也不会存在什么问题,mShowMessage也会在Dialog销毁时跟着销毁。

  综合1与2,分开来看,一般情况下大家互不干扰。但两者碰撞在一起时,问题就来了。Dialog从消息队列中可能会恰巧取到一个“仍然被某个阻塞中的HandlerThread本地变量引用的Message实例”,然后把listener赋给Message的obj,并一直保存在Dialog实例中(例如mShowMessage),这样内存泄漏就发生了。就算Dialog销毁,本地变量仍然引用保持着对Message的引用,导致obj变量的指向的listener无法回收,listener又包含对Activity的引用,导致Activity也无法正确回收。

  在这种情况下,除非HandlerThread收到新的Message处理,而给本地变量重新赋值从而切断了对上一个Message引用,否则会一直内存泄漏。

public void setOnShowListener(@Nullable OnShowListener listener) {
if (listener != null) {
mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
} else {
mShowMessage = null;
}
}
private void sendShowMessage() {
if (mShowMessage != null) {
// Obtain a new message so this dialog can be re-used
Message.obtain(mShowMessage).sendToTarget();
}
}

  解决方案:我们可以通过提供一个DialogInterface.OnCancelListener的包装类(Dialog其他listener也一样可行),仅包含对真正listener的引用,当Dialog退出后,解除对listener的引用。还有一个办法就是在Handler空闲时发送一个空Message,当然处理Dialog Message的Handler我们无法直接控制(在Dialog内部的私有变量),所以采用包装类方法解决。

public final class DetachableDialogCancelListener implements DialogInterface.OnCancelListener
{
public static DetachableDialogCancelListener wrap(DialogInterface.OnCancelListener delegate)
{
return new DetachableDialogCancelListener(delegate);
} private DialogInterface.OnCancelListener delegateOrNull; private DetachableDialogCancelListener(DialogInterface.OnCancelListener delegate)
{
this.delegateOrNull = delegate;
} @Override
public void onCancel(DialogInterface dialog)
{
if (delegateOrNull != null)
{
delegateOrNull.onCancel(dialog);
delegateOrNull = null;
}
} public void clearOnDetach(Dialog dialog)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
{
dialog.getWindow()
.getDecorView()
.getViewTreeObserver()
.addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener()
{
@Override
public void onWindowAttached()
{ } @Override
public void onWindowDetached()
{
if (delegateOrNull != null)
{
delegateOrNull.onCancel(dialog);
delegateOrNull = null;
}
}
});
}
}
}

  如下图所示,通过对内存进行快照,看到确实达到了我们的目的。

  当然,问题并没有因此而结束,当我将所有设置了setOnCancelListener等监听事件的地方都用包装类处理后,仍然收到了LeakCanary的内存泄漏通知。到底是怎么回事呢?通过一番debug,发现在DialogFragment的onActivityCreated中,设置过setOnCancelListener和setOnDismissListener,当自己再去设置时,还是会发生内存泄漏。其实问题就出在默认的设置,虽然我们重新设置了,但在执行默认设置时,仍然有可能会恰巧取到一个“仍然被某个阻塞中的HandlerThread本地变量引用的Message实例”,就算后面被重新设置了,但包含默认listener设置的Message仍然还被HandlerThread的本地变量引用,所以也就内存泄漏了。

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); if (!mShowsDialog) {
return;
} View view = getView();
if (view != null) {
if (view.getParent() != null) {
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
mDialog.setContentView(view);
}
final Activity activity = getActivity();
if (activity != null) {
mDialog.setOwnerActivity(activity);
}
mDialog.setCancelable(mCancelable);
mDialog.setOnCancelListener(this);
mDialog.setOnDismissListener(this);
if (savedInstanceState != null) {
Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
if (dialogState != null) {
mDialog.onRestoreInstanceState(dialogState);
}
}
}

  至此,问题既然出在DialogFragment的onActivityCreated默认设置上,那么如果能取消默认的设置,就不会发生内存泄漏。上面这段代码是DialogFragment的源码,不能修改,而super.onActivityCreated又必须调用。如何解决呢?看上面代码的第5行,通过调用setShowsDialog将mShowDialog设置为false,这样super.onActivityCreated就等于不会执行剩余代码逻辑了。在自己的onActivityCreated中,自行实现super类中本应执行的代码逻辑(copy即可),然后将setOnCancelListener和setOnDismissListener通过包装类进行设置,我这里是直接删除了这两行代码,由继承自BaseDialogFragment的子类自行设置。

public class BaseDialogFragment extends DialogFragment
{
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
{
boolean isShow = this.getShowsDialog();
this.setShowsDialog(false);
super.onActivityCreated(savedInstanceState);
this.setShowsDialog(isShow); View view = getView();
if (view != null)
{
if (view.getParent() != null)
{
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
this.getDialog().setContentView(view);
}
final Activity activity = getActivity();
if (activity != null)
{
this.getDialog().setOwnerActivity(activity);
}
this.getDialog().setCancelable(this.isCancelable());
if (savedInstanceState != null)
{
Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
if (dialogState != null)
{
this.getDialog().onRestoreInstanceState(dialogState);
}
}
}
else
{
super.onActivityCreated(savedInstanceState);
}
}
}

  至此,Dialog和DialogFragment在Android5.0以下的内存泄漏问题均得以解决。但该方案并不完美,能够解决内存泄漏的关键,还是通过监听OnWindowAttachListener,在Dialog退出时切断Message实例与真正listener对象的关联。但OnWindowAttachListener需要level18,所以。。。如果有什么好的低版本同样实现,烦请告知,感谢!

  如果是使用DialogFragment,可以在onDestory中切断Message实例与真正listener对象的关联。

  补充,本文一直在重点分析Dialog如何因为Message产生内存泄漏。而事实上,自己写的HandlerThread中,如果是Android5.0以下,一定要在取出Message用完后,将Message置为null,并且要防止被编译器优化掉,否则也会因为HandlerThread阻塞后,导致Message无法正确释放包含的内容,产生内存泄漏。(可运行本文给出的demo,重现问题)。

  demo运行后,打开SecondActivity,发送Message,然后返回,此时Activity应该被销毁,但LeakCanary会提示内存泄漏。将SecondActivity的Handler中取出的msg用完后置为null即可解决。而FourActivity模拟了HandlerThread发生泄漏的情况,可以尝试用本文提出的办法解决,Demo中给出了通过发送一个空消息,回收本地变量引用的Message实例。

demo GitHub地址

解决Android5.0以下Dialog引起的内存泄漏的更多相关文章

  1. 讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(中)- IOS不为人知的Bug

    前言: 话说昨晚还是前晚,写了一篇:讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(上) 文章写到最后时,多了很多莫名奇妙的问题!!! 为了解决了这些莫名奇妙的问题,我又战斗了2 ...

  2. 讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(下)- block中任性用self

    前言: 在处理完框架内存泄漏的问题后,见上篇:讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(中)- IOS不为人知的Bug 发现业务代码有一个地方的内存没释放,原因很也简单: ...

  3. [转] weak_ptr解决shared_ptr环状引用所引起的内存泄漏

    http://blog.csdn.net/liuzhi1218/article/details/6993135 循环引用: 引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环引 ...

  4. weak_ptr解决shared_ptr环状引用所引起的内存泄漏[转]

    转载:http://blog.csdn.net/liuzhi1218/article/details/6993135 循环引用: 引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理 ...

  5. 解决Android5.0以后DatePicker选择时间无效的bug。

    一.在布局中加上这句话. 加上了这句话后,就相当于强制用5.0以前的外观,所以外观会有所变化: 5.0以上没有这句话的外观: 加上之后的外观: 二.可以用DatePickerDialog代替

  6. 讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(上)

    前言: 好久没写文章了,最近先是重构IT恋.又重写IT恋中. Sagit框架也不断的更新,调整,现在感觉已完美了了相当的多. 今天不写教程,先简单分享一下技术内容. 1:见Block必有:#defin ...

  7. Android5.0以下drawable tag vector错误的解决办法(转发)

    Android5.0以下drawable tag vector错误的解决办法 在Androi 5.0以下的设备可能会报这样的错误: Caused by: org.xmlpull.v1.XmlPullP ...

  8. android中handler使用应该注意的问题(解决由handler引起的OOM内存泄漏)

    最近,在项目过程中频繁的使用handler处理一些ui线程上的操作,以及使用handler的postdealy.然而使用过后却不对handler进行处理,进而产生了内存溢出现象,通过google,发现 ...

  9. Android内存优化14 内存泄漏常见情况5 特殊对象造成的内存泄漏 WebView内存泄漏

    WebView造成内存泄露 关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存. ...

随机推荐

  1. mha 复制检查报错“There is no alive server. We can't do failover”

    安装mha所参考的文章: http://linzhijian.blog.51cto.com/1047212/1906434 http://www.cnblogs.com/xiaoboluo768/p/ ...

  2. angular之scope.$watch

    某“大神”挖了个陨石坑,我于是乎似懂非懂的接手,玩了一个月angular.现在项目告一段落,暂别了繁重的重复性工作,可以开始回顾.认真的折腾下之前犹抱琵琶的angular. angular吸引人的特性 ...

  3. CCNA+NP学习笔记—序章

    本人就读于南京捷式泰网络科技有限公司学习CCIE,这几天准备将多年来的纸质版笔记全部写成电子版献给大家以留下自己学习的足迹.本章是基础篇章,内容较少,主要为之后的内容做铺垫.所有笔记的分类顺序为:序章 ...

  4. Java单元测试之覆盖率统计eclemma

    安装 有两种安装方法 下载安装(推荐) 地址: http://sourceforge.net/projects/eclemma/ 将解压后的features和plugins目录下的文件分别拷贝到Ecl ...

  5. 团队作业4——第一次项目冲刺 SeCOnd DaY

    项目冲刺--Double Kill 喂喂喂,你好你好,听得见吗?这里是天霸动霸.tua广播站,我是主播小学生¥-¥ 第一次敏捷冲刺平稳的度过了第一天,第一天的任务大家也圆满完成啦[拍手庆祝],那么今天 ...

  6. 团队作业4——第一次项目冲刺(Alpha版本) Day7

    首先和助教及老师表示抱歉,博客确实当时就写了,但是一直不算写好,因为这几天卡住了,预计实现的功能实现不了,进度跟不上,现在也在寻求解决方法. 1.站立式会议: 遇到的困难和收获:在开发的过程中其实遇到 ...

  7. 201521123022 《Java程序设计》 第七周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图或其他)归纳总结集合相关内容. 2. 书面作业 Q1.ArrayList代码分析 Q1.1 解释ArrayList的contains源代码 ArrayLis ...

  8. 201521123110 java课程设计

    一.需求分析 功能要求:计算机产生随机数,猜中即胜,猜不中,提示是大了还是小了,继续猜,直至猜到,给出所用时间和评语.保留用户测试成绩,做出成绩排行榜.排行榜存放到文件中. 二.本组课题及本人任务 • ...

  9. jQuery 简介,与js的对比

    jquery可以说是js的封装,大多数情况下jquery比js简单,它们两个可以相互写对方的里面,使用jquery需要导入jquery文件. <script src="jquery-1 ...

  10. 磁盘管理之inode与block

    索引式文件系统 什么是inode? Inode其实就是索引号,便于我们寻找我们文件所存储的数据块block,索引式文件系统在查找信息,读写操作上都比原来的文件系统要快,我们可以通过inode中记录的b ...