Android中ViewPager你所不知道的優(yōu)化技巧分享
寫(xiě)在前面
提到ViewPager想必各位同學(xué)一點(diǎn)都不陌生,它是Android中最常用的組件之一,一般配合Fragment一起使用。網(wǎng)上關(guān)于它的基本使用和常規(guī)優(yōu)化方式也有很多,在這里我就不一一贅述,而是直接進(jìn)入這篇文章的主題--ViewPager一些新的優(yōu)化方式。
我獲得這項(xiàng)技能的背景
最近組里做新的Web容器的,一次承載多個(gè)H5頁(yè)面,以實(shí)現(xiàn)左右切換,默認(rèn)展示主會(huì)場(chǎng)頁(yè),并要達(dá)到提升打開(kāi)率的目標(biāo)。要達(dá)到這個(gè)目標(biāo),那勢(shì)必要從加載優(yōu)化入手,縮短頁(yè)面的打開(kāi)時(shí)間。 優(yōu)化的點(diǎn)包括但不限于,Activity初始化、ViewPager和Fragment的初始化、WebView的初始化等等。我做的第一個(gè)優(yōu)化點(diǎn)便是ViewPager相關(guān)。
解決ViewPager默認(rèn)加載多個(gè)Fragment的問(wèn)題
ViewPager會(huì)默認(rèn)給我們緩存多個(gè)Fragment,這樣設(shè)計(jì)的目的是為了提升左右滑動(dòng)的流暢度,代價(jià)就是會(huì)降低首次打開(kāi)的啟動(dòng)時(shí)間。這讓一個(gè)以打開(kāi)率為KPI的我來(lái)說(shuō)是不能容忍的!首先想到的解決方案便是懶加載,當(dāng)Fragment頁(yè)面可見(jiàn)時(shí),才從網(wǎng)絡(luò)加載數(shù)據(jù)并顯示出來(lái)。這樣做還是不能解決其它Fragment被緩存,以導(dǎo)致占用啟動(dòng)時(shí)間的問(wèn)題,那怎么辦?既然ViewPager不給我們只加載一個(gè)Fragment的機(jī)會(huì),那我們強(qiáng)行創(chuàng)造行不行。我首次只往Adapter塞一個(gè)Fragment,等加載完成后再調(diào)用notifyDataSetChanged方法更新其它頁(yè)面行不行!
解決重復(fù)刷新的問(wèn)題
FragmentPagerAdapter不會(huì)銷(xiāo)毀已經(jīng)初始化完畢的Fragment
那為什么會(huì)有重復(fù)刷新的問(wèn)題?且聽(tīng)我慢慢道來(lái)
我們的主會(huì)場(chǎng)在ViewPager中的位置是由后端下發(fā)的。首次加載單個(gè)Fragment,主會(huì)場(chǎng)在ViewPager中的位置只能是0,后續(xù)更新時(shí)根據(jù)后端下發(fā)的position動(dòng)態(tài)調(diào)整其所在的位置。
//調(diào)整主會(huì)場(chǎng)位置偽代碼 marketingInfoList.add(new MarketingInfo("www.juejin.com", "主會(huì)場(chǎng)")) for (int i = 0; i <= 3; i++) { //將放在前兩個(gè)主會(huì)場(chǎng)前面 if (i < 2) { marketingInfoList.add(i, new MarketingInfo("www.baidu.com", "模擬" + i)); } else { //后兩個(gè)往主會(huì)場(chǎng)后面添加 marketingInfoList.add(new MarketingInfo("www.baidu.com", "模擬" + i)); } } mPagerAdapter.notifyDataSetChanged(); //重新設(shè)置選中主會(huì)場(chǎng) mViewPager.setCurrentItem(2);
可在實(shí)際開(kāi)發(fā)的過(guò)程中卻發(fā)現(xiàn),主會(huì)場(chǎng)重復(fù)加載了兩次,ViewPager生成了一個(gè)新的Fragment去承載主會(huì)場(chǎng)。我們的用戶(hù)元?dú)鉂M滿的點(diǎn)開(kāi)我們的營(yíng)銷(xiāo)頁(yè),正準(zhǔn)備下單呢,頁(yè)面突然又重新白屏了一下。留下一句****,憤然離去。作為一名要給公司帶來(lái)增長(zhǎng)價(jià)值的開(kāi)發(fā)這是不能接受的!那怎么辦呢?分析源碼!
ViewPager源碼解析
instantiateItem方法作用
ViewPager會(huì)通過(guò)這個(gè)方法將構(gòu)造Fragment,F(xiàn)ragmentManager和Transaction都在這個(gè)方法里出現(xiàn)
public Object instantiateItem(ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); //跟據(jù)itemId生成fragment名字,通過(guò)名字去查找fragment是否加載過(guò) String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); //fragment加載過(guò)則直接attach,否則的話新生成一個(gè)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會(huì)通過(guò)getItemId獲取到itemId,再生成與fragment對(duì)應(yīng)的唯一tag,通過(guò)tag查找fragment是否加載過(guò)。也就是說(shuō)只要tag相同,無(wú)論你點(diǎn)擊的是哪個(gè)Tab都會(huì)加載到同一個(gè)fragment。我們?cè)俳又榭瓷蓆ag的方法makeFragmentName。
private static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; }
原來(lái)Tag就是由instantiateItem傳入的viewId和itemId兩個(gè)值組成,那么我們?cè)倏纯磇temId的生成方式
public long getItemId(int position) { return position; }
我驚了!更加的簡(jiǎn)單!也就是說(shuō)Fragment的唯一Tag是又position決定的。這下剛剛的問(wèn)題有答案了吧。
重復(fù)刷新的真相與解決
ViewPager在初始化Fragment時(shí),會(huì)根據(jù)Tag尋找Fragment,有則直接加載,無(wú)則重新生成。主會(huì)場(chǎng)首次加載的position是0,后續(xù)調(diào)整位置后變成了2,導(dǎo)致兩次的Tag不一至,所以就出現(xiàn)了重復(fù)加載的問(wèn)題。知道了問(wèn)題產(chǎn)生的原因,再來(lái)想解決辦法就好辦了。我們可以重寫(xiě)getItem方法,重新定義itemId的生成方式。
public long getItemId(int position) { //可以直接使用后端給頁(yè)面ID return pageId; //后端不給也沒(méi)事,我們自己生成一個(gè) return data.get(position).getTitle().hashCode(); }
延伸: #getItemPosition方法
如果不重寫(xiě)getItemId這方法,將頁(yè)面位置調(diào)整后再跳切回舊的位置,還會(huì)面臨就位置的頁(yè)面不刷新的問(wèn)題。舉個(gè)栗子:
掘金的position是0,我將它的position改為2,第0個(gè)position這個(gè)時(shí)候設(shè)置為百度,會(huì)發(fā)現(xiàn)首個(gè)頁(yè)面依然是掘金。
網(wǎng)上給出的答案是重寫(xiě)getItemPosition方法,雖然可以解決問(wèn)題,但是沒(méi)有一個(gè)能講明白這個(gè)方法的作用,在這里我來(lái)補(bǔ)充一下
public int getItemPosition(Object object) { return POSITION_UNCHANGED; }
getItemPosition默認(rèn)返回POSITION_UNCHANGED,表示頁(yè)面無(wú)變化。還有另外一個(gè)默認(rèn)值POSITION_NONE,表示頁(yè)面不存在。
???
頁(yè)面指的是哪個(gè)頁(yè)面?調(diào)用時(shí)機(jī)又是什么?還能再返回其它值嗎? 各位看官先別急且看我慢慢寫(xiě)來(lái),寫(xiě)帖一段源碼:
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項(xiàng)所處的位置 final int newPos = mAdapter.getItemPosition(ii.object); //返回的位置等于POSITION_UNCHANGED(-1)表示當(dāng)前頁(yè)未有變更,不做任何操作 if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue; } //如果返回的位置等于POSITION_NONE(-2)表示當(dāng)前頁(yè)Tab項(xiàng)刷新后不存在,需要銷(xiāo)毀并重新加載新的頁(yè)面 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; } //如果當(dāng)前頁(yè)的新的位置和和舊位置不等,則說(shuō)明調(diào)整了順序 if (ii.position != newPos) { //這段代碼是將頁(yè)面定位到刷新之前的打開(kāi)頁(yè),據(jù)數(shù)據(jù)的position和mCurItem相等的話,則表示這個(gè)item是之前打開(kāi)的,賦予它新位置的值 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方法之后會(huì)調(diào)用dataSetChanged方法,getItemPosition又是在dataSetChanged方法被調(diào)用的。
調(diào)用notifyDataSetChanged的后,會(huì)遍歷舊的頁(yè)面,通過(guò)getItemPosition方法返回的位置去決定當(dāng)前遍歷到的頁(yè)面是否需要更新。POSITION_UNCHANGED:表示頁(yè)面無(wú)變化;POSITION_NONE:表示頁(yè)面不存在,需要銷(xiāo)毀,重新加載新的頁(yè)面。如果返回值返回的是頁(yè)面具體的位置,則更新當(dāng)前頁(yè)在刷新數(shù)據(jù)后的位置,將Tab欄選中的對(duì)應(yīng)的Tab項(xiàng)選中。
以上就是Android中ViewPager你所不知道的優(yōu)化技巧分享的詳細(xì)內(nèi)容,更多關(guān)于Android ViewPager優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 檢測(cè)鍵盤(pán)顯示或隱藏鍵盤(pán)的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 檢測(cè)鍵盤(pán)顯示或隱藏鍵盤(pán)的實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-07-07Android列表控件Spinner簡(jiǎn)單用法示例
這篇文章主要介紹了Android列表控件Spinner簡(jiǎn)單用法,結(jié)合實(shí)例形式分析了Android列表控件Spinner的布局與功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-12-12ListView實(shí)現(xiàn)下拉動(dòng)態(tài)渲染數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了ListView實(shí)現(xiàn)下拉動(dòng)態(tài)渲染數(shù)據(jù)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06android重力感應(yīng)開(kāi)發(fā)之微信搖一搖功能
這篇文章主要為大家詳細(xì)介紹了android重力感應(yīng)開(kāi)發(fā)之微信搖一搖功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Jetpack Compose之選擇器使用實(shí)例講解
這篇文章主要介紹了Jetpack Compose之選擇器使用,選擇器主要是指Checkbox復(fù)選框,單選開(kāi)關(guān)Switch,滑桿組件Slider等用于提供給用戶(hù)選擇一些值和程序交互的組件,比如像復(fù)選框Checkbox,可以讓用戶(hù)選擇一個(gè)或者多個(gè)選項(xiàng)2023-04-04Android 取消藍(lán)牙配對(duì)框?qū)崿F(xiàn)自動(dòng)配對(duì)功能
這篇文章主要介紹了Android 取消藍(lán)牙配對(duì)框?qū)崿F(xiàn)自動(dòng)配對(duì)功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02Android報(bào)錯(cuò)Error:Could not find com.android.tools.build:gradle
這篇文章主要介紹了Android Studio報(bào)錯(cuò)Error:Could not find com.android.tools.build:gradle:4.1解決辦法,碰到該問(wèn)題的同學(xué)快過(guò)來(lái)看看吧2021-08-08Android開(kāi)發(fā)中amera2 Preview使用詳解
這篇文章主要介紹了Android開(kāi)發(fā)中amera2 Preview使用詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09android 拷貝sqlite數(shù)據(jù)庫(kù)到本地sd卡的方法
下面小編就為大家?guī)?lái)一篇android 拷貝sqlite數(shù)據(jù)庫(kù)到本地sd卡的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03