從源碼角度分析,為什么會發生Fragment重疊?

該文分析的support包版本為23.3.0,在24.0.0及以上官方已修復文章中所說的Fragment重疊BUG。

我們在使用Fragment的過程中,有時會發現一直表現正常的Fragment,突然重疊了!

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

一般滿足下面2個條件才可能會發生重疊:

1、發生了頁面重啟(旋轉屏幕、內存不足等情況被強殺重啟)。
2、重復replaceadd 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重疊的解決方案,同時會給出我通過分析源碼想到的一個解決方案,下一篇解決方案的傳送門

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容