一、什么是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產生了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全解析系列(二):正確的使用姿勢。