一个关于 DialogFragment 状态引起的崩溃日志

问题复现

最近出现了一个很奇怪的问题,问题异常日志以下:java

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2053)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2079)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:678)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:632)
at android.support.v4.app.DialogFragment.dismissInternal(DialogFragment.java:223)
at android.support.v4.app.DialogFragment.dismiss(DialogFragment.java:190)
复制代码

刚开始觉得是 DialogFragment 调用 show 的时候,没有判断 Activity 是否已经 Finish 了。结果发如今项目代码里是作了判断的,才知道确定没这么简单。android

FragmentManagerImpl.checkStateLossbash

private void checkStateLoss() {
     //异常抛出的地方
     if (mStateSaved) {
         throw new IllegalStateException("Can not perform this action after onSaveInstanceState");
     }
     if (mNoTransactionsBecause != null) {
         throw new IllegalStateException("Can not perform this action inside of " + mNoTransactionsBecause);
     }
}
复制代码

FragmentManagerImpl. enqueueAction服务器

/**
 * Adds an action to the queue of pending actions.
 *
 * @param action the action to add
 * @param allowStateLoss whether to allow loss of state information
 * @throws IllegalStateException if the activity has been destroyed
 */
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
    //注意这里的allowStateLoss 它决定了checkStateLoss方法是否进行
    if (!allowStateLoss) {
        checkStateLoss();
    }
    synchronized (this) {
        if (mDestroyed || mHost == null) {
            throw new IllegalStateException("Activity has been destroyed");
        }
        if (mPendingActions == null) {
            mPendingActions = new ArrayList<>();
        }
        mPendingActions.add(action);
        scheduleCommit();
    }
}
复制代码

BackStackRecord.commitInternalapp

int commitInternal(boolean allowStateLoss) {
    if (mCommitted) throw new IllegalStateException("commit already called");
    if (FragmentManagerImpl.DEBUG) {
        Log.v(TAG, "Commit: " + this);
        LogWriter logw = new LogWriter(TAG);
        PrintWriter pw = new PrintWriter(logw);
        dump(" ", null, pw, null);
        pw.close();
    }
    mCommitted = true;
    if (mAddToBackStack) {
        mIndex = mManager.allocBackStackIndex(this);
    } else {
        mIndex = -1;
    }
    //重点
    mManager.enqueueAction(this, allowStateLoss);
    return mIndex;
}
复制代码

BackStackRecord.commitide

public int commit() {
    //这里传入的就是allowStateLoss参数
    return commitInternal(false);
}
复制代码

DialogFragment.showui

public void show(FragmentManager manager, String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.add(this, tag);
    //没想到的时候DialogFragment的show方法会调用commit() 导火索
    ft.commit();
}
复制代码

目前来看是由于 DialogFragment 的 show 方法致使强行检查 Fragment 的状态,而刚好在检查的时候状态已经被保存致使 FragmentManager 的标记位 mStateSaved = true 。this

情景再现

当用户在进入首页模块时直接按 Home 键或者遇到内存不足等特殊状况下触发了onSaveInstanceState 致使 FragmentManager 进行“智能”保存当前 Activity 中 Fragment 的状态,由于 DialogFragment (广告弹窗)须要先从服务器拉取数据,因此以后在 show 的时候去 checkStateLoss 时触发了异常。spa

解决方案

  • 一、将广告弹窗实现改成纯 Dialog ,再也不使用 DialogFragment
  • 2若是本身的项目里面不须要对 Fragment 的相关状态进行保存和维护,能够在相关 Activity 中复写 onSaveInstance 不进行 super 回调便可(不建议使用此方案);
  • 三、可重写 DialogFragment 中的 show 方法,并使用 commitAllowStateLoss 提交,以下代码(推荐使用此方案)。
// 方案3代码示例
    override fun show(manager: FragmentManager?, tag: String?) {
        // super.show(manager, tag)
        val ft = manager?.beginTransaction()
        ft?.add(this, tag)
        ft?.commitAllowingStateLoss()
    }
复制代码
相关文章
相关标签/搜索