小心 DialogFragment 会造成内存泄漏

阅读: 评论:0

小心 DialogFragment 会造成内存泄漏

小心 DialogFragment 会造成内存泄漏

事情是这样的,我在项目里有个自定义的 Dialog 是继承 DialogFragment 实现的,接入 LeakCanary 后经常会提示我这个地方存在内存泄漏,定位的地方也有点奇怪,是一个布局控件上。

心想不应该啊,但既然报出来了,还是一探究竟。

内存泄漏

简单来说就是对象该销毁时没有被销毁回收,引用还被别的地方持有导致回收不掉,最后就变成了孤魂野鬼,达到一定程度就会导致 OOM 的问题了。

原因追踪

根据它报的位置我想难道是控件对象没被释放?于是我手动在 onDestoryView 里将控件 downLayout 赋值为 null,重试之后发现又报另外一个控件泄漏。显然问题不在这,这可能只是个表象。(得出这个推论花了我蛮长时间调试,经过多种猜测和尝试,大概定位在 DialogFragment 消失的时候,我复现问题的方式是,快速的打开销毁 DialogFragment)

回头在看看 LeakCanary 打出来的栈信息,指向的是 Message ???一开始我是不信的,打死都不信的,后来真香。表面上看 DialogFragment 的使用不会跟消息有什么关系啊,怎么会报 Message 的问题呢?看来需要我们看源码了。

DialogFragment 本质上是 Fragment 以 Dialog 的形式浮在 Activity 上展现,这个 Fragment 包含了一个 Dialog 对象,这个 Dialog 对象其实对我们是不可见的,因为我们不会直接操作它,重点就在这个 Dialog 上。

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {//省略部分......//Dialog 构造里会创建这个类,本质上是个 Handler 那肯定跟消息有关mListenersHandler = new ListenersHandler(this);}
//这个就是这个内部类,可以看到它对 Dialog 的持有是个弱引用,这其实是安全的,不会造成内存泄漏
private static final class ListenersHandler extends Handler {private final WeakReference<DialogInterface> mDialog;public ListenersHandler(Dialog dialog) {mDialog = new WeakReference<>(dialog);}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case DISMISS:((OnDismissListener) msg.obj).());break;case CANCEL:((OnCancelListener) msg.obj).());break;case SHOW:((OnShowListener) msg.obj).());break;}}}

这里我们简单看下 Dialog 的创建,简单来看就是创建对象以及一些属性设置,关联设置,有一个需要注意就是 ListenersHandler 类,本质上是个 Handler,看上去像是通过消息来处理 Dismiss, Show 等操作。

@Overridepublic void onActivityCreated(Bundle savedInstanceState) {//省略部分......//这段很关键if (!mDialog.takeCancelAndDismissListeners("DialogFragment", this, this)) {throw new IllegalStateException("You can not set Dialog's OnCancelListener or OnDismissListener");}//省略部分......}

回到 DialogFragment 中,上面这个是 Fragment 的生命周期方法,这里 Dialog 对象调用了 takeCancelAndDismissListeners() 方法,并且将 DialogFragment 对象作为入参传入。

public boolean takeCancelAndDismissListeners(@Nullable String msg,@Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) {//省略......//因为 DialogFragment 实现了 DialogInterface.OnCancelListener,//DialogInterface.OnDismissListener 那自然这里是成立的setOnCancelListener(cancel);setOnDismissListener(dismiss);//省略......return true;}
public void setOnCancelListener(@Nullable OnCancelListener listener) {if (mCancelAndDismissTaken != null) {throw new IllegalStateException("OnCancelListener is already taken by "+ mCancelAndDismissTaken + " and can not be replaced.");}if (listener != null) {mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);} else {mCancelMessage = null;}}
public void setOnDismissListener(@Nullable OnDismissListener listener) {if (mCancelAndDismissTaken != null) {throw new IllegalStateException("OnDismissListener is already taken by "+ mCancelAndDismissTaken + " and can not be replaced.");}if (listener != null) {mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);} else {mDismissMessage = null;}}

DialogFragment 对象作为 Message 对象的 obj 属性被关联了起来。上面我们说弹框消失时会引起内存泄漏,我们就去看下 dismiss(),DialogFragment 最终会调用 Dialog 的 dismiss() 

@Overridepublic void dismiss() {if (Looper() == Looper()) {dismissDialog();} else {mHandler.post(mDismissAction);}}
void dismissDialog() {//省略......try {veViewImmediate(mDecor);} finally {if (mActionMode != null) {mActionMode.finish();}mDecor = null;mWindow.closeAllPanels();onStop();mShowing = false;//这句是关键sendDismissMessage();}}
private void sendDismissMessage() {if (mDismissMessage != null) {// Obtain a new message so this dialog can be re-usedMessage.obtain(mDismissMessage).sendToTarget();}}

从 dismiss 一路调用下来,执行完 onStop 后会调用 sendDismissMessage 方法,这个变量 mDismissMessage 就是之前我们设置的 Message,它的 obj 是 DialogFragment 对象。通过 obtain 方法,其实是从消息池里另取一个消息逐个属性赋值,最后消息是在 ListenersHandler 里处理,也就是回调了 DialogFragment 对象的 onDismiss 方法。

要说是 Message 的问题,简单看,Dialog 里有 Message 变量,但 Dialog 销毁时,他们也会跟着销毁。至于 sendDismissMessage 里获取的 Message,它是从池子里来的,最后也会回到池子里去,感觉好像没什么地方会一直持有引用。

后来查阅了资料  非常感谢这篇文章,分析的很到位,让我知道了原来消息池是线程共享的,并且在消息队列阻塞的情况下会持有 Message 引用,这才是导致内存泄漏的原因所在

问题解决

根据这篇参考资料的建议 记一次 DialogFragment 造成的内存泄漏 我想自定义 Dialog 会相对的简单好处理一些。通过继承 Dialog 重写 setOnDismissListener(),setOnCancelListener() 方法,目的是不给 mDismissMessage 和 mCancelMessage 赋值了,这样就不会存在这个内存泄漏的问题,但带来的缺点是,DialogFragment 的 onDismiss 方法没有回调了。

后来我想了下,是否可以不通过发消息,而直接回调 onDismiss,因为发消息的方式其实也没有额外的逻辑。所以我想的是在原来发消息的位置替换成直接回调。那么在哪个地方比较合适呢?先看下源码:

void dismissDialog() {//省略......try {veViewImmediate(mDecor);} finally {if (mActionMode != null) {mActionMode.finish();}mDecor = null;mWindow.closeAllPanels();onStop();//这个方法不错,比较适合直接回调mShowing = false;//原本是要在这发消息的sendDismissMessage();}}

这里我们看中了 onStop 方法,而且可以重写,所以我们可以重写然后在调用 super 之后执行 onDismiss 的回调即可。不足的地方是,mShowing 字段的值更新被延后了。另外需要注意的是,这是基于 android-27 的源码,后续如果 Dialog 源码有较大变动,可能就不太合适了

cancel 操作和 dismiss 操作类似,可以重写 cancel() 方法,但是要注意,要先直接回调,再调用 super 。

通过这次问题解决,我知道了 消息池竟然是线程共享的 嗯,真香!
 

 

本文发布于:2024-02-02 03:20:09,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170681659541052.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:小心   内存   DialogFragment
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23