上篇文章我們簡單的介紹了Navigation元件的使用,以及深入分析了原始碼中的具體實現,基本原理我們已經很清晰了。本篇文章主要介紹下我在專案中遇到的問題,以及目前關於Navigation實現的一些探討。還沒有看過上篇文章的可以檢視一下:
Jetpack元件之Navigation---看完你就知道Navigation是什麼了?
1. 背景
先來看一下Navigation
元件在官方文件上的介紹:
今天,我們宣佈推出Navigation元件,作為構建您的應用內介面的框架,重點是讓單 Activity 應用成為首選架構。利用Navigation元件對 Fragment 的原生支援,您可以獲得架構元件的所有好處(例如生命週期和 ViewModel),同時讓此元件為您處理 FragmentTransaction 的複雜性。此外,Navigation元件還可以讓您宣告我們為您處理的轉場。它可以自動構建正確的“向上”和“返回”行為,包含對深層連結的完整支援,並提供了幫助程式,用於將導航關聯到合適的 UI 小部件,例如抽屜式導航欄和底部導航。
確實經過原始碼分析我們就可以發現,Navigation
元件封裝了Menu
選單欄、Fragment
的切換、NavigationView
、Drawerlayout
等一系列涉及到的元件,為了更方便的讓我們使用單Activity多Fragment的架構。
但是我在使用的時候發現,當一個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在顯示,減少了介面的層級關係。
不僅僅是這樣,上篇文章有小夥伴問切換了
Fragment
之後,點選返回按鈕,發現之前的Fragment
重走了onCreateView
流程,這就意味著之前的狀態沒了。對於這個問題其實根據上面的分析,也能大概想到是因為什麼,但是返回按鈕的操作我之前還真沒有看過原始碼,所以這次順便了解一下:
3. 返回都做了什麼
3.1 onBackPressed
我們同樣從首頁的onBackPressed
入手:
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
複製程式碼
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}
複製程式碼
最終呼叫了mOnBackPressedDispatcher
的onBackPressed()
方法。我們檢視這個類,通過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();
}
}
}
複製程式碼
我們可以看到通過遍歷棧陣列,對record
做executePopOps()
操作,通過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元件中的ViewModel、LiveData.....等,可以發現SunFlowerdemo中,即便是切換Fragmengt也不會有很明顯的卡頓現象,因為每個Fragment即便重新繪製,但是View所對應的ViewModel還在,資料並不需要重新載入或者請求,當然這僅僅是我自己的看法啊.
但是如果你沒有這種場景的話,建議還是用普通的方式我們自己來控制切換吧,這樣無論是基於Drawerlayout
還是BottomNaivgationView
的話,我們可以自己實現切換。這塊我也不是很確定哈,也希望聽取大家的意見和建議。
我還發現一個問題,就是Play商店,現在就是這樣的情況,抽屜欄中的
Item
每個基本都是重新繪製,而且第一個Item
我的應用和遊戲切換的時候就會有很明顯的卡頓和閃屏,猜測Google play 商店具體是不是使用的Navigation
元件不敢確定,但是它很大機率是通過replace
方式來做的切換。感興趣的話可以看一下,我這貼一個GIF圖,不一定能看清楚,不過確實是這個效果。
最後,如果有不對的地方或者更好的解決辦法,可以一起討論一下哈!