從源碼角度分析,為什么會(huì)發(fā)生Fragment重疊?

該文分析的support包版本為23.3.0,在24.0.0及以上官方已修復(fù)文章中所說的Fragment重疊BUG。

我們?cè)谑褂肍ragment的過程中,有時(shí)會(huì)發(fā)現(xiàn)一直表現(xiàn)正常的Fragment,突然重疊了!

什么情況下會(huì)發(fā)生Fragment重疊?

一般滿足下面2個(gè)條件才可能會(huì)發(fā)生重疊:

1、發(fā)生了頁(yè)面重啟(旋轉(zhuǎn)屏幕、內(nèi)存不足等情況被強(qiáng)殺重啟)。
2、重復(fù)replaceadd Fragment 或者 使用show , hide控制Fragment;

為什么會(huì)發(fā)生Fragment重疊?

從源碼角度分析,為什么發(fā)生頁(yè)面重啟后會(huì)導(dǎo)致重疊?(在以add方式加載Fragment的時(shí)候)

我們知道Activity中有個(gè)onSaveInstanceState()方法,該方法會(huì)在Activity將要被kill的時(shí)候回調(diào)(例如進(jìn)入后臺(tái)、屏幕旋轉(zhuǎn)前、跳轉(zhuǎn)下一個(gè)Activity等情況下會(huì)被調(diào)用)。

當(dāng)Activity只執(zhí)行onPause方法時(shí)(透明Activity),這時(shí)候如果App設(shè)置的targetVersion大于11則不會(huì)執(zhí)行onSaveInstanceState方法

此時(shí)系統(tǒng)幫我們保存一個(gè)Bundle類型的數(shù)據(jù),我們可以根據(jù)自己的需求,手動(dòng)保存一些例如播放進(jìn)度等數(shù)據(jù),而后如果發(fā)生了頁(yè)面重啟,我們可以在onRestoreInstanceState()onCreate()里get該數(shù)據(jù),從而恢復(fù)播放進(jìn)度等狀態(tài)。

而產(chǎn)生Fragment重疊的原因就與這個(gè)保存狀態(tài)的機(jī)制有關(guān),大致原因就是系統(tǒng)在頁(yè)面重啟前,幫我們保存了Fragment的狀態(tài),但是在重啟后恢復(fù)時(shí),視圖的可見狀態(tài)沒幫我們保存,而Fragment默認(rèn)的是show狀態(tài),所以產(chǎn)生了Fragment重疊現(xiàn)象。

分析:
我們先看FragmentActivity的相關(guān)源碼:

public class FragmentActivity extends ... {
    final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...省略
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Parcelable p = mFragments.saveAllState();
        ...省略
    }
}

從上面源碼可以看出,F(xiàn)ragmentActivity確實(shí)是幫我們保存了Fragment的狀態(tài),并且在頁(yè)面重啟后會(huì)幫我們恢復(fù)!

其中的mFragments是FragmentController,它是一個(gè)Controller,內(nèi)部通過FragmentHostCallback間接控制FragmentManagerImpl。
相關(guān)代碼如下:

public class FragmentController {
    private final FragmentHostCallback<?> mHost;

    public Parcelable saveAllState() {
        return mHost.mFragmentManager.saveAllState();
    }

    public void restoreAllState(Parcelable state, List<Fragment> nonConfigList) {
        mHost.mFragmentManager.restoreAllState(state, nonConfigList);
    }
}

public abstract class FragmentHostCallback<E> extends FragmentContainer {
    final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();
}

通過上面代碼可以看出FragmentController通過FragmentHostCallback里的FragmentManagerImpl對(duì)象來控制恢復(fù)工作。

我們接著看FragmentManagerImpl到底做了什么:

final class FragmentManagerImpl extends FragmentManager {
    Parcelable saveAllState() {
        ...省略 詳細(xì)保存過程
        FragmentManagerState fms = new FragmentManagerState();
        fms.mActive = active;
        fms.mAdded = added;
        fms.mBackStack = backStack;
        return fms;
    }

    void restoreAllState(Parcelable state, List<Fragment> nonConfig) {
        // 恢復(fù)核心代碼
        FragmentManagerState fms = (FragmentManagerState)state;
        FragmentState fs = fms.mActive[i];
        if (fs != null) {
            Fragment f = fs.instantiate(mHost, mParent);
        }
    }
}

我們通過saveAllState()看到了關(guān)鍵的保存代碼,原來是是通過FragmentManagerState來保存Fragment的狀態(tài)、所處Fragment棧下標(biāo)、回退棧狀態(tài)等。

而在restoreAllState()恢復(fù)時(shí),通過FragmentManagerState里的FragmentState的instantiate()方法恢復(fù)了Fragment(見下面的分析就明白啦)

我們看下FragmentManagerState:

final class FragmentManagerState implements Parcelable {
    FragmentState[] mActive;           // Fragment狀態(tài)
    int[] mAdded;                      // 所處Fragment棧下標(biāo)
    BackStackState[] mBackStack;       // 回退棧狀態(tài)
    ...
}

我們只看FragmentState,它也實(shí)現(xiàn)了Parcelable,保存了Fragment的類名、下標(biāo)、id、Tag、ContainerId以及Arguments等數(shù)據(jù):

final class FragmentState implements Parcelable {
    final String mClassName;
    final int mIndex;
    final boolean mFromLayout;
    final int mFragmentId;
    final int mContainerId;
    final String mTag;
    final boolean mRetainInstance;
    final boolean mDetached;
    final Bundle mArguments;
    ...

    //  在FragmentManagerImpl的restoreAllState()里被調(diào)用
    public Fragment instantiate(FragmentHostCallback host, Fragment parent) {
        ...省略
        mInstance = Fragment.instantiate(context, mClassName, mArguments);
    }
}

至此,我們就明白了系統(tǒng)幫我們保存的Fragment其實(shí)最終是以FragmentState形式存在的。

此時(shí)我們?cè)偎伎枷?strong>為什么在頁(yè)面重啟后會(huì)發(fā)生Fragment的重疊? 其實(shí)答案已經(jīng)很明顯了,根據(jù)上面的源碼分析,我們會(huì)發(fā)現(xiàn)FragmentState里沒有Hidden狀態(tài)的字段!

而Hidden狀態(tài)對(duì)應(yīng)Fragment中的mHidden,該值默認(rèn)false...

public class Fragment ... {
    boolean mHidden;
}

我想你應(yīng)該明白了,在以add方式加載Fragment的場(chǎng)景下,系統(tǒng)在恢復(fù)Fragment時(shí),mHidden=false,即show狀態(tài),這樣在頁(yè)面重啟后,Activity內(nèi)的Fragment都是以show狀態(tài)顯示的,而如果你不進(jìn)行處理,那么就會(huì)發(fā)生Fragment重疊現(xiàn)象!

為什么重復(fù)replace|add Fragment 或者 使用show , hide控制Fragment會(huì)導(dǎo)致重疊?
  • **重復(fù)replace|add Fragment **
    我們知道加載Fragment有2種方式:replace()add()。
    不管哪種方式,重復(fù)加載Fragment都會(huì)導(dǎo)致重疊,這個(gè)很好理解,你加載同一個(gè)Fragment2次當(dāng)然會(huì)重疊了;問題是我們?cè)谀睦镏貜?fù)加載了Fragment?
    一般情況下,我們會(huì)在Activity的onCreate()里或者Fragment的onCreateView()里加載根Fragment,如果在這里沒有進(jìn)行頁(yè)面重啟的判斷的話,就可能導(dǎo)致重復(fù)加載Fragment引起重疊,正確的寫法應(yīng)該是:
  @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...
        // 判空, Fragment同理
        if(findFragmentByTag(RootFragment) == null){
              // 這里replace或add 根Fragment
        }
    }

這里一定要在saveInstanceState==null時(shí)才加載Fragment,因?yàn)榻?jīng)過上面的分析,在頁(yè)面重啟時(shí),F(xiàn)ragment的狀態(tài)會(huì)被保存恢復(fù),而此時(shí)再加載Fragment會(huì)重復(fù)加載,就導(dǎo)致棧已經(jīng)有該Fragment的情況下,再加載一該Fragment,從而導(dǎo)致重疊!

  • 使用show , hide控制Fragment
    我們使用show(),hide()時(shí),都是使用add的方式加載Fragment的,add配合hide使Fragment的視圖改變?yōu)镚ONE狀態(tài);而replace是銷毀Fragment 的視圖。
    頁(yè)面重啟時(shí),add的Fragment會(huì)全部走生命周期,創(chuàng)建視圖;而replace的非棧頂Fragment不會(huì)走生命周期,只有Back時(shí),才會(huì)逐一走棧頂Fragment生命周期,創(chuàng)建視圖。

結(jié)合上面的源碼分析,在使用replace加載Fragment時(shí),頁(yè)面重啟后,F(xiàn)ragment視圖都還沒創(chuàng)建,所以mHidden沒有意義,不會(huì)發(fā)生重疊現(xiàn)象;
而在使用add加載時(shí),視圖是存在的并且疊加在一起,頁(yè)面重啟后 mHidden=false,所有的Fragment都會(huì)是show狀態(tài)顯示出來(即VISIBLE),從而造成了Fragment重疊!

最后&解決方案

通過上面的分析,我想小伙伴們應(yīng)該徹底明白Fragment重疊的原因了吧!

鑒于篇幅原因,我另寫了一篇簡(jiǎn)書來談?wù)?Fragment重疊的解決方案,同時(shí)會(huì)給出我通過分析源碼想到的一個(gè)解決方案,下一篇解決方案的傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,442評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,581評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,374評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,591評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,789評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,322評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,554評(píng)論 2 379

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