美文网首页
Fragment Transactions & Activity

Fragment Transactions & Activity

作者: 旅旅人 | 来源:发表于2018-04-23 15:59 被阅读0次

    Read the Fucking Source Code

    上周在Fabric的Crashlytics榜首出现了一个关于Fragment状态引发的崩溃日志:

    android.app.FragmentManagerImpl.checkStateLoss (FragmentManager.java:1858)
    android.app.FragmentManagerImpl.enqueueAction (FragmentManager.java:1881)
    android.app.BackStackRecord.commitInternal (BackStackRecord.java:688)
    android.app.BackStackRecord.commit (BackStackRecord.java:646)
    android.app.DialogFragment.show (DialogFragment.java:230)
    

    刚开始以为是DialogFragment调用show的时候,没有判断Activity是否已经Finish了。结果发现在项目代码里是做了判断的,看崩溃的人数和次数还是一个非必现的bug,瞬间就感觉事情可能不是那么简单了,只能硬着头皮去RTFSC了....

    这次我们反向追踪(就是这么皮)

    FragmentManagerImpl.checkStateLoss
     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.commitInternal
      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.commit
    public int commit() {
        //这里传入的就是allowStateLoss参数
        return commitInternal(false);
    }
    
    DialogFragment.show
    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,那么去查mStateSaved = true的调用时机一切都会水落石出了..

    ---------------------------------------查找中-------------------------------------------
    FragmentManager中mStateSaved = true总共出现了三次,最后确定了其中一条路线是本次崩溃走过的路...
    ---------------------------------------复现中-------------------------------------------


    情景再现

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

    解决方案:

    1.将广告弹窗实现改为纯Dialog,不再使用DialogFragment.
    2.如果自己的项目里面不需要对Fragment的相关状态进行保存和维护,可以在相关Activity中复写onSaveInstance不进行super回调即可.

    相关文章

      网友评论

          本文标题:Fragment Transactions & Activity

          本文链接:https://www.haomeiwen.com/subject/qkaclftx.html