Fragment這一篇就夠了

在日常工作中經(jīng)常用到Fragment,通過Fragment我們可以更加靈活的操作界面,但是這個東西有很多坑雷蹂,在我非常懵懂的時候經(jīng)常踩這些坑,下面就來總結(jié)一下我踩過的坑杯道。

首先Fragment事務(wù)的提交方式有四種:

  1. commit
  2. commitAllowingStateLoss
  3. commitNow
  4. 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來獲取。

  1. 不要復(fù)用全局的transaction
  2. 不要對同一個Fragment重復(fù)添加垢夹,尤其是DialogFragment
    3.使用commitNow的時候不能添加進(jìn)返回棧
  3. 盡量避免在網(wǎng)絡(luò)請求結(jié)果調(diào)用處使用commit溢吻,用commitAllowingStateLoss代替
  4. 在Fragment中盡量使用getChildFragmentManager來獲取FragmentManager的實例,不要使用activity的FragmentManager
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市促王,隨后出現(xiàn)的幾起案子犀盟,更是在濱河造成了極大的恐慌,老刑警劉巖蝇狼,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阅畴,死亡現(xiàn)場離奇詭異,居然都是意外死亡迅耘,警方通過查閱死者的電腦和手機(jī)贱枣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颤专,“玉大人纽哥,你說我怎么就攤上這事∑茱酰” “怎么了春塌?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長簇捍。 經(jīng)常有香客問我只壳,道長,這世上最難降的妖魔是什么暑塑? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任吼句,我火速辦了婚禮,結(jié)果婚禮上事格,老公的妹妹穿的比我還像新娘惕艳。我一直安慰自己,他們只是感情好驹愚,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布尔艇。 她就那樣靜靜地躺著,像睡著了一般么鹤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上味廊,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天蒸甜,我揣著相機(jī)與錄音,去河邊找鬼余佛。 笑死柠新,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辉巡。 我是一名探鬼主播恨憎,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了憔恳?” 一聲冷哼從身側(cè)響起瓤荔,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钥组,沒想到半個月后输硝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡程梦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年点把,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屿附。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡郎逃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挺份,到底是詐尸還是另有隱情褒翰,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布压恒,位于F島的核電站影暴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏探赫。R本人自食惡果不足惜型宙,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伦吠。 院中可真熱鬧妆兑,春花似錦、人聲如沸毛仪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箱靴。三九已至腺逛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衡怀,已是汗流浹背棍矛。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留抛杨,地道東北人够委。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像怖现,于是被迫代替她去往敵國和親茁帽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容