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

1.RecyclerView緩存原理

2.ListView和RecyclerView區別

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

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

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

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


1.RecyclerView緩存原理

RecyclerView 是?ListView 的升級版本,更加先進和靈活??疵治覀兙湍芸闯鲆稽c端倪,沒錯,它主要的特點就是復用?;厥盏念愒贚ayoutManager

回收原理:

注:官網上貌似把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),從而實現在滑動過程中不加載,當滾動靜止時,刷新界面,實現加載

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

  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容