一、什么是Fragment重疊洪燥?
二欠拾、什么情況下會發(fā)生Fragment重疊错蝴?
三门扇、為什么會發(fā)生Fragment重疊啃奴?
1.重復replace/add Fragment
2.使用show()或hide()控制Fragment
四梢褐、跟"重疊"輕松Say Goodbye
1.不重復replace/add Fragment
2.show()或hide() Fragment不重疊
什么是Fragment重疊挑社?
在使用Fragment過程中蚌斩,在某些情況下可能會發(fā)現(xiàn)一直表現(xiàn)正常的Fragment铆惑,突然重疊了,其表現(xiàn)為幾個Fragment的界面混合重疊在一起了送膳。下面就是一種Fragment重疊異常表現(xiàn):
(1)正常情況下的效果圖:
(2)Fragment重疊:
由以下打印出的生命周期也可以看出员魏,發(fā)生Fragment重疊時,F(xiàn)ragment產(chǎn)生了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()
什么情況下會發(fā)生Fragment重疊撕阎?
在“內(nèi)存重啟”后回到前臺,頁面發(fā)生銷毀重建(旋轉(zhuǎn)屏幕碌补、內(nèi)存不足等情況被強殺重啟)虏束,如果沒對頁面重啟后的Fragment狀態(tài)做好處理,就容易發(fā)生Fragment重疊厦章。
我們知道Activity中有個onSaveInstanceState()
方法镇匀,該方法在app進入后臺、屏幕旋轉(zhuǎn)前袜啃、跳轉(zhuǎn)下一個Activity等情況下會被調(diào)用汗侵,此時系統(tǒng)幫我們保存一個Bundle類型的數(shù)據(jù),我們可以根據(jù)自己的需求,手動保存一些例如播放進度等數(shù)據(jù)晰韵,而后如果發(fā)生了頁面重啟发乔,我們可以在onRestoreInstanceState()
或onCreate()
里獲取該數(shù)據(jù),從而恢復播放進度等狀態(tài)雪猪。下面是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),并且在頁面重啟后會幫我們恢復只恨。其中的mFragments是FragmentController译仗,它是一個Controller,內(nèi)部通過FragmentHostCallback間接控制FragmentManagerImpl官觅。由于FragmentController是間接控制古劲,沒有詳細保存Fragment狀態(tài)的內(nèi)容,所以我們直接看FragmentManagerImpl中的實現(xiàn)
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()
看到了關(guān)鍵的保存代碼缰猴,原來是通過FragmentManagerState來保存Fragment的狀態(tài)、所處Fragment棧下標疤剑、回退棧狀態(tài)等滑绒,而在restoreAllState()恢復時,通過FragmentManagerState里的FragmentState的instantiate()方法恢復了Fragment隘膘。我們重點看FragmentState疑故,F(xiàn)ragment的狀態(tài),類名弯菊、下標纵势、id、Tag管钳、ContainerId以及Arguments等數(shù)據(jù)就保存在里面钦铁。Fragment重疊的原因就與這個保存狀態(tài)的機制有關(guān)!
為什么會發(fā)生Fragment重疊才漆?
1.重復replace/add Fragment
我們知道加載Fragment有2種方式:replace()和add()牛曹。當發(fā)生內(nèi)存重啟時,比如屏幕發(fā)生旋轉(zhuǎn)醇滥,Activity會重新啟動黎比,默認Activity中的Fragment也會跟著Activity重新創(chuàng)建,這樣就造成了同一個Fragment會重復加載2次:
- 通過
onSaveInstanceState()
保存的Fragment會重新啟動鸳玩; - 當執(zhí)行Activity的
onCreate()
時阅虫,又會再次實例化一個新的Fragment這就是出現(xiàn)重疊的原因。
由以上分析可知不跟,一般情況下颓帝,我們會在Activity的onCreate()
里或者Fragment的onCreateView()
里加載根Fragment,如果在這里沒有進行頁面重啟的判斷的話,就可能導致重復加載Fragment引起重疊躲履。
2.使用show()或hide()控制Fragment(源碼support -v4 24.0.0以下)
由前面介紹可知见间,發(fā)生內(nèi)存重啟時,F(xiàn)ragment的狀態(tài)會被保存在FragmentState中工猜,但是在源碼support-v4 24.0.0以下米诉,F(xiàn)ragmentState里沒有mHidden
字段,默認情況下mHidden = false
篷帅。也就是說發(fā)生內(nèi)存重啟時史侣,沒有保存Fragment的顯示狀態(tài),導致頁面銷毀重建后魏身,F(xiàn)ragment就是默認情況下的show狀態(tài)惊橱,F(xiàn)ragment一次性從棧底向棧頂順序恢復時發(fā)生重疊。support-v424.0.0以下的FragmentState類源碼如下箭昵,它實現(xiàn)了Parcelable税朴,保存了Fragment的類名、下標家制、id正林、Tag、ContainerId以及Arguments等數(shù)據(jù)颤殴,但沒有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()里被調(diào)用
public Fragment instantiate(FragmentHostCallback host, Fragment parent) {
...省略
mInstance = Fragment.instantiate(context, mClassName, mArguments);
}
}
注:show()
觅廓、hide()
最終是讓Fragment的ViewsetVisibility(true或false)
,不會調(diào)用生命周期涵但;當使用add()
+show()
杈绸,hide()
跳轉(zhuǎn)新的Fragment時,舊的Fragment回調(diào)onHiddenChanged()
矮瘟,不會回調(diào)onStop()
等生命周期方法瞳脓,而新的Fragment在創(chuàng)建時是不會回調(diào)onHiddenChanged()
。
跟"重疊"輕松Say Goodbye
1.不重復replace/add Fragment
public class MyActivity ... {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
// 這里一定要在save為null時才加載Fragment澈侠,F(xiàn)ragment中onCreateView等生命周里加載根子Fragment同理
if(saveInstanceState == null){
// 正常情況下去 加載根Fragment
}
}
}
注:replace情況下篡殷,如果沒有加入回退棧,則不判斷也不會造成重疊埋涧;若加入回退棧板辽,則也會造成重疊現(xiàn)象,建議統(tǒng)一判斷下
2.show()
或hide()
Fragment不重疊(源碼support -v4 24.0.0及以上不用考慮)
從源碼角度的解決方案:從上面分析的原因棘催,我們知道Fragment重疊的根本原因在于FragmentState沒有保存Fragment的顯示狀態(tài)劲弦,即mHidden,那我們就自己手動在Fragment中維護一個mSupportHidden醇坝,在頁面重啟后邑跪,我們自己來決定Fragment是否顯示次坡。只需9行代碼!(摘自:9行代碼解決App內(nèi)的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全解析系列(二):正確的使用姿勢症脂。