美文网首页四大组件
Fragment这一篇就够了

Fragment这一篇就够了

作者: 孙大硕 | 来源:发表于2019-11-11 16:43 被阅读0次

    在日常工作中经常用到Fragment,通过Fragment我们可以更加灵活的操作界面,但是这个东西有很多坑,在我非常懵懂的时候经常踩这些坑,下面就来总结一下我踩过的坑。

    首先Fragment事务的提交方式有四种:

    1. commit
    2. commitAllowingStateLoss
    3. commitNow
    4. commitNowAllowingStateLoss

    下面的这些坑或多或少的都和这些方法有关,下面结合具体情况分析一下几种方法的区别

    1. commit already called

    字面意思是说提交已经被执行了,这种情况主要是下面的原因

    //创建了一个全局的事务
     val transaction = supportFragmentManager.beginTransaction()
    //提交了一次事务
     transaction.commit()
     LiveDataBus.getChannel<String>("1").observe(this, Observer{
           Log.d("test", it)
          transaction.replace(R.id.container, fragment2, "2")
         //l另一个事件来的时候又用原来的事务提交
          transaction.commit()
    })
    

    在源码中是这样的,这个transaction其实是这个东西

     public FragmentTransaction beginTransaction() {
          return new BackStackRecord(this);
     }
    //commit 最终调用了这个方法
    int commitInternal(boolean allowStateLoss) {
           //在这抛出的异常
            if (mCommitted) throw new IllegalStateException("commit already called");
           //省略若干代码
    
           mCommitted = true;
        }
    

    看到上面的代码,原因就在于mCommitted这个参数,在每次提交之后都置为true,这个commitInternal方法只在commit和commitAllowingStateLoss时调用。
    为什么这么设计呢,因为每一个事务其实是Fragment返回栈的一个实例,每次提交其实就是一次记录,当前的事务肯定只能代表一个Fragment,当然只允许提交一次了。

    commitNow中没有这种限制,如过改成这样呢:

     transaction.replace(R.id.container, fragment2, "2")
     transaction.commitNow()
    

    又抛出了这个异常:Fragment already added: FragmentOne

    2. Fragment already added(1)

    这个异常是我们现在的项目中最常见的,在上面我们commitNow 的明明是fragment2,但是却说FragmentOne已经添加过,这个异常是在这段代码中报的

    void executeOps() {
            final int numOps = mOps.size();
            for (int opNum = 0; opNum < numOps; opNum++) {
                final Op op = mOps.get(opNum);
                final Fragment f = op.mFragment;
                if (f != null) {
                    f.setNextTransition(mTransition, mTransitionStyle);
                }
                switch (op.mCmd) {
                    case OP_ADD:
                        f.setNextAnim(op.mEnterAnim);
                        mManager.addFragment(f, false);
                        break;
        }
    

    首先看到先对mOps进行了一次遍历,这是个List,在执行add,remove,hide,show方法时都会将操作加到这个集合中:

    addOp(new Op(opcmd, fragment));
    

    但是在上面我们调用的是replace,为什么走到这一步呢,因为共用了一个transaction,所以在遍历到这一步的时候首先取的是第一次提交,每次提交记录了操作和对应的Fragment,我们刚才正是通过add加入的FragmentOne,所以就提示已经添加过了。

    public void addFragment(Fragment fragment, boolean moveToStateNow) {
            if (DEBUG) Log.v(TAG, "add: " + fragment);
            makeActive(fragment);
            if (!fragment.mDetached) {
                if (mAdded.contains(fragment)) {
                    throw new IllegalStateException("Fragment already added: " + fragment);
                }
                synchronized (mAdded) {
                    mAdded.add(fragment);
                }
                fragment.mAdded = true;
                fragment.mRemoving = false;
                if (fragment.mView == null) {
                    fragment.mHiddenChanged = false;
                }
                if (isMenuAvailable(fragment)) {
                    mNeedMenuInvalidate = true;
                }
                if (moveToStateNow) {
                    moveToState(fragment);
                }
            }
        }
    

    小结:通过上面这两个例子我们一定不要去复用transaction,会出现各种各样的问题

    3. Fragment already added(2)

    上面那种异常情况还是很少发生的,因为很少有人会那样干,但是下面在说一种情况,大家可能会犯。

    在我们项目中造成崩溃最多的就是这个异常,项目中有个LoadingView是用DialogFragment来实现的,每个页面的LoadingView是全局变量,当在多个网络请求并发的情况下可能导致LoadingView还没有dismiss又调用了一次show,网上说有两个方法可以避免,isAdded、findFragmentByTag,但是在实际使用效果上并不好。看一下下面的代码:

      dialogFragmentOne.show(supportFragmentManager, "3")
      if (!dialogFragmentOne.isAdded || 
               supportFragmentManager.findFragmentByTag("3") != null) {
           dialogFragmentOne.show(supportFragmentManager, "3")
      }
    

    要明白为什么这样做之后还是会产生异常就必须搞明白isAdded和findFragmentByTag

    final public boolean isAdded() {
    
        return mHost != null && mAdded;
     }
    public Fragment findFragmentByTag(@Nullable String tag) {
            if (tag != null) {
                // First look through added fragments.
                for (int i=mAdded.size()-1; i>=0; i--) {
                    Fragment f = mAdded.get(i);
                    if (f != null && tag.equals(f.mTag)) {
                        return f;
                    }
                }
            }
            if (tag != null) {
                // Now for any known fragment.
                for (Fragment f : mActive.values()) {
                    if (f != null && tag.equals(f.mTag)) {
                        return f;
                    }
                }
            }
            return null;
        }
    

    isAdded里有两个两个关键参数mHost 和mAdded,通过分析源码,这两个参数都是正在addFragment之后设置的,当第二次提交来的时候理应这两个参数已经被设置过了,其实原因在于commit方法,show其实调用的就是commit

    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
            mDismissed = false;
            mShownByMe = true;
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        }
    

    调用commit方法之后,每个提交实际上是通过Handler发送出去的,所以在判断isAdded的时候第一个提交还没执行到addFragment,所以就又进去了,导致重复添加。

    这种情况下我们换成commitNow就行了,commitNow就是提交会被立马执行,到这,找出了commit和commitNow第一个不同之处:commit是异步的,commitNow是同步的,暂时不知道为什么要这样设计,还请大家指点。

    4. This transaction is already being added to the back stack

    看下面一段代码:

     val transaction = supportFragmentManager.beginTransaction()
     transaction.replace(R.id.container, fragment2, "2")
     transaction.addToBackStack(null)
     transaction.commitNow()
    

    addToBackStack表示添加到返回栈中,这段代码是必崩的,下面看一下原因

     public void commitNow() {
            disallowAddToBackStack();
            mManager.execSingleAction(this, false);
        }
     public FragmentTransaction disallowAddToBackStack() {
            if (mAddToBackStack) {
                throw new IllegalStateException(
                        "This transaction is already being added to the back stack");
            }
            mAllowAddToBackStack = false;
            return this;
        }
     public FragmentTransaction addToBackStack(@Nullable String name) {
            if (!mAllowAddToBackStack) {
                throw new IllegalStateException(
                        "This FragmentTransaction is not allowed to be added to the back stack.");
            }
            mAddToBackStack = true;
            mName = name;
            return this;
        }
    

    当调用addToBackStack mAddToBackStack为true,在commitNow中调用了disallowAddToBackStack,判断mAddToBackStack就直接抛异常,个人感觉这个提示信息不是很好,因为这个事务并没被添加到返回栈。

    相关联的异常就是这个了This FragmentTransaction is not allowed to be added to the back stack.因为如果先调用commitNow,mAllowAddToBackStack就置为true。

    为什么要这样设计呢,因为commit是通过消息机制,在前面的事件都处理完的时候才会真正的执行关键流程,但是commitNow会马上执行,所以如果同时调用这两个的话如果都允许入栈,那么真正进去的顺序可能和我们的操作顺序不同,所以就禁止commitNow入栈。

    commit和commitNow第二个不同:commit可添加到返回栈中,commitNow不允许添加到返回栈中。

    小结:上面分析了commit和commitNow的区别,commitNow从功能上来说就是不允许被添加到返回栈中,个人认为在不需要添加返回栈的时候尽量用commitNow,这样更稳定,不会有乱七八糟的异常。

    5. Can not perform this action after onSaveInstanceState

    这个异常也很常见,通常是通过在网络请求后的一个弹窗,而这是我们恰恰息屏了,比如下面的代码,当我们息屏后立马崩溃。

     val transaction = supportFragmentManager.beginTransaction()
     Handler().postDelayed({
         transaction.replace(R.id.container, fragment2, "2")
         transaction.commit()
    }, 5000)
    

    这个异常是说这个操作不能在onSaveInstanceState之后执行,这个方法是Activity保存状态时调用的,比如在息屏,屏幕旋转,Home的时候会调用,其目的就是当Activity异常销毁的时候恢复现场。看这个异常在哪抛出的,在commit之后会调用这个方法:

     private void checkStateLoss() {
            if (isStateSaved()) {
                throw new IllegalStateException(
                        "Can not perform this action after onSaveInstanceState");
    }
    
      
       public boolean isStateSaved() {
            // See saveAllState() for the explanation of this.  We do this for
            // all platform versions, to keep our behavior more consistent between
            // them.
            return mStateSaved || mStopped;
        }
    
    

    mStateSaved这个参数在FragmentActivity的onSaveInstanceState之后会置为true,因为commit会默认保存状态,所以在Activity的onSaveInstanceState之后再调用就保存不了状态了,所以不允许这么用,解决这个问题只需将commit换成
    commitAllowingStateLoss,所以这两个的区别就是commitAllowingStateLoss允许状态丢失,commit不允许,commitNow和commitNowAllowingStateLoss

    所以如果不需要保存状态就调用AllowingStateLoss

    6. FragmentManager is already executing transactions

    这个异常字面意思是说FragmentManager正在执行一个事务,由此可见同一个FragmentManager同时只能执行一个事务,这个异常通常发生在Fragment嵌套Fragment的情况,请看下面代码:

     val dialogFragment = DialogFragment()
                activity?.let {
                    dialogFragment.showNow(it.supportFragmentManager, "2")
      }
    

    在Fragment的onActivityCreated方法中直接显示一个DialogFragment,而且用的是Activity的FragmentManager,下面分析一下怎么造成的:FragmentManagerImpl

     private void ensureExecReady(boolean allowStateLoss) {
            if (mExecutingActions) {
                throw new IllegalStateException("FragmentManager is already executing transactions");
            }
    
            if (mHost == null) {
                throw new IllegalStateException("Fragment host has been destroyed");
            }
           //省略若干代码
            mExecutingActions = true;
            try {
                executePostponedTransaction(null, null);
            } finally {
                mExecutingActions = false;
            }
        }
    

    每次commit都会调用该方法,调用之后就将mExecutingActions置为true,在将一次提交的所有工作都完成之后再置为false,所以当一个Fragment在创建的生命周期内直接调用所属Activity的FragmentManager是有风险的,所以推荐在Fragment中使用getChildFragmentManager来获取。

    1. 不要复用全局的transaction
    2. 不要对同一个Fragment重复添加,尤其是DialogFragment
      3.使用commitNow的时候不能添加进返回栈
    3. 尽量避免在网络请求结果调用处使用commit,用commitAllowingStateLoss代替
    4. 在Fragment中尽量使用getChildFragmentManager来获取FragmentManager的实例,不要使用activity的FragmentManager

    相关文章

      网友评论

        本文标题:Fragment这一篇就够了

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