Android進階寶典 -- 從原始碼角度全面分析Frgament原理

語言: CN / TW / HK

在Android中,真正作為承載頁面級別的元件就兩個:Activity和Fragment。說起Activity,我想夥伴們都非常熟悉,如果想新建一個頁面,那麼就建立一個Activity,這個是傳統的開發思想。

夥伴們可以想一想,當一個專案成規模以後,Activity的量級達到了50+ or 100+,這個時候頁面之間的跳轉就需要路由來管理,如果想寫startActivity寫到吐,那麼也沒問題。那麼有沒有好的方式能夠非常方便地管理頁面,而且能夠用最少量的Activity,建議夥伴們去了解下Navigation,其內部有一套Fragment的管理機制。

image.png

為啥要出這篇Fragment核心原理分析,就是因為在專案中使用Navigation的時候遇到了一些問題,但是夥伴們可能對於Fragment的原理不是那麼瞭解,像生命週期、Fragment事務管理、回退棧等,那麼在使用Navigation的時候,如果想要Hook Navigation原始碼,不知道如何處理Fragment之間的跳轉邏輯,那麼看了這篇文章,可能會有所幫助。

1 Fragment基礎概念

1.1 Fragment的生命週期

首先我們先要知道,Fragment是不能像Activity那樣獨立存在,你可以認為它是一個View,它必須要依賴於Activity存在,而且是受Activity的生命週期影響,從而改變自身的生命週期,反之它沒有影響Activity生命週期的能力。

那麼我們從原始碼的角度看一下,Activity是如何影響Fragment的生命週期的,然後從例項出發驗證一下與原始碼是否一致。那麼我們從FragmentActivity的onCreate方法開始,當Activity呼叫onCreate方法的時候,就會回撥這個方法。

```java @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);

mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
mFragments.dispatchCreate();

} ```

首先我們看一下,mFragments是什麼,它是一個FragmentController,從字面意思上看,是Fragment的一個控制器 java final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); 當Activity執行onCreate方法的時候,最終是呼叫FragmentManager的dispatchCreate方法 java public void dispatchStart() { mHost.mFragmentManager.dispatchStart(); } java void dispatchCreate() { mStateSaved = false; mStopped = false; mNonConfig.setIsStateSaved(false); // 核心程式碼 - 1 dispatchStateChange(Fragment.CREATED); }

核心程式碼 - 1

我們看下Fragment的生命週期是如何發生變化的,首先在dispatchStateChange方法中,會傳入一個int值, java private void dispatchStateChange(int nextState) { try { mExecutingActions = true; mFragmentStore.dispatchStateChange(nextState); moveToState(nextState, false); if (USE_STATE_MANAGER) { Set<SpecialEffectsController> controllers = collectAllSpecialEffectsController(); for (SpecialEffectsController controller : controllers) { controller.forceCompleteAllOperations(); } } } finally { mExecutingActions = false; } execPendingActions(true); } 這個值從INITIALIZING -> RESUMED是升序排序,這裡我們可能會有疑問,Fragment的onPause、onStop、onDestory去哪了,為啥只到了RESUMEDjava static final int INITIALIZING = -1; // Not yet attached. static final int ATTACHED = 0; // Attached to the host. static final int CREATED = 1; // Created. static final int VIEW_CREATED = 2; // View Created. static final int AWAITING_EXIT_EFFECTS = 3; // Downward state, awaiting exit effects static final int ACTIVITY_CREATED = 4; // Fully created, not started. static final int STARTED = 5; // Created and started, not resumed. static final int AWAITING_ENTER_EFFECTS = 6; // Upward state, awaiting enter effects static final int RESUMED = 7; // Created started and resumed. 彆著急,我們往下看moveToState方法。 ```java void moveToState(int newState, boolean always) { if (mHost == null && newState != Fragment.INITIALIZING) { throw new IllegalStateException("No activity"); }

if (!always && newState == mCurState) {
    return;
}

mCurState = newState;

if (USE_STATE_MANAGER) {
    mFragmentStore.moveToExpectedState();
} else {
    // Must add them in the proper order. mActive fragments may be out of order
    for (Fragment f : mFragmentStore.getFragments()) {
        moveFragmentToExpectedState(f);
    }

    // Now iterate through all active fragments. These will include those that are removed
    // and detached.
    for (FragmentStateManager fragmentStateManager :
            mFragmentStore.getActiveFragmentStateManagers()) {
        Fragment f = fragmentStateManager.getFragment();
        if (!f.mIsNewlyAdded) {
            moveFragmentToExpectedState(f);
        }
        boolean beingRemoved = f.mRemoving && !f.isInBackStack();
        if (beingRemoved) {
            mFragmentStore.makeInactive(fragmentStateManager);
        }
    }
}

startPendingDeferredFragments();

if (mNeedMenuInvalidate && mHost != null && mCurState == Fragment.RESUMED) {
    mHost.onSupportInvalidateOptionsMenu();
    mNeedMenuInvalidate = false;
}

} 其中,核心方法為moveFragmentToExpectedState,傳入的引數為儲存的Fragment例項,最終生命週期的同步,是在moveToState方法中,此時兩個引數:一個是Fragment例項,另一個是即將更新的Fragment的生命週期狀態。java void moveToState(@NonNull Fragment f, int newState) { FragmentStateManager fragmentStateManager = mFragmentStore.getFragmentStateManager(f.mWho); //...... if (f.mFromLayout && f.mInLayout && f.mState == Fragment.VIEW_CREATED) { newState = Math.max(newState, Fragment.VIEW_CREATED); } newState = Math.min(newState, fragmentStateManager.computeExpectedState()); if (f.mState <= newState) { // If we are moving to the same state, we do not need to give up on the animation. if (f.mState < newState && !mExitAnimationCancellationSignals.isEmpty()) { // The fragment is currently being animated... but! Now we // want to move our state back up. Give up on waiting for the // animation and proceed from where we are. cancelExitAnimation(f); } switch (f.mState) { case Fragment.INITIALIZING: if (newState > Fragment.INITIALIZING) { fragmentStateManager.attach(); } // fall through case Fragment.ATTACHED: if (newState > Fragment.ATTACHED) { fragmentStateManager.create(); } // fall through case Fragment.CREATED: // We want to unconditionally run this anytime we do a moveToState that // moves the Fragment above INITIALIZING, including cases such as when // we move from CREATED => CREATED as part of the case fall through above. if (newState > Fragment.INITIALIZING) { fragmentStateManager.ensureInflatedView(); }

            if (newState > Fragment.CREATED) {
                fragmentStateManager.createView();
            }
            // fall through
        case Fragment.VIEW_CREATED:
            if (newState > Fragment.VIEW_CREATED) {
                fragmentStateManager.activityCreated();
            }
            // fall through
        case Fragment.ACTIVITY_CREATED:
            if (newState > Fragment.ACTIVITY_CREATED) {
                fragmentStateManager.start();
            }
            // fall through
        case Fragment.STARTED:
            if (newState > Fragment.STARTED) {
                fragmentStateManager.resume();
            }
    }
} else if (f.mState > newState) {
    switch (f.mState) {
        case Fragment.RESUMED:
            if (newState < Fragment.RESUMED) {
                fragmentStateManager.pause();
            }
            // fall through
        case Fragment.STARTED:
            if (newState < Fragment.STARTED) {
                fragmentStateManager.stop();
            }
            // fall through
        case Fragment.ACTIVITY_CREATED:
            if (newState < Fragment.ACTIVITY_CREATED) {
                if (isLoggingEnabled(Log.DEBUG)) {
                    Log.d(TAG, "movefrom ACTIVITY_CREATED: " + f);
                }
                if (f.mView != null) {
                    // Need to save the current view state if not
                    // done already.
                    if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
                        fragmentStateManager.saveViewState();
                    }
                }
            }
            // fall through
        case Fragment.VIEW_CREATED:
            if (newState < Fragment.VIEW_CREATED) {
                FragmentAnim.AnimationOrAnimator anim = null;
                if (f.mView != null && f.mContainer != null) {
                    // Stop any current animations:
                    f.mContainer.endViewTransition(f.mView);
                    f.mView.clearAnimation();
                    // If parent is being removed, no need to handle child animations.
                    if (!f.isRemovingParent()) {
                        if (mCurState > Fragment.INITIALIZING && !mDestroyed
                                && f.mView.getVisibility() == View.VISIBLE
                                && f.mPostponedAlpha >= 0) {
                            anim = FragmentAnim.loadAnimation(mHost.getContext(),
                                    f, false, f.getPopDirection());
                        }
                        f.mPostponedAlpha = 0;
                        // Robolectric tests do not post the animation like a real device
                        // so we should keep up with the container and view in case the
                        // fragment view is destroyed before we can remove it.
                        ViewGroup container = f.mContainer;
                        View view = f.mView;
                        if (anim != null) {
                            FragmentAnim.animateRemoveFragment(f, anim,
                                    mFragmentTransitionCallback);
                        }
                        container.removeView(view);
                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                            Log.v(FragmentManager.TAG, "Removing view " + view + " for "
                                    + "fragment " + f + " from container " + container);
                        }
                        // If the local container is different from the fragment
                        // container, that means onAnimationEnd was called, onDestroyView
                        // was dispatched and the fragment was already moved to state, so
                        // we should early return here instead of attempting to move to
                        // state again.
                        if (container != f.mContainer) {
                            return;
                        }
                    }
                }
                // If a fragment has an exit animation (or transition), do not destroy
                // its view immediately and set the state after animating
                if (mExitAnimationCancellationSignals.get(f) == null) {
                    fragmentStateManager.destroyFragmentView();
                }
            }
            // fall through
        case Fragment.CREATED:
            if (newState < Fragment.CREATED) {
                if (mExitAnimationCancellationSignals.get(f) != null) {
                    // We are waiting for the fragment's view to finish animating away.
                    newState = Fragment.CREATED;
                } else {
                    fragmentStateManager.destroy();
                }
            }
            // fall through
        case Fragment.ATTACHED:
            if (newState < Fragment.ATTACHED) {
                fragmentStateManager.detach();
            }
    }
}

if (f.mState != newState) {
    if (isLoggingEnabled(Log.DEBUG)) {
        Log.d(TAG, "moveToState: Fragment state for " + f + " not updated inline; "
                + "expected state " + newState + " found " + f.mState);
    }
    f.mState = newState;
}

} ``` 其實這個方法看著長,但是很簡單,就是將newState與當前Fragment的狀態做一次比較,如果傳入的狀態(newState)比當前要大,例如:

```java f.mState:CREATED -> 1 newState:VIEW_CREATED -> 2

fragmentStateManager.createView(); ``` 此時就會呼叫fragmentStateManager的createView方法,最終會呼叫Fragment的onCreateView方法,進行View的建立。

反之,如果傳入的狀態(newState)比當前要小,例如:

```java f.mState:RESUMED -> 7 newState:STARTED -> 5

fragmentStateManager.pause(); ``` 此時Fragment就進入了onPuse的狀態,所以Google工程師在Androidx之後,將狀態就限制到RESUMED,然後通過同步比較狀態的這種方式,進行生命週期狀態的回撥。

如果看過LifeCycle的原始碼,對於生命週期狀態的同步應該也會比較瞭解,感興趣的夥伴可以看下這篇文章: Android進階寶典 -- Jetpack篇(最新LiveData LifeCycle原始碼分析)

所以Activity是通過什麼手段去影響Fragment生命週期的呢?就是通過FragmentController呼叫dispatchCreate、dispatchResume......,其實內部是通過FragmentManager來管理,通過生命週期同步的方式來主動呼叫Fragment的生命週期方法。

1.2 Fragment的事務管理

如果人為管理Fragment,一般都是通過Transaction進行事務管理,

```kotlin //事務管理 val beginTransaction = supportFragmentManager.beginTransaction() beginTransaction.add(R.id.fl_fg,Fragment01()) beginTransaction.replace(R.id.fl_fg,Fragment01()) beginTransaction.hide(Fragment01()) beginTransaction.show(Fragment01())

beginTransaction.commit() ```

大概分為4種操作:add、replace、hide、show;其中在使用的時候,一般是add、replace是一掛,hide和show是一掛,具體的差別我們稍後再說,我們先看下Transaction是何許人也。

java @NonNull public FragmentTransaction beginTransaction() { return new BackStackRecord(this); } 在呼叫beginTransaction方法的時候,其實是建立了一個BackStackRecord例項,從字面意思上看是回退棧記錄類,用來記錄每個Fragment的回退棧的。

那麼在呼叫add、replace、hide、show的時候,其實就是呼叫BackStackRecord的方法,我們看下這幾個方法的實現。

java @NonNull public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment) { doAddOp(containerViewId, fragment, null, OP_ADD); return this; } 在呼叫add的時候,內部呼叫了doAddOp方法。 ```java void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) { final Class<?> fragmentClass = fragment.getClass(); final int modifiers = fragmentClass.getModifiers(); if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers) || (fragmentClass.isMemberClass() && !Modifier.isStatic(modifiers))) { throw new IllegalStateException("Fragment " + fragmentClass.getCanonicalName() + " must be a public static class to be properly recreated from" + " instance state."); }

if (tag != null) {
    if (fragment.mTag != null && !tag.equals(fragment.mTag)) {
        throw new IllegalStateException("Can't change tag of fragment "
                + fragment + ": was " + fragment.mTag
                + " now " + tag);
    }
    fragment.mTag = tag;
}

if (containerViewId != 0) {
    if (containerViewId == View.NO_ID) {
        throw new IllegalArgumentException("Can't add fragment "
                + fragment + " with tag " + tag + " to container view with no id");
    }
    if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
        throw new IllegalStateException("Can't change container ID of fragment "
                + fragment + ": was " + fragment.mFragmentId
                + " now " + containerViewId);
    }
    fragment.mContainerId = fragment.mFragmentId = containerViewId;
}

addOp(new Op(opcmd, fragment));

} ``` doAddOp方法前面是做了一些判斷,有幾個引數我要說一下:

(1)tag:這個引數可以在我們呼叫add的時候自定義傳入,如果我們想要獲取add加入的這個Fragment,可以通過findFragmentByTag方法來獲取;

(2)containerViewId:這個是裝載Fragment的容器,一般需要我們自行設定一個FrameLayout,取FrameLayout的id。

最後呼叫addOp方法,建立一個Op物件,其中Op物件中有兩個引數比較重要:opcmd代表要執行的操作,例如OP_ADD(add操作)、fragment代表建立的Fragment的例項。 java void addOp(Op op) { mOps.add(op); op.mEnterAnim = mEnterAnim; op.mExitAnim = mExitAnim; op.mPopEnterAnim = mPopEnterAnim; op.mPopExitAnim = mPopExitAnim; } 建立完成之後,存放在mOps陣列中。

java @NonNull public FragmentTransaction replace(@IdRes int containerViewId, @NonNull Fragment fragment, @Nullable String tag) { if (containerViewId == 0) { throw new IllegalArgumentException("Must use non-zero containerViewId"); } doAddOp(containerViewId, fragment, tag, OP_REPLACE); return this; } ```java @NonNull public FragmentTransaction hide(@NonNull Fragment fragment) { addOp(new Op(OP_HIDE, fragment));

return this;

} java @NonNull public FragmentTransaction show(@NonNull Fragment fragment) { addOp(new Op(OP_SHOW, fragment));

return this;

} ```

除此之外,我們看下replace、hide、show的邏輯,其實都是建立一個Op物件,然後存放在mOps陣列中,當所有的事務準備好之後,最終需要呼叫commit來執行。

kotlin beginTransaction.commit() beginTransaction.commitAllowingStateLoss() beginTransaction.commitNow() beginTransaction.commitNowAllowingStateLoss() commit執行有以上四種方式,這4種方式有什麼區別呢?我們從原始碼角度來看一下。

commit和commitAllowingStateLoss的區別

java @Override public int commit() { return commitInternal(false); } 我們看到,在commit方法執行的時候,其實是呼叫了commitInternal方法,傳入了一個false引數,代表是否允許狀態丟失。 java int commitInternal(boolean allowStateLoss) { if (mCommitted) throw new IllegalStateException("commit already called"); if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) { Log.v(TAG, "Commit: " + this); LogWriter logw = new LogWriter(TAG); PrintWriter pw = new PrintWriter(logw); dump(" ", pw); pw.close(); } mCommitted = true; if (mAddToBackStack) { mIndex = mManager.allocBackStackIndex(); } else { mIndex = -1; } mManager.enqueueAction(this, allowStateLoss); return mIndex; }

其實看到這裡,我們能夠猜到commitAllowingStateLoss方法呼叫的時候,commitInternal方法傳入的一定是true,允許狀態丟失。 java @Override public int commitAllowingStateLoss() { return commitInternal(true); } 在commitInternal方法中,呼叫了enqueueAction方法,將此次事務處理加入佇列中。 java void enqueueAction(@NonNull OpGenerator action, boolean allowStateLoss) { if (!allowStateLoss) { if (mHost == null) { if (mDestroyed) { throw new IllegalStateException("FragmentManager has been destroyed"); } else { throw new IllegalStateException("FragmentManager has not been attached to a " + "host."); } } checkStateLoss(); } synchronized (mPendingActions) { if (mHost == null) { if (allowStateLoss) { // This FragmentManager isn't attached, so drop the entire transaction. return; } throw new IllegalStateException("Activity has been destroyed"); } mPendingActions.add(action); // 核心程式碼 - 2 scheduleCommit(); } } 在一開始就判斷allowStateLoss是否為false,如果為false,也就是通過commit方式提交,那麼會進入程式碼塊,呼叫checkStateLoss方法。 java private void checkStateLoss() { if (isStateSaved()) { throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } } java public boolean isStateSaved() { // See saveAllState() for the explanation of this. We do this for // all platform versions, to keep our behavior more consistent between // them. return mStateSaved || mStopped; }

在這個方法中,會判斷當前Fragment的狀態,有兩個值mStateSaved 或者 mStopped有一個為true,那麼就會拋異常。

我說一個場景:當用戶退出後臺的瞬間,呼叫commit事務提交,此時mStopped = true,應用就會崩潰。其實這種場景還是比較常見的,在開發中可能很少碰到,但是如果上線後從bugly中可能會看到這種崩潰,但是使用者其實是無感知的,所以這種情況下建議使用commitAllowingStateLoss。

所以當Activity狀態發生變化的時候,例如退出後臺、螢幕旋轉等,使用commitAllowingStateLoss不會拋異常。

commitNow和commitNowAllowingStateLoss的區別

這兩種提交方式,我們使用的好像比較少,我們看下他倆和前面的有啥區別。 java @Override public void commitNow() { disallowAddToBackStack(); mManager.execSingleAction(this, false); } java @Override public void commitNowAllowingStateLoss() { disallowAddToBackStack(); mManager.execSingleAction(this, true); }

首先從原始碼中我們看到,這種提交方式是不允許將Fragment加入到回退棧的,在這個方法中,會將mAllowAddToBackStack設定為false。 java @NonNull public FragmentTransaction disallowAddToBackStack() { if (mAddToBackStack) { throw new IllegalStateException( "This transaction is already being added to the back stack"); } mAllowAddToBackStack = false; return this; } 那麼這個時候,如果呼叫addToBackStack方法,因為mAllowAddToBackStack = false,此時就直接丟擲異常。 java @NonNull public FragmentTransaction addToBackStack(@Nullable String name) { if (!mAllowAddToBackStack) { throw new IllegalStateException( "This FragmentTransaction is not allowed to be added to the back stack."); } mAddToBackStack = true; mName = name; return this; } 而且呼叫commitNow方法的時候,如果當前Fragment已經被加入到回退棧了,也會丟擲異常。

核心程式碼 - 2

這是一個區別,接下來我們關注一下commit提交和commitNow提交的另一個區別。如果通過commit提交,那麼最終呼叫這個方法scheduleCommit,我們看到是通過Handler來發送一個訊息來非同步執行事務的提交; java void scheduleCommit() { synchronized (mPendingActions) { boolean postponeReady = mPostponedTransactions != null && !mPostponedTransactions.isEmpty(); boolean pendingReady = mPendingActions.size() == 1; if (postponeReady || pendingReady) { mHost.getHandler().removeCallbacks(mExecCommit); mHost.getHandler().post(mExecCommit); updateOnBackPressedCallbackEnabled(); } } } 那麼commitNow在執行事務提交的時候,我們看下execSingleAction方法,發現是同步完成的,所以兩者的另一個區別就是執行事務時,commit是非同步操作,而commitNow是同步的。 ```java void execSingleAction(@NonNull OpGenerator action, boolean allowStateLoss) { if (allowStateLoss && (mHost == null || mDestroyed)) { // This FragmentManager isn't attached, so drop the entire transaction. return; } ensureExecReady(allowStateLoss); if (action.generateOps(mTmpRecords, mTmpIsPop)) { mExecutingActions = true; try { removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop); } finally { cleanupExec(); } }

updateOnBackPressedCallbackEnabled();
doPendingDeferredStart();
mFragmentStore.burpActive();

} ```

所以這個會解決什麼問題呢?熟悉的Handler的夥伴們應該知道,所有Handler傳送的訊息都會存在MessageQueue中,Looper通過loop方法從MessageQueue中取出事件並執行,所以當我們通過commit去提交新增一個Fragment的時候,如果還沒有執行到這個事件,就通過findFragmentByTag or findFragmentById去查詢這個Fragment就會找不到,有沒有夥伴們碰到過這個問題?

所以如果我們業務場景中必須要保證要拿到這個Fragment,那麼建議使用commitNow這個提交方式,但是需要注意回退棧的問題,我們可以通過反射的方式,將mAllowAddToBackStack設定為true,避免丟擲異常。但是這種方式也需要根據場景酌情使用,因為頻繁地使用commitNow可能會導致卡頓。

1.3 Fragment狀態儲存

當我們的應用發生異常,或者Activity的狀態發生變化時,我們想儲存Fragment的狀態,以便後續的展示,那麼我們先看下 Activity # onSaveInstanceState方法是怎麼實現的。 ```java protected void onSaveInstanceState(@NonNull Bundle outState) { outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
// 1
Parcelable p = mFragments.saveAllState();
if (p != null) {
    outState.putParcelable(FRAGMENTS_TAG, p);
}
if (mAutoFillResetNeeded) {
    outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
    getAutofillManager().onSaveInstanceState(outState);
}
dispatchActivitySaveInstanceState(outState);

} ```

首先,我們看呼叫了mFragments的saveAllState方法,mFragments還是我們之前看到的FragmentController,呼叫了還是FragmentManager的saveAllState方法。 ```java Parcelable saveAllState() { // Make sure all pending operations have now been executed to get // our state update-to-date. forcePostponedTransactions(); endAnimatingAwayFragments(); execPendingActions(true);

mStateSaved = true;
mNonConfig.setIsStateSaved(true);

// First collect all active fragments.
ArrayList<FragmentState> active = mFragmentStore.saveActiveFragments();

if (active.isEmpty()) {
    if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "saveAllState: no fragments!");
    return null;
}

// Build list of currently added fragments.
ArrayList<String> added = mFragmentStore.saveAddedFragments();

// Now save back stack.
BackStackState[] backStack = null;
if (mBackStack != null) {
    int size = mBackStack.size();
    if (size > 0) {
        backStack = new BackStackState[size];
        for (int i = 0; i < size; i++) {
            backStack[i] = new BackStackState(mBackStack.get(i));
            if (isLoggingEnabled(Log.VERBOSE)) {
                Log.v(TAG, "saveAllState: adding back stack #" + i
                        + ": " + mBackStack.get(i));
            }
        }
    }
}

FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
fms.mBackStackIndex = mBackStackIndex.get();
if (mPrimaryNav != null) {
    fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
}
fms.mResultKeys.addAll(mResults.keySet());
fms.mResults.addAll(mResults.values());
fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
return fms;

} ``` 首先拿到當前頁面展示的Fragment,並將其封裝為FragmentState,返回的是一個列表;然後拿到通過事務新增進來的全部Fragment的UUID集合,最終建立一個FragmentManagerState類,然後將全部的Fragment的狀態儲存到FragmentManagerState中,最終返回的序列化資料就是FragmentManagerState

那麼恢復資料,其實就是一個反序列化的過程,通過拿到FragmentManagerState資料之後,恢復所有Fragment在銷燬之前的狀態。

這裡需要注意一點,當系統恢復Fragment的時候,是採用反射的方式進行Fragment的建立,此時是通過newInstance()的方式完成的,所以Fragment一定要有一個空參構造方法,否則直接拋異常。

1.4 Fragment回退棧管理

如果閱讀過Navigation原始碼,我們會發現,當執行commit之前,都會將Fragment加入回退棧,那麼將Fragment新增到回退棧和不新增到回退棧,有什麼區別呢?

我們看下面這個場景,當建立Fragment01之後,點選按鈕跳轉到Fragment02,此時我們看當Fragment02展示之後,Fragment01直接走了銷燬的流程,而且已經呼叫了onDestroy方法。

java 2023-02-11 18:35:56.070 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onAttach 2023-02-11 18:35:56.071 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onCreate 2023-02-11 18:35:56.072 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onCreateView 2023-02-11 18:35:56.085 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onViewCreated 2023-02-11 18:35:56.095 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onResume 2023-02-11 18:36:45.065 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onPause 2023-02-11 18:36:45.068 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onStop 2023-02-11 18:36:45.072 30604-30604/com.lay.learn.asm E/TAG: Fragment02 onAttach 2023-02-11 18:36:45.075 30604-30604/com.lay.learn.asm E/TAG: Fragment02 onCreate 2023-02-11 18:36:45.079 30604-30604/com.lay.learn.asm E/TAG: Fragment02 onCreateView 2023-02-11 18:36:45.118 30604-30604/com.lay.learn.asm E/TAG: Fragment02 onViewCreated 2023-02-11 18:36:45.138 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onDestroyView 2023-02-11 18:36:45.162 30604-30604/com.lay.learn.asm E/TAG: Fragment01 onDestroy 2023-02-11 18:36:45.165 30604-30604/com.lay.learn.asm E/TAG: Fragment02 onResume 當我們將Fragment01加入到回退棧之後,我們發現,Fragment01好像並沒有呼叫onDestory方法,僅僅是將View銷燬了 java 2023-02-11 18:41:05.538 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onAttach 2023-02-11 18:41:05.540 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onCreate 2023-02-11 18:41:05.542 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onCreateView 2023-02-11 18:41:05.560 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onViewCreated 2023-02-11 18:41:05.576 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onResume 2023-02-11 18:41:09.784 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onPause 2023-02-11 18:41:09.787 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onStop 2023-02-11 18:41:09.788 30912-30912/com.lay.learn.asm E/TAG: Fragment02 onAttach 2023-02-11 18:41:09.790 30912-30912/com.lay.learn.asm E/TAG: Fragment02 onCreate 2023-02-11 18:41:09.796 30912-30912/com.lay.learn.asm E/TAG: Fragment02 onCreateView 2023-02-11 18:41:09.811 30912-30912/com.lay.learn.asm E/TAG: Fragment02 onViewCreated 2023-02-11 18:41:09.819 30912-30912/com.lay.learn.asm E/TAG: Fragment01 onDestroyView 2023-02-11 18:41:09.837 30912-30912/com.lay.learn.asm E/TAG: Fragment02 onResume

那麼此時,我們點選返回按鈕,這個時候我們發現Fragment01生命週期是從頭重新執行了嗎?並沒有,而是直接從onCreateView開始,沒有執行onCreate。

```java 2023-02-11 19:04:42.886 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onAttach 2023-02-11 19:04:42.887 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onCreate 2023-02-11 19:04:42.888 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onCreateView 2023-02-11 19:04:42.897 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onViewCreated 2023-02-11 19:04:42.908 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onResume 2023-02-11 19:04:45.572 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onPause 2023-02-11 19:04:45.572 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onStop 2023-02-11 19:04:45.573 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onAttach 2023-02-11 19:04:45.574 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onCreate 2023-02-11 19:04:45.574 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onCreateView 2023-02-11 19:04:45.582 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onViewCreated 2023-02-11 19:04:45.585 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onDestroyView 2023-02-11 19:04:45.588 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onResume 2023-02-11 19:04:46.874 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onPause 2023-02-11 19:04:46.875 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onStop 2023-02-11 19:04:46.875 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onCreateView 2023-02-11 19:04:46.881 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onViewCreated 2023-02-11 19:04:46.883 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onDestroyView 2023-02-11 19:04:46.884 25027-25027/com.lay.learn.asm E/TAG: Fragment02 onDestroy 2023-02-11 19:04:46.885 25027-25027/com.lay.learn.asm E/TAG: Fragment01 onResume

```

所以加入回退棧的作用,我給夥伴們總結一下:

(1)通過replace的方式執行頁面之間的切換,加入回退棧能夠避免資料丟失(onDestory時ViewModel資料會被清除),防止頁面被直接銷燬;

(2)加入回退棧更符合使用者行為邏輯,從哪個頁面來返回就返回哪個頁面,而且不會重複呼叫onCreate方法,因此可以放網路請求的邏輯,避免多次介面請求。

我們這裡帶一下Navigation的回退棧管理 ```kotlin private fun navigate( entry: NavBackStackEntry, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? ) { val initialNavigation = state.backStack.value.isEmpty() val restoreState = ( navOptions != null && !initialNavigation && navOptions.shouldRestoreState() && savedIds.remove(entry.id) ) if (restoreState) { // Restore back stack does all the work to restore the entry fragmentManager.restoreBackStack(entry.id) state.push(entry) return } val ft = createFragmentTransaction(entry, navOptions)

if (!initialNavigation) {
    ft.addToBackStack(entry.id)
}

if (navigatorExtras is Extras) {
    for ((key, value) in navigatorExtras.sharedElements) {
        ft.addSharedElement(key, value)
    }
}
ft.commit()
// The commit succeeded, update our view of the world
state.push(entry)

} ``` 這裡有個變數需要注意一下:initialNavigation,它是一個boolean型別,判斷當前回退棧是否為空,如果是第一次使用,那麼就為空,此時不會給當前頁面添加回退棧。這裡其實很好了解,如果把起點加入回退棧,那麼在返回的時候,起點其實已經沒有上級頁面了,就不知道要往哪跳,所以系統會生成一個空白頁面,這裡大家可以使用一下。

當然有些問題還是避免不了,因為如果加入回退棧,那麼Fragment的onCreateView可能會被多次執行,會導致頁面的狀態發生變化,無法保留上次頁面跳轉時狀態,頁面會被重新整理,因此可以考慮使用hide show的方式來進行頁面狀態管理。

2 Fragment常見問題解決

這個模組我主要介紹一下我們在日常開發中經常會遇到的問題

2.1 Can not perform this action after onSaveInstanceState

在呼叫commit方法的時候,因為allowStateloss為false,所以需要檢查狀態。 java private void checkStateLoss() { if (isStateSaved()) { throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } } java public boolean isStateSaved() { // See saveAllState() for the explanation of this. We do this for // all platform versions, to keep our behavior more consistent between // them. return mStateSaved || mStopped; }

如果在當前頁面發起網路請求,等到請求結果之前可能會有耗時,然後此時跳轉到了下一個頁面,我們知道這個頁面的生命週期會走到onDestoryView,此時會觸發onSaveInstance方法,mStateSaved會設定為true,那麼此時如果在拿到結果之後又進行了一次commit,就直接回丟擲異常。

所以對於commit提交來說,不建議在子執行緒中進行;如果確實需要這種操作,那麼就建議使用commitAllowingStateLoss。

可能很多夥伴也會好奇,在實際開發中這種問題很難碰到,一翻bugly就會看到好多線上使用者報這個問題。這個就是使用者場景我們無法cover全覆蓋,使用者可能用2G、3G網路就會出現網路載入緩慢的問題。

2.2 Fragment的重疊問題

這種問題其實如果瞭解系統的恢復機制,應該還是很好避免。為什麼會出現Fragment重疊的問題呢?首先我們做的時候是在Activity # onCreate方法中進行add操作。 ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_my_fragment_actvity) fl_fg = findViewById(R.id.fl_fg) btn_jump = findViewById(R.id.btn_jump)

btn_jump.setOnClickListener {
    val transient = supportFragmentManager.beginTransaction()
    transient.replace(R.id.fl_fg,Fragment02())
    transient.addToBackStack(null)
    transient.commit()
}
//事務管理
val beginTransaction = supportFragmentManager.beginTransaction()
beginTransaction.add(R.id.fl_fg, Fragment01())
beginTransaction.addToBackStack("Fragment01")

beginTransaction.commit()

} ``` 如果此時螢幕進行旋轉,前提是在沒有任何配置的情況下,Activity會被銷燬重建,此時按照我們在1.3小節中對於Fragment狀態儲存的瞭解,此時會將Fragment的狀態儲存為FragmentManagerState並將其序列化,在Activity重建之後,onCreate中會獲取FragmentManagerState並重建Fragment,此時其實系統已經幫我們重建了Fragment,但是我們在onCreate中再次進行了add操作,此時就會造成Fragment疊加。

其實我們瞭解這個機制之後就很好解決了,首先第一種方案:在onSaveInstanceState方法中,不去儲存Fragment的狀態,但是這種方案可能會有風險,因為一刀切可能會影響其他的功能。

另一種就是呼叫add的時機,當savedInstanceState為空的時候,一般就是首次進來的時候,這個時候就可以執行add;但是如果是重建狀態下,savedInstanceState不為空,就不需要自行add,使用系統幫我們恢復的那一份就行。 ```kotlin if (savedInstanceState == null) { //事務管理 val beginTransaction = supportFragmentManager.beginTransaction() beginTransaction.add(R.id.fl_fg, Fragment01()) beginTransaction.addToBackStack("Fragment01")

beginTransaction.commit()

} ```

除此之外,之前在使用Navigation的時候,因為官方的那種方式是採用replace的方式會導致View的狀態丟失,因此自定義了一個FragmentNavigator,但是落實到專案中的時候,發現一個重疊的問題。 Android進階寶典 -- JetPack Navigation的高階用法(解決路由跳轉新建Fragment頁面問題)

```kotlin val ft = fragmentManager.beginTransaction() val currentFragment = fragmentManager.primaryNavigationFragment KLog.d(TAG,"currentFragment $currentFragment") //將當前Fragment隱藏 if (currentFragment != null) { ft.hide(currentFragment) } //獲取目的地Fragment val destinationId = destination.id.toString() var nextFragment = fragmentManager.findFragmentByTag(destinationId)

if (nextFragment != null) { ft.show(nextFragment) } else { //說明當前Fragment沒有被建立過 nextFragment = fragmentManager.fragmentFactory.instantiate(context.classLoader, className) nextFragment.arguments = args ft.add(containerId, nextFragment, destinationId) } ``` 因為官方提交事務都是commit,所以自定義Navigator的時候,事務提交也是採用的commit;所以從1.2小節中我們對於commit的原理的認知,這是一個非同步的過程,看下圖:

image.png

首先Fragment1為起點,在載入路由表的時候,先將Fragment add到NavHostFragment中;因為commit是一個非同步的過程可能有延遲,此時呼叫navigate從Fragment1跳轉到Fragment2,按照上面程式碼中的邏輯,首先會呼叫primaryNavigationFragment獲取當前頁面例項(Fragment1),此時因為commit延遲導致沒有獲取到,此時currentFragment = null,隱藏失敗!

那麼當載入完成Fragment2的時候,Fragment1也載入完成,此時兩個頁面就發生了重疊;所以這種情況下,就需要考慮使用commitNow做同步處理,但是需要注意使用commitNow就不允許加入回退棧,這裡還需要考慮使用反射將標誌位取反

目前我在業務場景中做的處理是在Fragment1呼叫onAttach的時候,此時Fragment已經建立,然後呼叫navigate進行跳轉。

當然還有一種Fragment重疊的問題,就是多次呼叫add往同一個containerId中新增,此時Fragment就會全部重疊到一起,但是這種情況下,不會影響任意一個Fragment的生命週期,即便是下面的Fragment已經不可見了;而replace則是會將當前容器中Fragment銷燬,然後再新增新的Fragment,這就是add和replace的區別。

這一篇文章原理性的東西比較多,但是使用上很少介紹,相信大部分的夥伴們應該都瞭解使用方式,但是在使用的時候可能都會遇到一些問題,如果熟悉了其中的原理,定位問題也會比較迅速,而且在使用api的時候也會更加謹慎。