针对 DialogFragment 状态异常和内存泄漏的解决方案

·  阅读 1422
针对 DialogFragment 状态异常和内存泄漏的解决方案

DialogFragment 是一种弹窗实现方式,其本质是 Fragment。

//它的类定义表明,它继承自Fragment,并且拥有Dialog的cancel和dismiss行为
public class DialogFragment extends Fragment
        implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener 
复制代码

因为在 DialogFragment 内部有一个 Dialog 类型的成员变量,所以可以简单理解 DialogFragment 是对 Dialog 进行了一层包装,使得我们可以通过 Fragment 的方式去管理 Dialog 弹窗。

实际场景使用上会存在两个问题,一个是内存泄漏,这个问题是内存泄漏检测时发现的。另一个是状态异常(Can not perform this action after onSaveInstanceState),这个问题是个老问题了。

就这两个问题,我整理了解决方案,方便项目实际处理。

如何解决 DialogFragment 的内存泄漏

这个问题在很多版本中都存在(例如 API 29, androidx 1.0.0),在 androidx 1.1.0 版本上已经做了处理。所以建议使用 androidx 1.1.0 库里的 DialogFragment。

内存泄漏的本质

那使用老版本的 DialogFragment,又是如何引起内存泄漏的呢?

我们需要了解下泄漏本质。

Android 的消息机制中是通过 Message 对象作为信息载体在消息队列中进行处理的,也就是说并不是发出消息后立马就执行该消息,有可能队列里有更优先的消息先处理,导致我们的消息一直处在队列中。

Message 类中有个 Object 类型的成员变量 obj,可以用来做引用指向。在 DialogFragment 里就用到了这点。

//9.0.0 的源码 DialogFragment
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    //省略部分源码。。。
    if (!mDialog.takeCancelAndDismissListeners("DialogFragment", this, this)) {
            throw new IllegalStateException(
                   "You can not set Dialog's OnCancelListener or OnDismissListener");
    }
    //省略部分源码。。。
}
//对应的 Dialog
/** @hide */
public boolean takeCancelAndDismissListeners(@Nullable String msg,
            @Nullable OnCancelListener cancel, @Nullable OnDismissListener dismiss) {
    //注意这里的两个 Listener 入参,就是 DialogFragment 对象
    if (mCancelAndDismissTaken != null) {
        mCancelAndDismissTaken = null;
    } else if (mCancelMessage != null || mDismissMessage != null) {
        return false;
    }
    //这里把 DialogFragment 对象再传入
    setOnCancelListener(cancel);
    setOnDismissListener(dismiss);
    mCancelAndDismissTaken = msg;

    return true;
}
public void setOnDismissListener(@Nullable OnDismissListener listener) {
    if (mCancelAndDismissTaken != null) {
        throw new IllegalStateException(
                "OnDismissListener is already taken by "
                + mCancelAndDismissTaken + " and can not be replaced.");
    }
    //这里的 listener 就是 DialogFragment 对象
    if (listener != null) {
        //这样一来,DialogFragment 对象就作为创建 mDismissMessage 对象的入参
        //被赋值给了 Message 的 obj 变量。
        mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
    } else {
        mDismissMessage = null;
    }
}
复制代码

这也正是造成内存泄漏的源头:Dialog 对象的 mDismissMessage 变量的 obj 变量持有着 DialogFragment 对象的引用。

如果在 DialogFragment 销毁时,这个 mDismissMessage 在消息队列里还没处理,那么就意味着 mDismissMessage 对象还在,也就造成 DialogFragment 对象无法被回收,引起内存泄漏。

androidx 里是如何解决内存泄漏

androidx 1.1.0 版本与上述版本相比,主要的差别在于 DialogFragment 的 dismissInternal(boolean, boolean) 方法。

void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) {
    //省略部分源码。。。
    if (mDialog != null) {
        // Instead of waiting for a posted onDismiss(), null out
        // the listener and call onDismiss() manually to ensure
        // that the callback happens before onDestroy()
        //将 dismissListener 置空的目的就是为了将 mDismissMessage 置空,
        //这样也就断开了对 DialogFragment 对象的引用
        //从而避免了在 DialogFragment 主动销毁(调用dismiss)时,引起的内存泄漏的可能
        mDialog.setOnDismissListener(null);
        mDialog.dismiss();
        //省略部分源码。。。
    }
    //省略部分源码。。。
}
复制代码

老版本如何自己解决内存泄漏

解决的重点是不让 mDismissMessage 对象持有 DialogFragment 对象,我们可以自定义一个 Dialog,重写 setOnDismissListener(OnDismissListener) 方法,不执行现有的创建 mDismissMessage 逻辑,这样就不会通过 Message 对象持有 DialogFragment 对象了。

然后重写 Dialog 的 dismiss() 方法,调用 super 方法后,再手动回调 OnDismissListener 给到 DialogFragment。由于 mDismissMessage 对象没有被赋值,自然也就不会再发消息了。

最后再自定义 DialogFragment,重写 onCreateDialog(Bundle) 方法,返回上面我们自定义的 Dialog 对象,这样就不会有内存泄漏的可能了。

另外和 DismissListener 类似的 CancelListener, ShowListener 也需要处理。

如何解决 DialogFragment 的状态异常

这个问题更常见,平时在使用 Fragment 的时候就可能经常遇到这个问题。处理的方法 1.将 commit() 方法改为 commitAllowingStateLoss() 也就避免了异常的产生。

但可惜的是,DialogFragment 所有的 show 方法都是用的 commit() 进行 Fragment 展示,反而 dismiss 方法却是有 dismissAllowingStateLoss(),这显然不太行。

通过查阅资料,我们可以这样处理,

//1.在 show 的调用前,判断状态,异常就不展示
@Override
public void show(FragmentManager manager, String tag) {
    if (manager.isStateSaved() || manager.isDestroyed()) {
        //这里可以做下日志记录
        return;
    }
    super.show(manager, tag);
}
//2.通过反射调用,将 commitAllowingStateLoss() 暴露出来调用
public void showAllowingStateLoss(FragmentManager manager, String tag) {
    setBooleanField("mDismissed", false);
    setBooleanField("mShownByMe", true);
    FragmentTransaction ft = manager.beginTransaction();
    ft.add(this, tag);
    ft.commitAllowingStateLoss();
}

private void setBooleanField(String fieldName, boolean value) {
    try {
        Field field = DialogFragment.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.setBoolean(this, value);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
复制代码

怎么理解 Fragment 的状态异常

粗浅点理解,我觉得就是 Fragment 也是有合理生命周期的,通过状态校验确保数据展示及交互处理的正常,特别是一些对页面状态要求比较高的数据交互,但如果是一些提示性交互,对状态的控制也不需要那么高要求。

所以,我们可以从业务角度出发,对状态要求高的,可以提前做状态判断,避免交给系统报错,不然可能就会造成有感知的崩溃(当然大概率可能是无感崩溃)。对状态没什么要求的,只要满足条件就可以展示或者消失的,那就允许 stateLoss。

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改