最近项目开发中,开发人员和测试人员均反应在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. 第2阶段——编写uboot之硬件初始化和制作链接脚本lds(1)

    目标: 1.关看门狗 2.设置时钟 3.初始化SDRAM (初始化寄存器以及清除bss段) 4.重定位 (将nand/nor中代码COPY到链接地址上,需要初始化nandflash,读flash) 5 ...

  2. 第1阶段——u-boot分析之make 100ask24x0_config指令(1)

    本文学习目标:         掌握"make 100ask24x0_config"指令在Makefile和mkconfig文件中是怎么实现配置芯片选型 1.执行make 100a ...

  3. 使用vs2015编写c语言程序

    使用vs2015编写c语言程序 转载Yanky--博客园 http://www.cnblogs.com/yankyblogs/p/7058036.html   编写c语言程序的软件有很多,当年刚开始学 ...

  4. [自制操作系统] BMP格式文件读取&图形界面系统框架/应用接口设计

    本文将介绍在本人JOS中实现的简单图形界面应用程序接口,应用程序启动器,以及一些利用了图形界面的示例应用程序. 本文主要涉及以下部分: 内核/用户RW/RW调色板framebuffer共享区域 8bi ...

  5. 201521123082 《Java程序设计》第12周学习总结

    201521123082 <Java程序设计>第12周学习总结 标签(空格分隔): java 1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. An ...

  6. 201521123101 《Java程序设计》第6周学习总结

    1. 本周学习总结 1.面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 2. 书面作业 1.clone方法 1.1 Object对象中 ...

  7. 201521123070 《JAVA程序设计》第5周学习总结

    1. 本章学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. http://naotu.baidu.com/file/02b01f465e125c5942648a03358273b0 2. ...

  8. 201521123093 java 第二周学习总结

    201521123093 <java程序设计> 第二周学习总结 一.第二周学习总结 答:(1)关于进一步使用码云管理代码,本周才真正学会了如何将Eclipse里的代码上传到码云中,并且能够 ...

  9. 201521123108 《Java程序设计》第11周学习总结

    1. 本周学习总结 2. 书面作业 本次PTA作业题集多线程 Q1.互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用synchronized修饰方法实现互斥同步访问 ...

  10. 商城项目整理(四)JDBC+富文本编辑器实现商品增加,样式设置,和修改

    UEditor富文本编辑器:http://ueditor.baidu.com/website/ 相应页面展示: 商品添加: 商品修改: 前台商品展示: 商品表建表语句: create table TE ...