Fragment重疊異常

一、什么是Fragment重疊?
二、什么情況下會發生Fragment重疊?
三、為什么會發生Fragment重疊?
  1.重復replace/add Fragment
  2.使用show()或hide()控制Fragment
四、跟"重疊"輕松Say Goodbye
  1.不重復replace/add Fragment
  2.show()或hide() Fragment不重疊


什么是Fragment重疊?

在使用Fragment過程中,在某些情況下可能會發現一直表現正常的Fragment,突然重疊了,其表現為幾個Fragment的界面混合重疊在一起了。下面就是一種Fragment重疊異常表現:
(1)正常情況下的效果圖:


正常情況

(2)Fragment重疊:


Fragment重疊

由以下打印出的生命周期也可以看出,發生Fragment重疊時,Fragment產生了2個實例,導致彈了兩個ToastDialog:

10-20 10:52:23.577 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderActivity onCreate()
10-20 10:52:23.781 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment onViewCreated()
10-20 10:52:23.786 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment showToastDialog()
10-20 10:52:23.864 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment onViewCreated()
10-20 10:52:23.864 1616-1616/com.sankuai.meituan D/Toast: FlightSubmitOrderFragment showToastDialog()

什么情況下會發生Fragment重疊?

在“內存重啟”后回到前臺,頁面發生銷毀重建(旋轉屏幕、內存不足等情況被強殺重啟),如果沒對頁面重啟后的Fragment狀態做好處理,就容易發生Fragment重疊。
我們知道Activity中有個onSaveInstanceState()方法,該方法在app進入后臺、屏幕旋轉前、跳轉下一個Activity等情況下會被調用,此時系統幫我們保存一個Bundle類型的數據,我們可以根據自己的需求,手動保存一些例如播放進度等數據,而后如果發生了頁面重啟,我們可以在onRestoreInstanceState()onCreate()里獲取該數據,從而恢復播放進度等狀態。下面是FragmentActivity的相關源碼


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();
        ...省略代碼
    }
}

從上可以看出,FragmentActivity確實是幫我們保存了Fragment的狀態,并且在頁面重啟后會幫我們恢復。其中的mFragments是FragmentController,它是一個Controller,內部通過FragmentHostCallback間接控制FragmentManagerImpl。由于FragmentController是間接控制,沒有詳細保存Fragment狀態的內容,所以我們直接看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) {
        // 恢復核心代碼
        FragmentManagerState fms = (FragmentManagerState)state;
        FragmentState fs = fms.mActive[i];
        if (fs != null) {
            Fragment f = fs.instantiate(mHost, mParent);
        }
    }
}

我們通過saveAllState()看到了關鍵的保存代碼,原來是通過FragmentManagerState來保存Fragment的狀態、所處Fragment棧下標、回退棧狀態等,而在restoreAllState()恢復時,通過FragmentManagerState里的FragmentState的instantiate()方法恢復了Fragment。我們重點看FragmentState,Fragment的狀態,類名、下標、id、Tag、ContainerId以及Arguments等數據就保存在里面。Fragment重疊的原因就與這個保存狀態的機制有關!

為什么會發生Fragment重疊?

1.重復replace/add Fragment

我們知道加載Fragment有2種方式:replace()和add()。當發生內存重啟時,比如屏幕發生旋轉,Activity會重新啟動,默認Activity中的Fragment也會跟著Activity重新創建,這樣就造成了同一個Fragment會重復加載2次:

  • 通過onSaveInstanceState()保存的Fragment會重新啟動;
  • 當執行Activity的onCreate()時,又會再次實例化一個新的Fragment這就是出現重疊的原因。

由以上分析可知,一般情況下,我們會在Activity的onCreate()里或者Fragment的onCreateView()里加載根Fragment,如果在這里沒有進行頁面重啟的判斷的話,就可能導致重復加載Fragment引起重疊。

2.使用show()或hide()控制Fragment(源碼support -v4 24.0.0以下)

由前面介紹可知,發生內存重啟時,Fragment的狀態會被保存在FragmentState中,但是在源碼support-v4 24.0.0以下,FragmentState里沒有mHidden字段,默認情況下mHidden = false。也就是說發生內存重啟時,沒有保存Fragment的顯示狀態,導致頁面銷毀重建后,Fragment就是默認情況下的show狀態,Fragment一次性從棧底向棧頂順序恢復時發生重疊。support-v424.0.0以下的FragmentState類源碼如下,它實現了Parcelable,保存了Fragment的類名、下標、id、Tag、ContainerId以及Arguments等數據,但沒有mHidden字段(24.0.0及以上有該字段):

 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()里被調用
    public Fragment instantiate(FragmentHostCallback host, Fragment parent) {
        ...省略
        mInstance = Fragment.instantiate(context, mClassName, mArguments);
    }
}

注:show()hide()最終是讓Fragment的ViewsetVisibility(true或false),不會調用生命周期;當使用add()+show()hide()跳轉新的Fragment時,舊的Fragment回調onHiddenChanged(),不會回調onStop()等生命周期方法,而新的Fragment在創建時是不會回調onHiddenChanged()

跟"重疊"輕松Say Goodbye

1.不重復replace/add Fragment
public class MyActivity ... {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...
        // 這里一定要在save為null時才加載Fragment,Fragment中onCreateView等生命周里加載根子Fragment同理
        if(saveInstanceState == null){
              // 正常情況下去 加載根Fragment 
        }
    }
}

注:replace情況下,如果沒有加入回退棧,則不判斷也不會造成重疊;若加入回退棧,則也會造成重疊現象,建議統一判斷下

2.show()hide() Fragment不重疊(源碼support -v4 24.0.0及以上不用考慮)

從源碼角度的解決方案:從上面分析的原因,我們知道Fragment重疊的根本原因在于FragmentState沒有保存Fragment的顯示狀態,即mHidden,那我們就自己手動在Fragment中維護一個mSupportHidden,在頁面重啟后,我們自己來決定Fragment是否顯示。只需9行代碼!(摘自:9行代碼解決App內的Fragment重疊


public class BaseFragment extends Fragment {
    private static final String STATE_SAVE_IS_HIDDEN = "STATE_SAVE_IS_HIDDEN";

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    if (savedInstanceState != null) {
        boolean isSupportHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN);

        FragmentTransaction ft = getFragmentManager().beginTransaction();
        if (isSupportHidden) {
            ft.hide(this);
        } else {
            ft.show(this);
        }
        ft.commit();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        ...
        outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden());
    }
}

注:在使用show()hide()對多個Fragment的顯示進行控制時,在不同場景下如何選擇,用findFragmentByTag()還是用getFragments()恢復Fragment時(同時防止Fragment重疊),詳細分析見Fragment全解析系列(二):正確的使用姿勢

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

推薦閱讀更多精彩內容