Android開發(fā)RecyclerView性能優(yōu)化之異步預加載
前言
首先需要強調的是,這篇文章是對我之前寫的《淺談RecyclerView的性能優(yōu)化》文章的補充,建議大家先讀完這篇文章后再來看這篇文章,味道更佳。
當時由于篇幅的原因,并沒有深入展開講解,于是有很多感興趣的朋友紛紛留言表示:能不能結合相關的示例代碼講解一下到底如何實現?那么今天我就結合之前講的如何優(yōu)化onCreateViewHolder
的加載時間,講一講如何實現onCreateViewHolder
的異步預加載,文章末尾會給出示例代碼的鏈接地址,希望能給你帶來啟發(fā)。
分析
之前我們講過,在優(yōu)化onCreateViewHolder
方法的時候,可以降低item的布局層級,可以減少界面創(chuàng)建的渲染時間,其本質就是降低view的inflate時間。因為onCreateViewHolder
最大的耗時部分,就是view的inflate。相信讀過LayoutInflater.inflate
源碼的人都知道,這部分的代碼是同步操作,并且涉及到大量的文件IO的操作以及鎖操作,通常來說這部分的代碼快的也需要幾毫秒,慢的可能需要幾十毫秒乃至上百毫秒也是很有可能的。 如果真到了每個ItemView的inflate需要花上上百毫秒的話,那么在大數據量的RecyclerView進行快速上下滑動的時候,就必然會導致界面的滑動卡頓、不流暢。
那么如何你的程序里真的有這樣一個列表,它的每個ItemView都需要花上上百毫秒的時間去inflate的話,你該怎么做?
- 首先就是對布局進行優(yōu)化,降低item的布局層級。但這點的優(yōu)化往往是微乎其微的。
- 其次可能就是想辦法讓設計師重新設計,將布局中的某些內容刪除或者折疊了,對暫不展示的內容使用ViewStub進行延遲加載。不過說實在話,你既然有能力讓設計師重新設計的話,還干個球的開發(fā)啊,直接當項目經理不香嗎?
- 最后你可能會考慮不用xml寫布局,改為使用代碼自己一個一個new布局。話說回來了,一個使用xml加載的布局都要花上上百毫秒的布局,可能xml都快上千行下去了,你確定要自己一個一個new下去?
以上的方式,都是建立在列表布局可以修改的情況下,如果我們使用的列表布局是第三方已經提供好的呢?(例如廣告SDK等)
那么有沒有什么辦法既可以不用修改當前的xml布局,又可以極大地縮短布局的加載時間呢?毫無疑問,布局異步加載將為你打開新的世界。
原理
Google官方很早就發(fā)現了XML布局加載的性能問題,于是在androidx中提供了異步加載工具AsyncLayoutInflater。其本質就是開了一個長期等待的異步線程,在子線程中inflate view,然后把加載好的view通過接口拋出去,完成view的加載。
一般來說,對于復雜的列表,往往都對應了復雜的數據,而這復雜的數據往往又是通過服務器獲取而來。所以一般來說,一個列表在加載前,往往先需要訪問服務器獲取數據,然后再刷新列表顯示,而這訪問服務器的時間大約也在300ms~1000ms之間。很多開發(fā)人員對這段時間往往沒有加以利用,只是加上一個loading動畫了事。
其實對于這一段事務真空的時間窗口,我們可以提前進行列表的ItemView的加載,這樣等數據請求下來刷新列表的時候,我們onCreateViewHolder
的時候就可以直接到已經事先預加載好的View緩存池中直接獲取View傳到ViewHolder中使用,這樣onCreateViewHolder
的創(chuàng)建時間幾乎耗時為0,從而極大地提升了列表的加載和渲染速度。詳細的流程可以參見下圖:
實現
上面我簡單地講解了一下原理,下一步就是考慮如何實現這樣的效果了。
預加載緩存池
首先在預加載前,我們需要先創(chuàng)建一個緩存池來存儲預加載的View對象。
這里我選擇使用SparseArray
進行存儲,key是Int型,存放布局資源的layoutId,value是Object型,存放的是這類布局加載View的集合。
這里的集合類型我選擇的是LinkedList,因為我們的緩存需要頻繁的添加和刪除操作,并且LinkedList實現了Deque接口,具備先入先出的能力。
這里View的引用我選擇的是軟引用SoftReference,之所以不采用WeakReference, 目的就是希望緩存能多存在一段時間,避免內存的頻繁釋放和回收造成內存的抖動。
private static class ViewCache { private final SparseArray<LinkedList<SoftReference<View>>> mViewPools = new SparseArray<>(); @NonNull 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)); } @Nullable 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的加載方法,涉及到三個參數: 資源Id-resourceId, 父布局-root和是否添加到根布局-attachToRoot。
public View inflate(int resource, ViewGroup root, boolean attachToRoot) { }
這里在onCreateViewHolder
方法中attachToRoot恒為false,因此異步布局加載只需要前面兩個參數以及一個回調接口即可,即如下的定義:
public interface ILayoutInflater { /** * 異步加載View * * @param parent 父布局 * @param layoutId 布局資源id * @param callback 加載回調 */ 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); }
至于接口實現的話,就直接使用Google官方提供的異步加載工具AsyncLayoutInflater來實現。
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; } @Override 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); } }); } @Override public View inflateView(@NonNull ViewGroup parent, int layoutId) { return InflateUtils.getInflateView(parent, layoutId); } }
預加載輔助類
有了預加載緩存池ViewCache和異步加載能力的提供者IAsyncInflater,下面就是來協調這兩者進行合作,完成布局的預加載和View的讀取。
首先需要定義的是根據ViewGroup和layoutId獲取View的方法,提供給Adapter的onCreateViewHolder
方法使用。
- 首先我們需要去ViewCache中去取是否已有預加載好的view供我們使用。如果有則取出,并進行一次預加載補充給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); }
對于預加載布局,并加入緩存的方法實現。
- 首先我們需要去ViewCache查詢當前可用緩存的數量,如果可用緩存的數量大于等于最大數量,即不需要進行預加載。
- 對于需要預加載的,需要計算預加載的數量,如果當前沒有強制執(zhí)行的次數,就直接按剩余最大數量進行加載,否則取強制執(zhí)行次數和剩余最大數量的最小值進行加載。
- 對于預加載完畢獲取的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() { @Override public void onInflateFinished(int layoutId, View view) { mViewCache.putView(layoutId, view); } }); } }
Adapter中執(zhí)行預加載
有了預加載輔助類PreInflateHelper,下面我們只需要直接調用它的preload
方法和getView
方法即可。這里需要注意的是,ViewHolder中ItemView的ViewGroup就是RecyclerView它本身,所以Adapter的構造方法需要傳入RecyclerView供預加載輔助類進行預加載。
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)); } @Override 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,我們調用上面的方法創(chuàng)建ViewHolder。
public class MockLongTimeLoadListAdapter extends BaseRecyclerAdapter<NewInfo> { /** * 這里是加載view的地方, 使用mockLongTimeLoad進行mock */ @Override 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); } }
對比數據
優(yōu)化前
優(yōu)化后
從上面的動圖和日志,我們不難看出在優(yōu)化前,每個onCreateViewHolder
的耗時都在之前設定的300ms以上,這就導致了列表滑動和刷新都會產生比較明顯的卡頓。
而再看優(yōu)化后的效果,不僅列表滑動和刷新效果非常絲滑,而且每個onCreateViewHolder
的耗時都在0ms,極大地提升了列表的刷新和渲染性能。
總結
相信看完以上內容后,你會發(fā)現寫了這么多,無非就是把onCreateViewHolder
中加載布局的操作提前,并放到了子線程中去處理,其本質依然是空間換時間,并將列表數據網絡請求到列表刷新這段事務真空的時間窗口有效利用起來。
本文的全部源碼我都放在了github上, 感興趣的小伙伴可以下下來研究和學習。
以上就是Android開發(fā)RecyclerView性能優(yōu)化之異步預加載的詳細內容,更多關于Android RecyclerView異步預加載的資料請關注腳本之家其它相關文章!
相關文章
Android采取BroadcastReceiver方式自動獲取驗證碼
這篇文章主要介紹了Android采取BroadcastReceiver方式自動獲取驗證碼,感興趣的小伙伴們可以參考一下2016-08-08Android、iOS和Windows Phone中的推送技術詳解
這篇文章主要介紹了Android、iOS和Windows Phone中的推送技術詳解,推送技術的實現通常會使用服務端向客戶端推送消息的方式,也就是說客戶端通過用戶名、Key等ID注冊到服務端后,在服務端就可以將消息向所有活動的客戶端發(fā)送,需要的朋友可以參考下2015-01-01Android仿微信Viewpager-Fragment惰性加載(lazy-loading)
這篇文章主要為大家詳細介紹了Android仿微信Viewpager-Fragment惰性加載lazy-loading,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08Compose狀態(tài)保存rememberSaveable原理解析
這篇文章主要為大家介紹了Compose狀態(tài)保存rememberSaveable原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11