最近项目开发中,开发人员和测试人员均反应在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. 记一次wiki数据爬取过程

    最近有个爬取各国领导人信息的奇怪需求,要求百度和维基两种版本的数据,最要命的还要保持数据的结构不变.正好印象中隐约记得维基有专门的领导人列表页,不考虑爬取下来的格式不变的话应该很好爬的样子. 首先思路 ...

  2. Spring集成RabbitMQ-连接和消息模板

    ConnectionFactory ConnectionFactory是RabbitMQ服务掌握连接Connection生杀大权的重要组件 有了它,就可以创建Connection(org.spring ...

  3. Mac系统的终端显示git当前分支

    当我第一次在mac系统下使用git的时候,发现一个问题,git默认是不显示当前所在的分支名称,然后网上查找资料,找到了解决办法,终于可以显示本地当前分支,现在分享如下. 1 进入你的home目录 cd ...

  4. 个人作业3——个人总结(Alpha阶段)

    个人总结 在Alpha冲刺阶段中,我们团队基本完成了项目的大致基础框架,还有很多不足需要更多的时间来让我们做得更好. 对我个人而言,Alpha冲刺阶段是一个强度很大的阶段,每天都在吸收新的知识,团队也 ...

  5. Java中继承与多态

    Java类的继承继承的语法结构:    [修饰符列表] class 子类名 extends 父类名{        类体;    }子类就是当前这个类,父类就是我们要复用的那个类java中只支持单继承 ...

  6. 201521123039 《java程序设计》第八周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 总结: 1.集合可以动态修改大小,但是不可以存放基本数据类型: 2.java中任何对象都是is-a Objec ...

  7. 201521123038 《Java程序设计》 第八周学习总结

    201521123038 <Java程序设计> 第八周学习总结 1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 从集合里面获取对象时必须进行强制类 ...

  8. 201521123049 《JAVA程序设计》 第8周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 本次作业题集集合 1.List中指定元素的删除(题目4-1) 1.1 实验总结 public ...

  9. 201521123019 《Java程序设计》第3周学习总结

    1. 本周学习总结 2. 书面作业 (1)代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; ...

  10. java课程设计(个人)--五子棋

    1.团队课程设计博客链接 http://www.cnblogs.com/mz201521044152/p/7065575.html 2.个人负责模块说明 棋盘类,绘制棋盘,绘制棋子,按钮设置,鼠标监听 ...