RecyclerView性能優(yōu)化之異步預(yù)加載

前言
首先需要強調(diào)的是,這篇文章是對我之前寫的《淺談RecyclerView的性能優(yōu)化》文章的補充,建議大家先讀完這篇文章后再來看這篇文章,味道更佳。
當時由于篇幅的原因,并沒有深入展開講解,于是有很多感興趣的朋友紛紛留言表示:能不能結(jié)合相關(guān)的示例代碼講解一下到底如何實現(xiàn)?那么今天我就結(jié)合之前講的如何優(yōu)化onCreateViewHolder
的加載時間,講一講如何實現(xiàn)onCreateViewHolder
的異步預(yù)加載,文章末尾會給出示例代碼的鏈接地址,希望能給你帶來啟發(fā)。
分析
之前我們講過,在優(yōu)化onCreateViewHolder
方法的時候,可以降低item的布局層級,可以減少界面創(chuàng)建的渲染時間,其本質(zhì)就是降低view的inflate時間。因為onCreateViewHolder
最大的耗時部分,就是view的inflate。相信讀過LayoutInflater.inflate
源碼的人都知道,這部分的代碼是同步操作,并且涉及到大量的文件IO的操作以及鎖操作,通常來說這部分的代碼快的也需要幾毫秒,慢的可能需要幾十毫秒乃至上百毫秒也是很有可能的。 如果真到了每個ItemView的inflate需要花上上百毫秒的話,那么在大數(shù)據(jù)量的RecyclerView進行快速上下滑動的時候,就必然會導(dǎo)致界面的滑動卡頓、不流暢。
那么如何你的程序里真的有這樣一個列表,它的每個ItemView都需要花上上百毫秒的時間去inflate的話,你該怎么做?
首先就是對布局進行優(yōu)化,降低item的布局層級。但這點的優(yōu)化往往是微乎其微的。
其次可能就是想辦法讓設(shè)計師重新設(shè)計,將布局中的某些內(nèi)容刪除或者折疊了,對暫不展示的內(nèi)容使用ViewStub進行延遲加載。不過說實在話,你既然有能力讓設(shè)計師重新設(shè)計的話,還干個球的開發(fā)啊,直接當項目經(jīng)理不香嗎?
最后你可能會考慮不用xml寫布局,改為使用代碼自己一個一個new布局。話說回來了,一個使用xml加載的布局都要花上上百毫秒的布局,可能xml都快上千行下去了,你確定要自己一個一個new下去?
以上的方式,都是建立在列表布局可以修改的情況下,如果我們使用的列表布局是第三方已經(jīng)提供好的呢?(例如廣告SDK等)
那么有沒有什么辦法既可以不用修改當前的xml布局,又可以極大地縮短布局的加載時間呢?毫無疑問,布局異步加載將為你打開新的世界。
原理
Google官方很早就發(fā)現(xiàn)了XML布局加載的性能問題,于是在androidx中提供了異步加載工具AsyncLayoutInflater。其本質(zhì)就是開了一個長期等待的異步線程,在子線程中inflate view,然后把加載好的view通過接口拋出去,完成view的加載。
一般來說,對于復(fù)雜的列表,往往都對應(yīng)了復(fù)雜的數(shù)據(jù),而這復(fù)雜的數(shù)據(jù)往往又是通過服務(wù)器獲取而來。所以一般來說,一個列表在加載前,往往先需要訪問服務(wù)器獲取數(shù)據(jù),然后再刷新列表顯示,而這訪問服務(wù)器的時間大約也在300ms~1000ms之間。很多開發(fā)人員對這段時間往往沒有加以利用,只是加上一個loading動畫了事。
其實對于這一段事務(wù)真空的時間窗口,我們可以提前進行列表的ItemView的加載,這樣等數(shù)據(jù)請求下來刷新列表的時候,我們onCreateViewHolder
的時候就可以直接到已經(jīng)事先預(yù)加載好的View緩存池中直接獲取View傳到ViewHolder中使用,這樣onCreateViewHolder
的創(chuàng)建時間幾乎耗時為0,從而極大地提升了列表的加載和渲染速度。詳細的流程可以參見下圖:

實現(xiàn)
上面我簡單地講解了一下原理,下一步就是考慮如何實現(xiàn)這樣的效果了。
預(yù)加載緩存池
首先在預(yù)加載前,我們需要先創(chuàng)建一個緩存池來存儲預(yù)加載的View對象。
這里我選擇使用SparseArray
進行存儲,key是Int型,存放布局資源的layoutId,value是Object型,存放的是這類布局加載View的集合。
這里的集合類型我選擇的是LinkedList,因為我們的緩存需要頻繁的添加和刪除操作,并且LinkedList實現(xiàn)了Deque接口,具備先入先出的能力。
這里View的引用我選擇的是軟引用SoftReference,之所以不采用WeakReference, 目的就是希望緩存能多存在一段時間,避免內(nèi)存的頻繁釋放和回收造成內(nèi)存的抖動。
private?static?class?ViewCache?{
????private?final?SparseArray<LinkedList<SoftReference<View>>>?mViewPools?=?new?SparseArray<>();
????
????public?LinkedList<SoftReference<View>>?getViewPool(int?layoutId)?{
????????LinkedList<SoftReference<View>>?views?=?mViewPools.get(layoutId);
????????if?(views?==?null)?{
????????????views?=?new?LinkedList<>();
????????????mViewPools.put(layoutId,?views);
????????}
????????return?views;
????}
????public?int?getViewPoolAvailableCount(int?layoutId)?{
????????LinkedList<SoftReference<View>>?views?=?getViewPool(layoutId);
????????Iterator<SoftReference<View>>?it?=?views.iterator();
????????int?count?=?0;
????????while?(it.hasNext())?{
????????????if?(it.next().get()?!=?null)?{
????????????????count++;
????????????}?else?{
????????????????it.remove();
????????????}
????????}
????????return?count;
????}
????public?void?putView(int?layoutId,?View?view)?{
????????if?(view?==?null)?{
????????????return;
????????}
????????getViewPool(layoutId).offer(new?SoftReference<>(view));
????}
????
????public?View?getView(int?layoutId)?{
????????return?getViewFromPool(getViewPool(layoutId));
????}
????private?View?getViewFromPool(@NonNull?LinkedList<SoftReference<View>>?views)?{
????????if?(views.isEmpty())?{
????????????return?null;
????????}
????????View?target?=?views.pop().get();
????????if?(target?==?null)?{
????????????return?getViewFromPool(views);
????????}
????????return?target;
????}
}
從getViewFromPool
方法我們可以看出,這里對于ViewCache來說,每次取出一個緩存View使用的是pop
方法,我們都會將它從Pool中移除。
布局加載者
因為view的加載方法,涉及到三個參數(shù): 資源Id-resourceId, 父布局-root和是否添加到根布局-attachToRoot。
public?View?inflate(int?resource,?ViewGroup?root,?boolean?attachToRoot)?{
????
}
這里在onCreateViewHolder
方法中attachToRoot恒為false,因此異步布局加載只需要前面兩個參數(shù)以及一個回調(diào)接口即可,即如下的定義:
public?interface?ILayoutInflater?{
????/**
?????*?異步加載View
?????*
?????*?@param?parent???父布局
?????*?@param?layoutId?布局資源id
?????*?@param?callback?加載回調(diào)
?????*/
????void?asyncInflateView(@NonNull?ViewGroup?parent,?int?layoutId,?InflateCallback?callback);
????/**
?????*?同步加載View
?????*
?????*?@param?parent???父布局
?????*?@param?layoutId?布局資源id
?????*?@return?加載的View
?????*/
????View?inflateView(@NonNull?ViewGroup?parent,?int?layoutId);
}
public?interface?InflateCallback?{
????void?onInflateFinished(int?layoutId,?View?view);
}
至于接口實現(xiàn)的話,就直接使用Google官方提供的異步加載工具AsyncLayoutInflater來實現(xiàn)。
public?class?DefaultLayoutInflater?implements?PreInflateHelper.ILayoutInflater?{
????private?AsyncLayoutInflater?mInflater;
????private?DefaultLayoutInflater()?{}
????private?static?final?class?InstanceHolder?{
????????static?final?DefaultLayoutInflater?sInstance?=?new?DefaultLayoutInflater();
????}
????public?static?DefaultLayoutInflater?get()?{
????????return?InstanceHolder.sInstance;
????}
????
????public?void?asyncInflateView(@NonNull?ViewGroup?parent,?int?layoutId,?PreInflateHelper.InflateCallback?callback)?{
????????if?(mInflater?==?null)?{
????????????Context?context?=?parent.getContext();
????????????mInflater?=?new?AsyncLayoutInflater(new?ContextThemeWrapper(context.getApplicationContext(),?context.getTheme()));
????????}
????????mInflater.inflate(layoutId,?parent,?(view,?resId,?parent1)?->?{
????????????if?(callback?!=?null)?{
????????????????callback.onInflateFinished(resId,?view);
????????????}
????????});
????}
????
????public?View?inflateView(@NonNull?ViewGroup?parent,?int?layoutId)?{
????????return?InflateUtils.getInflateView(parent,?layoutId);
????}
}
預(yù)加載輔助類
有了預(yù)加載緩存池ViewCache和異步加載能力的提供者IAsyncInflater,下面就是來協(xié)調(diào)這兩者進行合作,完成布局的預(yù)加載和View的讀取。
首先需要定義的是根據(jù)ViewGroup和layoutId獲取View的方法,提供給Adapter的onCreateViewHolder
方法使用。
首先我們需要去ViewCache中去取是否已有預(yù)加載好的view供我們使用。如果有則取出,并進行一次預(yù)加載補充給ViewCache。
如果沒有,就只能同步加載布局了。
public?View?getView(@NonNull?ViewGroup?parent,?int?layoutId,?int?maxCount)?{
????View?view?=?mViewCache.getView(layoutId);
????if?(view?!=?null)?{
????????UILog.dTag(TAG,?"get?view?from?cache!");
????????preloadOnce(parent,?layoutId,?maxCount);
????????return?view;
????}
????return?mLayoutInflater.inflateView(parent,?layoutId);
}
對于預(yù)加載布局,并加入緩存的方法實現(xiàn)。
首先我們需要去ViewCache查詢當前可用緩存的數(shù)量,如果可用緩存的數(shù)量大于等于最大數(shù)量,即不需要進行預(yù)加載。
對于需要預(yù)加載的,需要計算預(yù)加載的數(shù)量,如果當前沒有強制執(zhí)行的次數(shù),就直接按剩余最大數(shù)量進行加載,否則取強制執(zhí)行次數(shù)和剩余最大數(shù)量的最小值進行加載。
對于預(yù)加載完畢獲取的View,直接加入到ViewCache中。
public?void?preload(@NonNull?ViewGroup?parent,?int?layoutId,?int?maxCount,?int?forcePreCount)?{
????int?viewsAvailableCount?=?mViewCache.getViewPoolAvailableCount(layoutId);
????if?(viewsAvailableCount?>=?maxCount)?{
????????return;
????}
????int?needPreloadCount?=?maxCount?-?viewsAvailableCount;
????if?(forcePreCount?>?0)?{
????????needPreloadCount?=?Math.min(forcePreCount,?needPreloadCount);
????}
????for?(int?i?=?0;?i?<?needPreloadCount;?i++)?{
????????//?異步加載View
????????mLayoutInflater.asyncInflateView(parent,?layoutId,?new?InflateCallback()?{
????????????
????????????public?void?onInflateFinished(int?layoutId,?View?view)?{
????????????????mViewCache.putView(layoutId,?view);
????????????}
????????});
????}
}
Adapter中執(zhí)行預(yù)加載
有了預(yù)加載輔助類PreInflateHelper,下面我們只需要直接調(diào)用它的preload
方法和getView
方法即可。這里需要注意的是,ViewHolder中ItemView的ViewGroup就是RecyclerView它本身,所以Adapter的構(gòu)造方法需要傳入RecyclerView供預(yù)加載輔助類進行預(yù)加載。
public?class?OptimizeListAdapter?extends?MockLongTimeLoadListAdapter?{
????private?static?final?class?InstanceHolder?{
????????static?final?PreInflateHelper?sInstance?=?new?PreInflateHelper();
????}
????
????public?static?PreInflateHelper?getInflateHelper()?{
????????return?OptimizeListAdapter.InstanceHolder.sInstance;
????}
????public?OptimizeListAdapter(RecyclerView?recyclerView)?{
????????getInflateHelper().preload(recyclerView,?getItemLayoutId(0));
????}
????
????protected?View?inflateView(@NonNull?ViewGroup?parent,?int?layoutId)?{
????????return?getInflateHelper().getView(parent,?layoutId);
????}
}
對比實驗
模擬耗時場景
為了能夠模擬inflateView的極端情況,這里我簡單給inflateView增加300ms的線程sleep來模擬耗時操作。
/**
?*?模擬耗時加載
?*/
public?static?View?mockLongTimeLoad(@NonNull?ViewGroup?parent,?int?layoutId)?{
????try?{
????????//?模擬耗時
????????Thread.sleep(300);
????}?catch?(InterruptedException?e)?{
????????e.printStackTrace();
????}
????return?LayoutInflater.from(parent.getContext()).inflate(layoutId,?parent,?false);
}
對于模擬耗時加載的Adapter,我們調(diào)用上面的方法創(chuàng)建ViewHolder。
public?class?MockLongTimeLoadListAdapter?extends?BaseRecyclerAdapter<NewInfo>?{
????/**
?????*?這里是加載view的地方,?使用mockLongTimeLoad進行mock
?????*/
????
????protected?View?inflateView(@NonNull?ViewGroup?parent,?int?layoutId)?{
????????return?InflateUtils.mockLongTimeLoad(parent,?layoutId);
????}
}
而對于異步加載的耗時模擬,我則是copy了AsyncLayoutInflater
的源碼,然后修改了它在InflateThread中的加載方法:
private?static?class?InflateThread?extends?Thread?{
????public?void?runInner()?{
????????//?部分代碼省略....
????????//?模擬耗時加載
????????request.view?=?InflateUtils.mockLongTimeLoad(request.inflater.mInflater,
????????????????request.parent,?request.resid);
????}
}
對比數(shù)據(jù)
優(yōu)化前


優(yōu)化后


從上面的動圖和日志,我們不難看出在優(yōu)化前,每個onCreateViewHolder
的耗時都在之前設(shè)定的300ms以上,這就導(dǎo)致了列表滑動和刷新都會產(chǎn)生比較明顯的卡頓。
而再看優(yōu)化后的效果,不僅列表滑動和刷新效果非常絲滑,而且每個onCreateViewHolder
的耗時都在0ms,極大地提升了列表的刷新和渲染性能。
總結(jié)
相信看完以上內(nèi)容后,你會發(fā)現(xiàn)寫了這么多,無非就是把onCreateViewHolder
中加載布局的操作提前,并放到了子線程中去處理,其本質(zhì)依然是空間換時間,并將列表數(shù)據(jù)網(wǎng)絡(luò)請求到列表刷新這段事務(wù)真空的時間窗口有效利用起來。
本文的全部源碼我都放在了github上, 感興趣的小伙伴可以下下來研究和學習。
項目地址: https://github.com/xuexiangjys/XUI/tree/master/app/src/main/java/com/xuexiang/xuidemo/fragment/components/refresh/sample/preload
我是xuexiangjys,一枚熱愛學習,愛好編程,勤于思考,致力于Android架構(gòu)研究以及開源項目經(jīng)驗分享的技術(shù)up主。獲取更多資訊,歡迎微信搜索公眾號:【我的Android開源之旅】