在日常工作中經(jīng)常用到Fragment,通過Fragment我們可以更加靈活的操作界面,但是這個東西有很多坑雷蹂,在我非常懵懂的時候經(jīng)常踩這些坑,下面就來總結(jié)一下我踩過的坑杯道。
首先Fragment事務(wù)的提交方式有四種:
- commit
- commitAllowingStateLoss
- commitNow
- commitNowAllowingStateLoss
下面的這些坑或多或少的都和這些方法有關(guān)匪煌,下面結(jié)合具體情況分析一下幾種方法的區(qū)別
1. commit already called
字面意思是說提交已經(jīng)被執(zhí)行了,這種情況主要是下面的原因
//創(chuàng)建了一個全局的事務(wù)
val transaction = supportFragmentManager.beginTransaction()
//提交了一次事務(wù)
transaction.commit()
LiveDataBus.getChannel<String>("1").observe(this, Observer{
Log.d("test", it)
transaction.replace(R.id.container, fragment2, "2")
//l另一個事件來的時候又用原來的事務(wù)提交
transaction.commit()
})
在源碼中是這樣的党巾,這個transaction其實是這個東西
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
//commit 最終調(diào)用了這個方法
int commitInternal(boolean allowStateLoss) {
//在這拋出的異常
if (mCommitted) throw new IllegalStateException("commit already called");
//省略若干代碼
mCommitted = true;
}
看到上面的代碼萎庭,原因就在于mCommitted這個參數(shù),在每次提交之后都置為true齿拂,這個commitInternal方法只在commit和commitAllowingStateLoss時調(diào)用驳规。
為什么這么設(shè)計呢,因為每一個事務(wù)其實是Fragment返回棧的一個實例署海,每次提交其實就是一次記錄吗购,當(dāng)前的事務(wù)肯定只能代表一個Fragment,當(dāng)然只允許提交一次了砸狞。
commitNow中沒有這種限制捻勉,如過改成這樣呢:
transaction.replace(R.id.container, fragment2, "2")
transaction.commitNow()
又拋出了這個異常:Fragment already added: FragmentOne
2. Fragment already added(1)
這個異常是我們現(xiàn)在的項目中最常見的,在上面我們commitNow 的明明是fragment2刀森,但是卻說FragmentOne已經(jīng)添加過踱启,這個異常是在這段代碼中報的
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進(jìn)行了一次遍歷,這是個List研底,在執(zhí)行add,remove,hide,show方法時都會將操作加到這個集合中:
addOp(new Op(opcmd, fragment));
但是在上面我們調(diào)用的是replace埠偿,為什么走到這一步呢,因為共用了一個transaction榜晦,所以在遍歷到這一步的時候首先取的是第一次提交冠蒋,每次提交記錄了操作和對應(yīng)的Fragment,我們剛才正是通過add加入的FragmentOne芽隆,所以就提示已經(jīng)添加過了浊服。
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);
}
}
}
小結(jié):通過上面這兩個例子我們一定不要去復(fù)用transaction,會出現(xiàn)各種各樣的問題
3. Fragment already added(2)
上面那種異常情況還是很少發(fā)生的胚吁,因為很少有人會那樣干牙躺,但是下面在說一種情況,大家可能會犯腕扶。
在我們項目中造成崩潰最多的就是這個異常孽拷,項目中有個LoadingView是用DialogFragment來實現(xiàn)的,每個頁面的LoadingView是全局變量半抱,當(dāng)在多個網(wǎng)絡(luò)請求并發(fā)的情況下可能導(dǎo)致LoadingView還沒有dismiss又調(diào)用了一次show脓恕,網(wǎng)上說有兩個方法可以避免膜宋,isAdded、findFragmentByTag炼幔,但是在實際使用效果上并不好秋茫。看一下下面的代碼:
dialogFragmentOne.show(supportFragmentManager, "3")
if (!dialogFragmentOne.isAdded ||
supportFragmentManager.findFragmentByTag("3") != null) {
dialogFragmentOne.show(supportFragmentManager, "3")
}
要明白為什么這樣做之后還是會產(chǎn)生異常就必須搞明白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里有兩個兩個關(guān)鍵參數(shù)mHost 和mAdded乃秀,通過分析源碼肛著,這兩個參數(shù)都是正在addFragment之后設(shè)置的,當(dāng)?shù)诙翁峤粊淼臅r候理應(yīng)這兩個參數(shù)已經(jīng)被設(shè)置過了跺讯,其實原因在于commit方法枢贿,show其實調(diào)用的就是commit
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
調(diào)用commit方法之后,每個提交實際上是通過Handler發(fā)送出去的刀脏,所以在判斷isAdded的時候第一個提交還沒執(zhí)行到addFragment局荚,所以就又進(jìn)去了,導(dǎo)致重復(fù)添加愈污。
這種情況下我們換成commitNow就行了耀态,commitNow就是提交會被立馬執(zhí)行,到這暂雹,找出了commit和commitNow第一個不同之處:commit是異步的茫陆,commitNow是同步的,暫時不知道為什么要這樣設(shè)計擎析,還請大家指點。
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;
}
當(dāng)調(diào)用addToBackStack mAddToBackStack為true,在commitNow中調(diào)用了disallowAddToBackStack棚瘟,判斷mAddToBackStack就直接拋異常现斋,個人感覺這個提示信息不是很好,因為這個事務(wù)并沒被添加到返回棧偎蘸。
相關(guān)聯(lián)的異常就是這個了This FragmentTransaction is not allowed to be added to the back stack.因為如果先調(diào)用commitNow庄蹋,mAllowAddToBackStack就置為true。
為什么要這樣設(shè)計呢迷雪,因為commit是通過消息機(jī)制限书,在前面的事件都處理完的時候才會真正的執(zhí)行關(guān)鍵流程,但是commitNow會馬上執(zhí)行章咧,所以如果同時調(diào)用這兩個的話如果都允許入棧倦西,那么真正進(jìn)去的順序可能和我們的操作順序不同,所以就禁止commitNow入棧赁严。
commit和commitNow第二個不同:commit可添加到返回棧中扰柠,commitNow不允許添加到返回棧中粉铐。
小結(jié):上面分析了commit和commitNow的區(qū)別,commitNow從功能上來說就是不允許被添加到返回棧中卤档,個人認(rèn)為在不需要添加返回棧的時候盡量用commitNow蝙泼,這樣更穩(wěn)定,不會有亂七八糟的異常劝枣。
5. Can not perform this action after onSaveInstanceState
這個異常也很常見汤踏,通常是通過在網(wǎng)絡(luò)請求后的一個彈窗,而這是我們恰恰息屏了哨免,比如下面的代碼茎活,當(dāng)我們息屏后立馬崩潰。
val transaction = supportFragmentManager.beginTransaction()
Handler().postDelayed({
transaction.replace(R.id.container, fragment2, "2")
transaction.commit()
}, 5000)
這個異常是說這個操作不能在onSaveInstanceState之后執(zhí)行琢唾,這個方法是Activity保存狀態(tài)時調(diào)用的载荔,比如在息屏,屏幕旋轉(zhuǎn),Home的時候會調(diào)用采桃,其目的就是當(dāng)Activity異常銷毀的時候恢復(fù)現(xiàn)場懒熙。看這個異常在哪拋出的普办,在commit之后會調(diào)用這個方法:
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這個參數(shù)在FragmentActivity的onSaveInstanceState之后會置為true工扎,因為commit會默認(rèn)保存狀態(tài),所以在Activity的onSaveInstanceState之后再調(diào)用就保存不了狀態(tài)了衔蹲,所以不允許這么用肢娘,解決這個問題只需將commit換成
commitAllowingStateLoss,所以這兩個的區(qū)別就是commitAllowingStateLoss允許狀態(tài)丟失舆驶,commit不允許橱健,commitNow和commitNowAllowingStateLoss
所以如果不需要保存狀態(tài)就調(diào)用AllowingStateLoss
6. FragmentManager is already executing transactions
這個異常字面意思是說FragmentManager正在執(zhí)行一個事務(wù),由此可見同一個FragmentManager同時只能執(zhí)行一個事務(wù)沙廉,這個異常通常發(fā)生在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都會調(diào)用該方法珊皿,調(diào)用之后就將mExecutingActions置為true,在將一次提交的所有工作都完成之后再置為false巨税,所以當(dāng)一個Fragment在創(chuàng)建的生命周期內(nèi)直接調(diào)用所屬Activity的FragmentManager是有風(fēng)險的蟋定,所以推薦在Fragment中使用getChildFragmentManager來獲取。
- 不要復(fù)用全局的transaction
- 不要對同一個Fragment重復(fù)添加垢夹,尤其是DialogFragment
3.使用commitNow的時候不能添加進(jìn)返回棧 - 盡量避免在網(wǎng)絡(luò)請求結(jié)果調(diào)用處使用commit溢吻,用commitAllowingStateLoss代替
- 在Fragment中盡量使用getChildFragmentManager來獲取FragmentManager的實例,不要使用activity的FragmentManager