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

