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

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

我們在使用Fragment的過程中,有時會發(fā)現(xiàn)一直表現(xiàn)正常的Fragment议经,突然重疊了斧账!

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

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

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

為什么會發(fā)生Fragment重疊渠抹?

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

我們知道Activity中有個onSaveInstanceState()方法梧却,該方法會在Activity將要被kill的時候回調(diào)(例如進入后臺、屏幕旋轉(zhuǎn)前败去、跳轉(zhuǎn)下一個Activity等情況下會被調(diào)用)放航。

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

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

而產(chǎn)生Fragment重疊的原因就與這個保存狀態(tài)的機制有關(guān)行拢,大致原因就是系統(tǒng)在頁面重啟前祖秒,幫我們保存了Fragment的狀態(tài),但是在重啟后恢復(fù)時剂陡,視圖的可見狀態(tài)沒幫我們保存狈涮,而Fragment默認的是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確實是幫我們保存了Fragment的狀態(tài),并且在頁面重啟后會幫我們恢復(fù)晕鹊!

其中的mFragments是FragmentController松却,它是一個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對象來控制恢復(fù)工作晓锻。

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

final class FragmentManagerImpl extends FragmentManager {
    Parcelable saveAllState() {
        ...省略 詳細保存過程
        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ù)時屑墨,通過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躁锁,它也實現(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其實最終是以FragmentState形式存在的啄踊。

此時我們再思考下為什么在頁面重啟后會發(fā)生Fragment的重疊? 其實答案已經(jīng)很明顯了刁标,根據(jù)上面的源碼分析颠通,我們會發(fā)現(xiàn)FragmentState里沒有Hidden狀態(tài)的字段!

而Hidden狀態(tài)對應(yīng)Fragment中的mHidden命雀,該值默認false...

public class Fragment ... {
    boolean mHidden;
}

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

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

這里一定要在saveInstanceState==null時才加載Fragment,因為經(jīng)過上面的分析吭露,在頁面重啟時吠撮,F(xiàn)ragment的狀態(tài)會被保存恢復(fù),而此時再加載Fragment會重復(fù)加載讲竿,就導(dǎo)致棧已經(jīng)有該Fragment的情況下泥兰,再加載一該Fragment择浊,從而導(dǎo)致重疊!

  • 使用show , hide控制Fragment
    我們使用show(),hide()時逾条,都是使用add的方式加載Fragment的琢岩,add配合hide使Fragment的視圖改變?yōu)镚ONE狀態(tài);而replace是銷毀Fragment 的視圖师脂。
    頁面重啟時担孔,add的Fragment會全部走生命周期,創(chuàng)建視圖吃警;而replace的非棧頂Fragment不會走生命周期糕篇,只有Back時,才會逐一走棧頂Fragment生命周期酌心,創(chuàng)建視圖拌消。

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

最后&解決方案

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

鑒于篇幅原因螟凭,我另寫了一篇簡書來談?wù)?Fragment重疊的解決方案虚青,同時會給出我通過分析源碼想到的一個解決方案,下一篇解決方案的傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赂摆,一起剝皮案震驚了整個濱河市挟憔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烟号,老刑警劉巖绊谭,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異汪拥,居然都是意外死亡达传,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宪赶,“玉大人宗弯,你說我怎么就攤上這事÷蓿” “怎么了蒙保?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長欲主。 經(jīng)常有香客問我邓厕,道長,這世上最難降的妖魔是什么扁瓢? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任详恼,我火速辦了婚禮,結(jié)果婚禮上引几,老公的妹妹穿的比我還像新娘昧互。我一直安慰自己,他們只是感情好伟桅,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布敞掘。 她就那樣靜靜地躺著,像睡著了一般贿讹。 火紅的嫁衣襯著肌膚如雪渐逃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天民褂,我揣著相機與錄音,去河邊找鬼疯潭。 笑死赊堪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的竖哩。 我是一名探鬼主播哭廉,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼相叁!你這毒婦竟也來了遵绰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤增淹,失蹤者是張志新(化名)和其女友劉穎椿访,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虑润,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡成玫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哭当。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡猪腕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出钦勘,到底是詐尸還是另有隱情陋葡,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布彻采,位于F島的核電站腐缤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏颊亮。R本人自食惡果不足惜柴梆,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望终惑。 院中可真熱鬧绍在,春花似錦、人聲如沸雹有。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霸奕。三九已至溜宽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間质帅,已是汗流浹背适揉。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留煤惩,地道東北人嫉嘀。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像魄揉,于是被迫代替她去往敵國和親剪侮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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