該文分析的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ù)replace
|add
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重疊的解決方案虚青,同時會給出我通過分析源碼想到的一個解決方案,下一篇解決方案的傳送門