最近项目开发中,开发人员和测试人员均反应在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. js实现换肤效果

    一,js换肤的基本原理 基本原理很简单,就是使用 JS 切换对应的 CSS 样式表文件.例如导航网站 Hao123 的右上方就有网页换肤功能.除了切换 CSS 样式表文件之外,通常的网页换肤还需要通过 ...

  2. 201521123118《java程序与设计》第七次作业

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

  3. 201521123017 《Java程序设计》第5周学习总结

    1. 本周学习总结 2. 书面作业 1.代码阅读:Child压缩包内源代码 1.1 com.parent包中Child.java文件能否编译通过?哪句会出现错误?试改正该错误.并分析输出结果. 1.2 ...

  4. 201521123001《Java程序设计》第2周学习总结

    本周学习总结 码云可以很方便地储存我们写好的代码,不用在写代码的时候担心没带U盘 Java中有许多已经写好的具有特定功能的一段独立小程序,不需要每一个函数都自己编写 Java的float型和C语言的不 ...

  5. VBScript中Msgbox函数的用法

    MsgBox(prompt[, buttons][, title][, helpfile, context]) [用途]:弹出对话框,并获取用户的操作结果. [参数说明]: propmt:对话框中展示 ...

  6. Geronimo tomcat: 在 Apache Geronimo 插件体系中将 Apache Tomcat 这个优秀的 Web 容器整合至其中

    Apache Geronimo 灵活的插件体系将 Tomcat, OpenJPA, OpenEJB, ActiveMQ 等第三方组件集成至其中.本文从多角度介绍了在 Apache Geronimo 中 ...

  7. 多个版本的Python如何设置不冲突

    同时装有Python2.7和Python3.5说明. 问题1: 设置右键点击"Edit with IDLE"选项打开的Python版本? 在运行输入"regedit&qu ...

  8. websphere部署 hibernate jpa & Error 500: javax/persistence/OneToOne.orphanRemoval()Z

    WebSphere 7 & Javax/Persistence/OneToMany.OrphanRemoval() Error 文章出处:http://www.mkyong.com/websp ...

  9. PolarDB · 新品介绍 · 深入了解阿里云新一代产品 PolarDB

    背景意义 云计算为如今的互联网时代提供了更多的计算能力,乃至创造能力,关系型数据库作为所有应用不可或缺的重要部件,开箱即用,高性价加比特性的云数据库深受开发者的喜爱.作为一线的开发和运维人员,在阿里云 ...

  10. 页面设计-数据列表 DataGrid

    传统软件项目开发时,针对每个业务单据页面需要每控件一个一个的来设计,同时需要在页面功能中对每个控件的属性进行判定处理,尤其是页面风格布局样式需要花去一大半的时间,并且后续要想修改是非常麻烦繁琐,这样就 ...