四大组件

Fragment这一篇就够了

2019-11-11  本文已影响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
上一篇下一篇

猜你喜欢

热点阅读