該文分析的support包版本為23.3.0,在24.0.0及以上官方已修復文章中所說的Fragment重疊BUG。
我們在使用Fragment的過程中,有時會發現一直表現正常的Fragment,突然重疊了!
什么情況下會發生Fragment重疊?
一般滿足下面2個條件才可能會發生重疊:
1、發生了頁面重啟(旋轉屏幕、內存不足等情況被強殺重啟)。
2、重復replace
|add
Fragment 或者 使用show
, hide
控制Fragment;
為什么會發生Fragment重疊?
從源碼角度分析,為什么發生頁面重啟后會導致重疊?(在以add方式加載Fragment的時候)
我們知道Activity中有個onSaveInstanceState()
方法,該方法會在Activity將要被kill的時候回調(例如進入后臺、屏幕旋轉前、跳轉下一個Activity等情況下會被調用)。
當Activity只執行onPause方法時(透明Activity),這時候如果App設置的targetVersion大于11則不會執行onSaveInstanceState方法
此時系統幫我們保存一個Bundle類型的數據,我們可以根據自己的需求,手動保存一些例如播放進度等數據,而后如果發生了頁面重啟,我們可以在onRestoreInstanceState()
或onCreate()
里get該數據,從而恢復播放進度等狀態。
而產生Fragment重疊的原因就與這個保存狀態的機制有關,大致原因就是系統在頁面重啟前,幫我們保存了Fragment的狀態,但是在重啟后恢復時,視圖的可見狀態沒幫我們保存,而Fragment默認的是show狀態,所以產生了Fragment重疊現象。
分析:
我們先看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。
相關代碼如下:
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對象來控制恢復工作。
我們接著看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(見下面的分析就明白啦)
我們看下FragmentManagerState:
final class FragmentManagerState implements Parcelable {
FragmentState[] mActive; // Fragment狀態
int[] mAdded; // 所處Fragment棧下標
BackStackState[] mBackStack; // 回退棧狀態
...
}
我們只看FragmentState,它也實現了Parcelable,保存了Fragment的類名、下標、id、Tag、ContainerId以及Arguments等數據:
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);
}
}
至此,我們就明白了系統幫我們保存的Fragment其實最終是以FragmentState形式存在的。
此時我們再思考下為什么在頁面重啟后會發生Fragment的重疊? 其實答案已經很明顯了,根據上面的源碼分析,我們會發現FragmentState里沒有Hidden狀態的字段!
而Hidden狀態對應Fragment中的mHidden
,該值默認false...
public class Fragment ... {
boolean mHidden;
}
我想你應該明白了,在以add方式加載Fragment的場景下,系統在恢復Fragment時,mHidden=false
,即show狀態,這樣在頁面重啟后,Activity內的Fragment都是以show狀態顯示的,而如果你不進行處理,那么就會發生Fragment重疊現象!
為什么重復replace
|add
Fragment 或者 使用show
, hide
控制Fragment會導致重疊?
- **重復
replace
|add
Fragment **
我們知道加載Fragment有2種方式:replace()
和add()
。
不管哪種方式,重復加載Fragment都會導致重疊,這個很好理解,你加載同一個Fragment2次當然會重疊了;問題是我們在哪里重復加載了Fragment?
一般情況下,我們會在Activity的onCreate()
里或者Fragment的onCreateView()
里加載根Fragment,如果在這里沒有進行頁面重啟的判斷的話,就可能導致重復加載Fragment引起重疊,正確的寫法應該是:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
// 判空, Fragment同理
if(findFragmentByTag(RootFragment) == null){
// 這里replace或add 根Fragment
}
}
這里一定要在saveInstanceState==null
時才加載Fragment,因為經過上面的分析,在頁面重啟時,Fragment的狀態會被保存恢復,而此時再加載Fragment會重復加載,就導致棧已經有該Fragment的情況下,再加載一該Fragment,從而導致重疊!
-
使用
show
,hide
控制Fragment
我們使用show()
,hide()
時,都是使用add
的方式加載Fragment的,add配合hide使Fragment的視圖改變為GONE狀態;而replace是銷毀Fragment 的視圖。
頁面重啟時,add的Fragment會全部走生命周期,創建視圖;而replace的非棧頂Fragment不會走生命周期,只有Back時,才會逐一走棧頂Fragment生命周期,創建視圖。
結合上面的源碼分析,在使用replace加載Fragment時,頁面重啟后,Fragment視圖都還沒創建,所以mHidden
沒有意義,不會發生重疊現象;
而在使用add加載時,視圖是存在的并且疊加在一起,頁面重啟后 mHidden=false
,所有的Fragment都會是show狀態顯示出來(即VISIBLE),從而造成了Fragment重疊!
最后&解決方案
通過上面的分析,我想小伙伴們應該徹底明白Fragment重疊的原因了吧!
鑒于篇幅原因,我另寫了一篇簡書來談談 Fragment重疊的解決方案,同時會給出我通過分析源碼想到的一個解決方案,下一篇解決方案的傳送門