Android 3分鐘徹底搞懂 RecyclerView 的緩存機制,再也不怕面試被虐了 RecyclerView卡頓優化 刷新閃爍優化(絕對干貨!!)

1.RecyclerView緩存原理

2.ListView和RecyclerView區別

3.為什么RecyclerView加載首屏會慢一些

4.如何讓兩個 RecyclerView 共用一個緩存,今日頭條頁面實例

5.如何解決RecyclerView滑動卡頓問題

6.快速滑動RecycleView卡頓解決辦法


1.RecyclerView緩存原理

RecyclerView 是?ListView 的升級版本,更加先進和靈活。看名字我們就能看出一點端倪,沒錯,它主要的特點就是復用。回收的類在LayoutManager

回收原理:

注:官網上貌似把mAttachedScrap、mCachedViews當成一級了,為了方便區分,本文還是把他們當成兩級緩存。

緩存涉及對象作用重新創建視圖View(onCreateViewHolder)重新綁定數據(onBindViewHolder)

一級緩存mAttachedScrap緩存屏幕中可見范圍的ViewHolderfalsefalse

二級緩存mCachedViews緩存滑動時即將與RecyclerView分離的ViewHolder,按子View的position或id緩存,默認最多存放2個falsefalse

三級緩存mViewCacheExtension開發者自行實現的緩存--

四級緩存mRecyclerPoolViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType(int類型),value存放的是 ArrayList< ViewHolder>,默認每個ArrayList中最多存放5個ViewHolderfalsetrue

RecyclerView滑動時會觸發onTouchEvent#onMove,回收及復用ViewHolder在這里就會開始

mAttachedScrap(第一屏,可見)----mCachedViews(剛剛移除的)--------mRecyclerPool(總的)

1).它會先在mAttachedScrap中找,看要的View是不是剛剛剝離的,如果是就直接返回使用,

2).如果不是,先在mCachedViews中查找,因為在mCachedViews中精確匹配,如果匹配到,就說明這個HolderView是剛剛被移除的,也直接返回,

3).如果匹配不到就會最終到mRecyclerPool找,如果mRecyclerPool有現成的holderView實例,這時候就不再是精確匹配了,只要有現成的holderView實例就返回給我們使用,只有在mRecyclerPool為空時,才會調用onCreateViewHolder新建。

具體分析

一.mAttachedScrap到底有什么用?

(第一屏,可見),第一次存放。用于插入一個數據進去的時候用到。滑動的時候不用到

二.mCachedViews它的作用就是保存最新被移除的HolderView

自定義ViewCacheExtension緩存作用,適用場景:ViewHolder位置固定、內容固定、數量有限時使用

緩存的存和取的過程:

取的原則:mCachedViews > mRecyclerPool

mAttachedScrap不參與回收復用,只保存從在重新布局時,從RecyclerView中剝離的當前在顯示的HolderView列表。

所以,mCachedViews、mViewCacheExtension、mRecyclerPool組成了回收復用的三級緩存,當RecyclerView要拿一個復用的HolderView時,獲取優先級是mCachedViews > mViewCacheExtension > mRecyclerPool。由于一般而言我們是不會自定義mViewCacheExtension的。所以獲取順序其實就是mCachedViews > mRecyclerPool,

存放過程:mCachedViews------mRecyclerPool(一個靜態類)

在我們標記為Removed以為,會把這個HolderView移到mCachedViews中,如果mCachedViews已滿,就利用先進先出原則,將mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。

舉例:

上滑動:上面不可見的移動到mCachedViews然后是mRecyclerPool=========調用的方法getViewForPosition()

下面新的可見, 會從到mCachedViews找然后是mRecyclerPool============調用的方法removeAndRecycleView(child, recycler)

為什么這么設計多個緩存?優化效率:

這里需要注意的是,在mAttachedScrap和mCachedViews中拿到的HolderView,因為都是精確匹配的,所以都是直接使用,不會調用onBindViewHolder重新綁定數據,只有在mRecyclerPool中拿到的HolderView才會重新綁定數據。正是有mCachedViews的存在,所以只有在RecyclerView來回滾動時,池子的使用效率最高,因為凡是從mCachedViews中取的HolderView是直接使用的,不需要重新綁定數據。

mRecyclerPool容量是5

mCachedViews容量是2,他們最多是7個,為什么后面一直不用創建了呢?一般只創建一屏!

后面移出一個,然后就填充一個。

源碼分析:

ViewgetViewForPosition(int position, boolean dryRun) {

return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;

}

ViewHoldertryGetViewHolderForPositionByDeadline(int position,

? ? ? ? boolean dryRun, long deadlineNs) {

if (position <0 || position >=mState.getItemCount()) {

throw new IndexOutOfBoundsException("Invalid item position " + position

+"(" + position +"). Item count:" +mState.getItemCount());

? ? }

boolean fromScrapOrHiddenOrCache =false;

? ? ViewHolder holder =null;

? ? // 0) If there is a changed scrap, try to find from there

? ? if (mState.isPreLayout()) {

holder = getChangedScrapViewForPosition(position);

? ? ? ? fromScrapOrHiddenOrCache = holder !=null;

? ? }

// 1) Find by position from scrap/hidden list/cache

? ? if (holder ==null) {

holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

? ? ? ? if (holder !=null) {

if (!validateViewHolderForOffsetPosition(holder)) {

// recycle holder (and unscrap if relevant) since it can't be used

? ? ? ? ? ? ? ? if (!dryRun) {

// we would like to recycle this but need to make sure it is not used by

// animation logic etc.

? ? ? ? ? ? ? ? ? ? holder.addFlags(ViewHolder.FLAG_INVALID);

? ? ? ? ? ? ? ? ? ? if (holder.isScrap()) {

removeDetachedView(holder.itemView, false);

? ? ? ? ? ? ? ? ? ? ? ? holder.unScrap();

? ? ? ? ? ? ? ? ? ? }else if (holder.wasReturnedFromScrap()) {

holder.clearReturnedFromScrapFlag();

? ? ? ? ? ? ? ? ? ? }

recycleViewHolderInternal(holder);

? ? ? ? ? ? ? ? }

holder =null;

? ? ? ? ? ? }else {

fromScrapOrHiddenOrCache =true;

? ? ? ? ? ? }

}

}

if (holder ==null) {

final int offsetPosition =mAdapterHelper.findPositionOffset(position);

? ? ? ? if (offsetPosition <0 || offsetPosition >=mAdapter.getItemCount()) {

throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "

? ? ? ? ? ? ? ? ? ? +"position " + position +"(offset:" + offsetPosition +")."

? ? ? ? ? ? ? ? ? ? +"state:" +mState.getItemCount());

? ? ? ? }

final int type =mAdapter.getItemViewType(offsetPosition);

? ? ? ? // 2) Find from scrap/cache via stable ids, if exists

? ? ? ? if (mAdapter.hasStableIds()) {

holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),

? ? ? ? ? ? ? ? ? ? type, dryRun);

? ? ? ? ? ? if (holder !=null) {

// update position

? ? ? ? ? ? ? ? holder.mPosition = offsetPosition;

? ? ? ? ? ? ? ? fromScrapOrHiddenOrCache =true;

? ? ? ? ? ? }

}

if (holder ==null &&mViewCacheExtension !=null) {

// We are NOT sending the offsetPosition because LayoutManager does not

// know it.

? ? ? ? ? ? final View view =mViewCacheExtension

? ? ? ? ? ? ? ? ? ? .getViewForPositionAndType(this, position, type);

? ? ? ? ? ? if (view !=null) {

holder = getChildViewHolder(view);

? ? ? ? ? ? ? ? if (holder ==null) {

throw new IllegalArgumentException("getViewForPositionAndType returned"

? ? ? ? ? ? ? ? ? ? ? ? ? ? +" a view which does not have a ViewHolder");

? ? ? ? ? ? ? ? }else if (holder.shouldIgnore()) {

throw new IllegalArgumentException("getViewForPositionAndType returned"

? ? ? ? ? ? ? ? ? ? ? ? ? ? +" a view that is ignored. You must call stopIgnoring before"

? ? ? ? ? ? ? ? ? ? ? ? ? ? +" returning this view.");

? ? ? ? ? ? ? ? }

}

}

if (holder ==null) {// fallback to pool

? ? ? ? ? ? if (DEBUG) {

Log.d(TAG, "tryGetViewHolderForPositionByDeadline("

? ? ? ? ? ? ? ? ? ? ? ? + position +") fetching from shared pool");

? ? ? ? ? ? }

holder = getRecycledViewPool().getRecycledView(type);

? ? ? ? ? ? if (holder !=null) {

holder.resetInternal();

? ? ? ? ? ? ? ? if (FORCE_INVALIDATE_DISPLAY_LIST) {

invalidateDisplayListInt(holder);

? ? ? ? ? ? ? ? }

}

}

if (holder ==null) {

long start = getNanoTime();

? ? ? ? ? ? if (deadlineNs !=FOREVER_NS

? ? ? ? ? ? ? ? ? ? && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {

// abort - we have a deadline we can't meet

? ? ? ? ? ? ? ? return null;

? ? ? ? ? ? }

holder =mAdapter.createViewHolder(RecyclerView.this, type);

? ? ? ? ? ? if (ALLOW_THREAD_GAP_WORK) {

// only bother finding nested RV if prefetching

? ? ? ? ? ? ? ? RecyclerView innerView =findNestedRecyclerView(holder.itemView);

? ? ? ? ? ? ? ? if (innerView !=null) {

holder.mNestedRecyclerView =new WeakReference<>(innerView);

? ? ? ? ? ? ? ? }

}

long end = getNanoTime();

? ? ? ? ? ? mRecyclerPool.factorInCreateTime(type, end - start);

? ? ? ? ? ? if (DEBUG) {

Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");

? ? ? ? ? ? }

}

}

Demo地址:https://github.com/pengcaihua123456/shennandadao/tree/master

onCreateViewHolder()方法執行次數

onBindViewHolder()方法的執行次數

2.ListView和RecyclerView區別

1).緩存機制不一樣

?RecyclerView中mCacheViews(屏幕外)獲取緩存時,是通過匹配pos獲取目標位置的緩存,這樣做的好處是,當數據源數據不變的情況下,無須重新bindView,而同樣是離屏緩存,ListView從mScrapViews根據pos獲取相應的緩存,但是并沒有直接使用,而是重新getView(即必定會重新bindView)

ListView和RecyclerView最大的區別在于數據源改變時的緩存的處理邏輯,ListView是”一鍋端”,將所有的mActiveViews都移入了二級緩存mScrapViews,而RecyclerView則是更加靈活地對每個View修改標志位,區分是否重新bindView。

2).Listview支持,HeaderView 和 FooterView? ?而RecyclerView支持橫豎滑動LayoutManager

3).?RecyclerView支持動畫

4).局部刷新方式

3.為什么RecyclerView加載首屏會慢一些

第一次要createview和bindview()。沒有任何緩存

4.如何讓兩個 RecyclerView 共用一個緩存

通過RecyclewView直接獲回收池

RecyclerView.RecycledViewPool recycledViewPool=mRecyclerView.getRecycledViewPool();

使用多個RecyclerView,并且里面有相同item布局時,這時就可以通過setRecycledViewPool()設置同一個RecycledViewPool;

5.如何解決RecyclerView滑動卡頓問題

1)、根據需求修改RecyclerView默認的繪制緩存選項

recyclerView.setItemViewCacheSize(20);recyclerView.setDrawingCacheEnabled(true);recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

當item會出現頻繁的來回滑動時,可以通過setItemViewCacheSize()設置mCachedViews的數量,這個緩存主要是不需要重新進行綁定數據;

典型的是:用空間換時間的方法。

2).使用多個RecyclerView,并且里面有相同item布局時,這時就可以通過setRecycledViewPool()設置同一個RecycledViewPool;

因為,RecycleViewPool用來存放?mCachedViews 移除的ViewHolder。按照?Type 類型,默認對每個Type最多緩存 5 個。重點源碼中它是被?public static?修飾,表示可以被其他RecyclerView 共享。

3).當是網格布局的時候,如果一行的item超過五個,需要通過setMaxRecycledViews()去重新設置緩存的最大個數;

4).可以使用setHasStableIds(true)進行設置(同時重寫Adapter的getItemID()方法),這時會復用到scrap緩存;

源碼里面:

ViewHoldertryGetViewHolderForPositionByDeadline(int position,

? ? ? ? boolean dryRun, long deadlineNs) {

if (position <0 || position >=mState.getItemCount()) {

throw new IndexOutOfBoundsException("Invalid item position " + position

+"(" + position +"). Item count:" +mState.getItemCount());

? ? }

boolean fromScrapOrHiddenOrCache =false;

? ? ViewHolder holder =null;

? ? // 0) If there is a changed scrap, try to find from there

? ? if (mState.isPreLayout()) {

holder = getChangedScrapViewForPosition(position);

? ? ? ? fromScrapOrHiddenOrCache = holder !=null;

? ? }

5).局部刷新替代全局刷新。避免整個列表的數據更新,只更新受影響的布局。例如,加載更多時,不使用notifyDataSetChanged(),而是使用notifyItemRangeInserted(rangeStart, rangeEnd)

6).滑動監聽

主要就是對onScrollStateChanged方法進行監聽,然后通知adapter是否加載圖片或復雜布局

7).measure()優化和減少requestLayout()調用

當RecyclerView寬高的測量模式都是EXACTLY時,onMeasure()方法不需要執行dispatchLayoutStep1()等方法來進行測量。而當RecyclerView的寬高不確定并且至少一個child的寬高不確定時,要measure兩遍。

因此將RecyclerView的寬高模式都設置為EXACTLY有助于優化性能。

? ? protected void onMeasure(int widthSpec, int heightSpec) {

? ? ? ? // ......

? ? ? ? if (mLayout.isAutoMeasureEnabled()) {

? ? ? ? ? ? final int widthMode = MeasureSpec.getMode(widthSpec);

? ? ? ? ? ? final int heightMode = MeasureSpec.getMode(heightSpec);

? ? ? ? ? ? ? ? ? ? ? ?mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

? ? ? ? ? ? final boolean measureSpecModeIsExactly =

? ? ? ? ? ? ? ? ? ? widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;

? ? ? ? ? ? if (measureSpecModeIsExactly || mAdapter == null) {

? ? ? ? ? ? ? ? return;

? ? ? ? ? ? }

? ? ? ? ? ? // ......

? ? }

還有一個方法RecyclerView.setHasFixedSize(true)可以避免數據改變時重新計算RecyclerView的大小

6.快速滑動RecycleView卡頓解決辦法

(1)快速滑動RecycleView卡頓原因:

因為,列表上下滑動的時候,RecycleView會在執行復用策略,onCreateViewHolder和onBindViewHolder會執行。item視圖創建或數據綁定的方法會隨著滑動被多次執行,容易造成卡頓。

(2)解決快速滑動造成的卡頓

一般都采用滑動關閉數據加載優化:主要是設置RecyclerView.addOnScrollListener();通過自定義一個滑動監聽類繼承onScrollListener抽象類,實現滑動狀態改變的方法onScrollStateChanged(recycleview,state),從而實現在滑動過程中不加載,當滾動靜止時,刷新界面,實現加載

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容