2. Jetpack源碼解析---Navigation爲何切換Fragment會重繪?

上篇文章咱們簡單的介紹了Navigation組件的使用,以及深刻分析了源碼中的具體實現,基本原理咱們已經很清晰了。本篇文章主要介紹下我在項目中遇到的問題,以及目前關於Navigation實現的一些探討。尚未看過上篇文章的能夠查看一下:java

Jetpack組件之Navigation---看完你就知道Navigation是什麼了?node

1. 背景

先來看一下Navigation組件在官方文檔上的介紹:android

今天,咱們宣佈推出Navigation組件,做爲構建您的應用內界面的框架,重點是讓單 Activity 應用成爲首選架構。利用Navigation組件對 Fragment 的原生支持,您能夠得到架構組件的全部好處(例如生命週期和 ViewModel),同時讓此組件爲您處理 FragmentTransaction 的複雜性。此外,Navigation組件還可讓您聲明咱們爲您處理的轉場。它能夠自動構建正確的「向上」和「返回」行爲,包含對深層連接的完整支持,並提供了幫助程序,用於將導航關聯到合適的 UI 小部件,例如抽屜式導航欄和底部導航。git

確實通過源碼分析咱們就能夠發現,Navigation組件封裝了Menu菜單欄、Fragment的切換、NavigationViewDrawerlayout等一系列涉及到的組件,爲了更方便的讓咱們使用單Activity多Fragment的架構。github

可是我在使用的時候發現,當一個Fragment中的佈局稍微複雜一些,切換Fragment的時候會頓卡,並且若是再配合DrawrLayout使用的話,還會閃一下屏,效果體驗不是很好,本着這個問題,我又再次對Navigation組件進行了分析。數組

2.Fragment切換

經過現象分析,發現當切換NavigationView中的menu菜單來切換Fragment時,DrawerLayout抽屜關閉有一個短暫的動畫(具體的這裏就不分析了,感興趣的能夠自行查看,可是這不是根本緣由),同時Fragment切換,發生頓卡和閃屏的現象。因此....仍是看源碼吧:架構

2.1 NavController

private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        ....
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
       ....
    }

複製代碼

2.2 FragmentNavigator

public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        ...
        //根據classname反射獲取Fragmnent
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        //獲取Fragment事務
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        //切換動畫設置
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
        //切換Fragment
        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);
        ......
        
        ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
        ........
    }
複製代碼

看到這裏就很清楚了吧,Fragment的切換是經過replace方式來切換的,而且加入回退棧,也就是說每次切換Fragment,都會銷燬視圖和從新建立視圖。至於爲何用這種方式我是真的想不到,也沒搞清楚初衷是什麼?按照咱們目前的開發來講,Fragment的切換一般都會使用hide()show(),而replcae()的方式不多用,替換會把容器中的全部內容全都替換掉,有一些app會使用這樣的作法,保持只有一個fragment在顯示,減小了界面的層級關係。app

不只僅是這樣,上篇文章有小夥伴問切換了Fragment以後,點擊返回按鈕,發現以前的Fragment重走了onCreateView流程,這就意味着以前的狀態沒了。對於這個問題其實根據上面的分析,也能大概想到是由於什麼,可是返回按鈕的操做我以前還真沒有看過源碼,因此此次順便了解一下:框架

3. 返回都作了什麼

3.1 onBackPressed

咱們一樣從首頁的onBackPressed入手:ide

override fun onBackPressed() {
        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.closeDrawer(GravityCompat.START)
        } else {
            super.onBackPressed()
        }
    }
複製代碼
public void onBackPressed() {
        mOnBackPressedDispatcher.onBackPressed();
    }
複製代碼

最終調用了mOnBackPressedDispatcheronBackPressed()方法。咱們查看這個類,經過Debug調試,咱們跟到了FragmentManagerImpl類:

private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            FragmentManagerImpl.this.handleOnBackPressed();
        }
    };
複製代碼

發現點擊返回按鈕以後就走到這個,執行handleOnBackPressed()方法。

3.2 FragmentManagerImpl

繼續跟蹤源碼,中間的一些過程我這裏就忽略掉了,大部分都是一些popBackStack的操做,這裏咱們直接跟蹤到關鍵點:

//在BackStackRecords中進行入棧出棧操做。
private static void executeOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
        for (int i = startIndex; i < endIndex; i++) {
            final BackStackRecord record = records.get(i);
            final boolean isPop = isRecordPop.get(i);
            if (isPop) {
                record.bumpBackStackNesting(-1);
                // Only execute the add operations at the end of
                // all transactions.
                boolean moveToState = i == (endIndex - 1);
                record.executePopOps(moveToState);
            } else {
                record.bumpBackStackNesting(1);
                record.executeOps();
            }
        }
    }
複製代碼

咱們能夠看到經過遍歷棧數組,對recordexecutePopOps()操做,經過cmd來讓FragmentManager作相關操做。

void executePopOps(boolean moveToState) {
        for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
            final Op op = mOps.get(opNum);
            Fragment f = op.mFragment;
            if (f != null) {
                f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition),
                        mTransitionStyle);
            }
            switch (op.mCmd) {
                case OP_ADD:
                    f.setNextAnim(op.mPopExitAnim);
                    mManager.removeFragment(f);
                    break;
                case OP_REMOVE:
                    f.setNextAnim(op.mPopEnterAnim);
                    mManager.addFragment(f, false);
                    break;
                case OP_HIDE:
                    f.setNextAnim(op.mPopEnterAnim);
                    mManager.showFragment(f);
                    break;
                case OP_SHOW:
                    f.setNextAnim(op.mPopExitAnim);
                    mManager.hideFragment(f);
                    break;
                case OP_DETACH:
                    f.setNextAnim(op.mPopEnterAnim);
                    mManager.attachFragment(f);
                    break;
                case OP_ATTACH:
                    f.setNextAnim(op.mPopExitAnim);
                    mManager.detachFragment(f);
                    break;
                case OP_SET_PRIMARY_NAV:
                    mManager.setPrimaryNavigationFragment(null);
                    break;
                case OP_UNSET_PRIMARY_NAV:
                    mManager.setPrimaryNavigationFragment(f);
                    break;
                case OP_SET_MAX_LIFECYCLE:
                    mManager.setMaxLifecycle(f, op.mOldMaxState);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown cmd: " + op.mCmd);
            }
            if (!mReorderingAllowed && op.mCmd != OP_REMOVE && f != null) {
                mManager.moveFragmentToExpectedState(f);
            }
        }
        if (!mReorderingAllowed && moveToState) {
            mManager.moveToState(mManager.mCurState, true);
        }
    }
複製代碼

同時從新設置PrimaryNavigationFragment,add咱們的首頁Fragment,最後執行moveToState方法:

public void addFragment(Fragment fragment, boolean moveToStateNow) {
        if (DEBUG) Log.v(TAG, "add: " + fragment);
        makeActive(fragment);
        if (!fragment.mDetached) {
            if (mAdded.contains(fragment)) {
                throw new IllegalStateException("Fragment already added: " + fragment);
            }
            synchronized (mAdded) {
                mAdded.add(fragment);
            }
            fragment.mAdded = true;
            fragment.mRemoving = false;
            if (fragment.mView == null) {
                fragment.mHiddenChanged = false;
            }
            if (isMenuAvailable(fragment)) {
                mNeedMenuInvalidate = true;
            }
            if (moveToStateNow) {
                moveToState(fragment);
            }
        }
    }
複製代碼

當咱們繼續跟蹤的時候就會發現,在moveToState方法中,Fragment的state是Fragment.CREATED,而且會執行performCreateView()中的onCreateView()方法:

f.mContainer = container;
    f.performCreateView(f.performGetLayoutInflater(f.mSavedFragmentState), container, f.mSavedFragmentState);
複製代碼
void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mChildFragmentManager.noteStateNotSaved();
        mPerformedCreateView = true;
        mViewLifecycleOwner = new FragmentViewLifecycleOwner();
        mView = onCreateView(inflater, container, savedInstanceState);
        ....
    }
複製代碼

到這裏就基本結束了,我只分析了一個大概,能夠了解到點擊返回按鈕,一樣也會從新建立視圖,也就是onCreateView會從新走一遍。

4. 總結

對於Navigation組件的這種切換方式,我也很無奈,並且也並無暴露出來API供咱們使用其餘切換方式,我也詢問了不少大佬,他們也不是很清楚,也有的發現這也是Navigation的一個很大的詬病。那麼有沒有解決辦法呢?很遺憾我目前尚未想到比較好的辦法。

基於Navigation用來承載Fragment的容器是NavHostFragment,因此咱們並不能使用ViewPager+Fragment的經過setUserVisibleHint實現懶加載的方式;一樣咱們也沒辦法使用onHiddenChanged的方式來實現複雜邏輯的加載;可是你能夠在進入Fragment的時候先顯示一個Loading框,加載完數據以後再渲染布局,這樣的話能夠減小一些尷尬。

4.1 建議

這裏個人建議是:若是你的每一個Fragment真的每次都須要從新繪製的話,你能夠考慮使用Navigation組件來實現,畢竟經過Navgation組件真的很方便幫助咱們切換導航,並且雖然佈局會從新繪製,可是Google的官方Demo--SunFlower仍是使用了這種方式,因此這裏面我以爲:官方推薦咱們使用Jetpack組件中的ViewModelLiveData.....等,能夠發現SunFlowerdemo中,即使是切換Fragmengt也不會有很明顯的卡頓現象,由於每一個Fragment即使從新繪製,可是View所對應的ViewModel還在,數據並不須要從新加載或者請求,固然這僅僅是我本身的見解啊.

可是若是你沒有這種場景的話,建議仍是用普通的方式咱們本身來控制切換吧,這樣不管是基於Drawerlayout仍是BottomNaivgationView的話,咱們能夠本身實現切換。這塊我也不是很肯定哈,也但願聽取你們的意見和建議。

我還發現一個問題,就是Play商店,如今就是這樣的狀況,抽屜欄中的Item每一個基本都是從新繪製,並且第一個Item個人應用和遊戲切換的時候就會有很明顯的卡頓和閃屏,猜想Google play 商店具體是否是使用的Navigation組件不敢肯定,可是它很大概率是經過replace方式來作的切換。感興趣的話能夠看一下,我這貼一個GIF圖,不必定能看清楚,不過確實是這個效果。

最後,若是有不對的地方或者更好的解決辦法,能夠一塊兒討論一下哈!

相關文章
相關標籤/搜索