Fragment这一篇就够了
在日常工作中经常用到Fragment,通过Fragment我们可以更加灵活的操作界面,但是这个东西有很多坑,在我非常懵懂的时候经常踩这些坑,下面就来总结一下我踩过的坑。
首先Fragment事务的提交方式有四种:
- commit
- commitAllowingStateLoss
- commitNow
- 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来获取。
- 不要复用全局的transaction
- 不要对同一个Fragment重复添加,尤其是DialogFragment
3.使用commitNow的时候不能添加进返回栈 - 尽量避免在网络请求结果调用处使用commit,用commitAllowingStateLoss代替
- 在Fragment中尽量使用getChildFragmentManager来获取FragmentManager的实例,不要使用activity的FragmentManager