Android中ViewPager你所不知道的優(yōu)化技巧分享
寫在前面
提到ViewPager想必各位同學一點都不陌生,它是Android中最常用的組件之一,一般配合Fragment一起使用。網(wǎng)上關于它的基本使用和常規(guī)優(yōu)化方式也有很多,在這里我就不一一贅述,而是直接進入這篇文章的主題--ViewPager一些新的優(yōu)化方式。
我獲得這項技能的背景
最近組里做新的Web容器的,一次承載多個H5頁面,以實現(xiàn)左右切換,默認展示主會場頁,并要達到提升打開率的目標。要達到這個目標,那勢必要從加載優(yōu)化入手,縮短頁面的打開時間。 優(yōu)化的點包括但不限于,Activity初始化、ViewPager和Fragment的初始化、WebView的初始化等等。我做的第一個優(yōu)化點便是ViewPager相關。
解決ViewPager默認加載多個Fragment的問題
ViewPager會默認給我們緩存多個Fragment,這樣設計的目的是為了提升左右滑動的流暢度,代價就是會降低首次打開的啟動時間。這讓一個以打開率為KPI的我來說是不能容忍的!首先想到的解決方案便是懶加載,當Fragment頁面可見時,才從網(wǎng)絡加載數(shù)據(jù)并顯示出來。這樣做還是不能解決其它Fragment被緩存,以導致占用啟動時間的問題,那怎么辦?既然ViewPager不給我們只加載一個Fragment的機會,那我們強行創(chuàng)造行不行。我首次只往Adapter塞一個Fragment,等加載完成后再調用notifyDataSetChanged方法更新其它頁面行不行!
解決重復刷新的問題
FragmentPagerAdapter不會銷毀已經(jīng)初始化完畢的Fragment
那為什么會有重復刷新的問題?且聽我慢慢道來
我們的主會場在ViewPager中的位置是由后端下發(fā)的。首次加載單個Fragment,主會場在ViewPager中的位置只能是0,后續(xù)更新時根據(jù)后端下發(fā)的position動態(tài)調整其所在的位置。
//調整主會場位置偽代碼 marketingInfoList.add(new MarketingInfo("www.juejin.com", "主會場")) for (int i = 0; i <= 3; i++) { //將放在前兩個主會場前面 if (i < 2) { marketingInfoList.add(i, new MarketingInfo("www.baidu.com", "模擬" + i)); } else { //后兩個往主會場后面添加 marketingInfoList.add(new MarketingInfo("www.baidu.com", "模擬" + i)); } } mPagerAdapter.notifyDataSetChanged(); //重新設置選中主會場 mViewPager.setCurrentItem(2);
可在實際開發(fā)的過程中卻發(fā)現(xiàn),主會場重復加載了兩次,ViewPager生成了一個新的Fragment去承載主會場。我們的用戶元氣滿滿的點開我們的營銷頁,正準備下單呢,頁面突然又重新白屏了一下。留下一句****,憤然離去。作為一名要給公司帶來增長價值的開發(fā)這是不能接受的!那怎么辦呢?分析源碼!
ViewPager源碼解析
instantiateItem方法作用
ViewPager會通過這個方法將構造Fragment,F(xiàn)ragmentManager和Transaction都在這個方法里出現(xiàn)
public Object instantiateItem(ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); //跟據(jù)itemId生成fragment名字,通過名字去查找fragment是否加載過 String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); //fragment加載過則直接attach,否則的話新生成一個fragment if (fragment != null) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); } return fragment; }
instantiateItem會通過getItemId獲取到itemId,再生成與fragment對應的唯一tag,通過tag查找fragment是否加載過。也就是說只要tag相同,無論你點擊的是哪個Tab都會加載到同一個fragment。我們再接著查看生成tag的方法makeFragmentName。
private static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; }
原來Tag就是由instantiateItem傳入的viewId和itemId兩個值組成,那么我們再看看itemId的生成方式
public long getItemId(int position) { return position; }
我驚了!更加的簡單!也就是說Fragment的唯一Tag是又position決定的。這下剛剛的問題有答案了吧。
重復刷新的真相與解決
ViewPager在初始化Fragment時,會根據(jù)Tag尋找Fragment,有則直接加載,無則重新生成。主會場首次加載的position是0,后續(xù)調整位置后變成了2,導致兩次的Tag不一至,所以就出現(xiàn)了重復加載的問題。知道了問題產(chǎn)生的原因,再來想解決辦法就好辦了。我們可以重寫getItem方法,重新定義itemId的生成方式。
public long getItemId(int position) { //可以直接使用后端給頁面ID return pageId; //后端不給也沒事,我們自己生成一個 return data.get(position).getTitle().hashCode(); }
延伸: #getItemPosition方法
如果不重寫getItemId這方法,將頁面位置調整后再跳切回舊的位置,還會面臨就位置的頁面不刷新的問題。舉個栗子:
掘金的position是0,我將它的position改為2,第0個position這個時候設置為百度,會發(fā)現(xiàn)首個頁面依然是掘金。
網(wǎng)上給出的答案是重寫getItemPosition方法,雖然可以解決問題,但是沒有一個能講明白這個方法的作用,在這里我來補充一下
public int getItemPosition(Object object) { return POSITION_UNCHANGED; }
getItemPosition默認返回POSITION_UNCHANGED,表示頁面無變化。還有另外一個默認值POSITION_NONE,表示頁面不存在。
???
頁面指的是哪個頁面?調用時機又是什么?還能再返回其它值嗎? 各位看官先別急且看我慢慢寫來,寫帖一段源碼:
void dataSetChanged() { // This method only gets called if our observer is attached, so mAdapter is non-null. final int adapterCount = mAdapter.getCount(); mExpectedAdapterCount = adapterCount; boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && mItems.size() < adapterCount; int newCurrItem = mCurItem; boolean isUpdating = false; //mItems為舊數(shù)據(jù)的容器 for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); //返回刷新之前Tab項所處的位置 final int newPos = mAdapter.getItemPosition(ii.object); //返回的位置等于POSITION_UNCHANGED(-1)表示當前頁未有變更,不做任何操作 if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue; } //如果返回的位置等于POSITION_NONE(-2)表示當前頁Tab項刷新后不存在,需要銷毀并重新加載新的頁面 if (newPos == PagerAdapter.POSITION_NONE) { mItems.remove(i); i--; if (!isUpdating) { mAdapter.startUpdate(this); isUpdating = true; } mAdapter.destroyItem(this, ii.position, ii.object); needPopulate = true; if (mCurItem == ii.position) { // Keep the current item in the valid range newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); needPopulate = true; } continue; } //如果當前頁的新的位置和和舊位置不等,則說明調整了順序 if (ii.position != newPos) { //這段代碼是將頁面定位到刷新之前的打開頁,據(jù)數(shù)據(jù)的position和mCurItem相等的話,則表示這個item是之前打開的,賦予它新位置的值 if (ii.position == mCurItem) { // Our current item changed position. Follow it. newCurrItem = newPos; } ii.position = newPos; needPopulate = true; } } if (isUpdating) { mAdapter.finishUpdate(this); } Collections.sort(mItems, COMPARATOR); if (needPopulate) { // Reset our known page widths; populate will recompute them. final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) { lp.widthFactor = 0.f; } } setCurrentItemInternal(newCurrItem, false, true); requestLayout(); } }
notifyDataSetChanged方法之后會調用dataSetChanged方法,getItemPosition又是在dataSetChanged方法被調用的。
調用notifyDataSetChanged的后,會遍歷舊的頁面,通過getItemPosition方法返回的位置去決定當前遍歷到的頁面是否需要更新。POSITION_UNCHANGED:表示頁面無變化;POSITION_NONE:表示頁面不存在,需要銷毀,重新加載新的頁面。如果返回值返回的是頁面具體的位置,則更新當前頁在刷新數(shù)據(jù)后的位置,將Tab欄選中的對應的Tab項選中。
以上就是Android中ViewPager你所不知道的優(yōu)化技巧分享的詳細內容,更多關于Android ViewPager優(yōu)化的資料請關注腳本之家其它相關文章!
相關文章
Android 檢測鍵盤顯示或隱藏鍵盤的實現(xiàn)代碼
這篇文章主要介紹了Android 檢測鍵盤顯示或隱藏鍵盤的實現(xiàn)代碼的相關資料,需要的朋友可以參考下2017-07-07ListView實現(xiàn)下拉動態(tài)渲染數(shù)據(jù)
這篇文章主要為大家詳細介紹了ListView實現(xiàn)下拉動態(tài)渲染數(shù)據(jù)的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06Android報錯Error:Could not find com.android.tools.build:gradle
這篇文章主要介紹了Android Studio報錯Error:Could not find com.android.tools.build:gradle:4.1解決辦法,碰到該問題的同學快過來看看吧2021-08-08Android開發(fā)中amera2 Preview使用詳解
這篇文章主要介紹了Android開發(fā)中amera2 Preview使用詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09android 拷貝sqlite數(shù)據(jù)庫到本地sd卡的方法
下面小編就為大家?guī)硪黄猘ndroid 拷貝sqlite數(shù)據(jù)庫到本地sd卡的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03