Android 3分鐘徹底搞懂 RecyclerView 的緩存機制,再也不怕面試被虐了 RecyclerView卡頓優(yōu)化 刷新閃爍優(yōu)化(絕對干貨P仿浴5鞘А)

1.RecyclerView緩存原理

2.ListView和RecyclerView區(qū)別

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

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

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

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


1.RecyclerView緩存原理

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

回收原理:

注:官網(wǎng)上貌似把mAttachedScrap令杈、mCachedViews當成一級了走敌,為了方便區(qū)分,本文還是把他們當成兩級緩存逗噩。

緩存涉及對象作用重新創(chuàng)建視圖View(onCreateViewHolder)重新綁定數(shù)據(jù)(onBindViewHolder)

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

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

三級緩存mViewCacheExtension開發(fā)者自行實現(xiàn)的緩存--

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

RecyclerView滑動時會觸發(fā)onTouchEvent#onMove项炼,回收及復用ViewHolder在這里就會開始

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

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

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

3).如果匹配不到就會最終到mRecyclerPool找摄杂,如果mRecyclerPool有現(xiàn)成的holderView實例坝咐,這時候就不再是精確匹配了,只要有現(xiàn)成的holderView實例就返回給我們使用析恢,只有在mRecyclerPool為空時墨坚,才會調(diào)用onCreateViewHolder新建。

具體分析

一.mAttachedScrap到底有什么用映挂?

(第一屏泽篮,可見),第一次存放柑船。用于插入一個數(shù)據(jù)進去的時候用到帽撑。滑動的時候不用到

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

自定義ViewCacheExtension緩存作用鞍时,適用場景:ViewHolder位置固定亏拉、內(nèi)容固定扣蜻、數(shù)量有限時使用

緩存的存和取的過程:

取的原則:mCachedViews > mRecyclerPool

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

所以,mCachedViews笙僚、mViewCacheExtension芳肌、mRecyclerPool組成了回收復用的三級緩存,當RecyclerView要拿一個復用的HolderView時肋层,獲取優(yōu)先級是mCachedViews > mViewCacheExtension > mRecyclerPool亿笤。由于一般而言我們是不會自定義mViewCacheExtension的。所以獲取順序其實就是mCachedViews > mRecyclerPool栋猖,

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

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

舉例:

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

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

為什么這么設(shè)計多個緩存衷掷?優(yōu)化效率:

這里需要注意的是辱姨,在mAttachedScrap和mCachedViews中拿到的HolderView,因為都是精確匹配的戚嗅,所以都是直接使用雨涛,不會調(diào)用onBindViewHolder重新綁定數(shù)據(jù),只有在mRecyclerPool中拿到的HolderView才會重新綁定數(shù)據(jù)懦胞。正是有mCachedViews的存在替久,所以只有在RecyclerView來回滾動時,池子的使用效率最高躏尉,因為凡是從mCachedViews中取的HolderView是直接使用的蚯根,不需要重新綁定數(shù)據(jù)。

mRecyclerPool容量是5

mCachedViews容量是2胀糜,他們最多是7個颅拦,為什么后面一直不用創(chuàng)建了呢?一般只創(chuàng)建一屏教藻!

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

源碼分析:

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()方法執(zhí)行次數(shù)

onBindViewHolder()方法的執(zhí)行次數(shù)

2.ListView和RecyclerView區(qū)別

1).緩存機制不一樣

?RecyclerView中mCacheViews(屏幕外)獲取緩存時括堤,是通過匹配pos獲取目標位置的緩存碌秸,這樣做的好處是绍移,當數(shù)據(jù)源數(shù)據(jù)不變的情況下,無須重新bindView讥电,而同樣是離屏緩存蹂窖,ListView從mScrapViews根據(jù)pos獲取相應(yīng)的緩存,但是并沒有直接使用允趟,而是重新getView(即必定會重新bindView)

ListView和RecyclerView最大的區(qū)別在于數(shù)據(jù)源改變時的緩存的處理邏輯恼策,ListView是”一鍋端”,將所有的mActiveViews都移入了二級緩存mScrapViews潮剪,而RecyclerView則是更加靈活地對每個View修改標志位涣楷,區(qū)分是否重新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()設(shè)置同一個RecycledViewPool碳褒;

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

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

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

當item會出現(xiàn)頻繁的來回滑動時看疗,可以通過setItemViewCacheSize()設(shè)置mCachedViews的數(shù)量沙峻,這個緩存主要是不需要重新進行綁定數(shù)據(jù);

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

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

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

3).當是網(wǎng)格布局的時候季惩,如果一行的item超過五個,需要通過setMaxRecycledViews()去重新設(shè)置緩存的最大個數(shù)坚弱;

4).可以使用setHasStableIds(true)進行設(shè)置(同時重寫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).局部刷新替代全局刷新荒叶。避免整個列表的數(shù)據(jù)更新碾阁,只更新受影響的布局。例如些楣,加載更多時脂凶,不使用notifyDataSetChanged()宪睹,而是使用notifyItemRangeInserted(rangeStart, rangeEnd)

6).滑動監(jiān)聽

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

7).measure()優(yōu)化和減少requestLayout()調(diào)用

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

因此將RecyclerView的寬高模式都設(shè)置為EXACTLY有助于優(yōu)化性能罪帖。

? ? 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)可以避免數(shù)據(jù)改變時重新計算RecyclerView的大小

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

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

因為,列表上下滑動的時候邮屁,RecycleView會在執(zhí)行復用策略整袁,onCreateViewHolder和onBindViewHolder會執(zhí)行。item視圖創(chuàng)建或數(shù)據(jù)綁定的方法會隨著滑動被多次執(zhí)行佑吝,容易造成卡頓坐昙。

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

一般都采用滑動關(guān)閉數(shù)據(jù)加載優(yōu)化:主要是設(shè)置RecyclerView.addOnScrollListener();通過自定義一個滑動監(jiān)聽類繼承onScrollListener抽象類,實現(xiàn)滑動狀態(tài)改變的方法onScrollStateChanged(recycleview,state)芋忿,從而實現(xiàn)在滑動過程中不加載炸客,當滾動靜止時,刷新界面戈钢,實現(xiàn)加載痹仙。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市殉了,隨后出現(xiàn)的幾起案子蝶溶,更是在濱河造成了極大的恐慌,老刑警劉巖宣渗,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異梨州,居然都是意外死亡痕囱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門暴匠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞍恢,“玉大人,你說我怎么就攤上這事每窖“锏簦” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵窒典,是天一觀的道長蟆炊。 經(jīng)常有香客問我,道長瀑志,這世上最難降的妖魔是什么涩搓? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任污秆,我火速辦了婚禮,結(jié)果婚禮上昧甘,老公的妹妹穿的比我還像新娘良拼。我一直安慰自己,他們只是感情好充边,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布庸推。 她就那樣靜靜地躺著,像睡著了一般浇冰。 火紅的嫁衣襯著肌膚如雪贬媒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天湖饱,我揣著相機與錄音掖蛤,去河邊找鬼。 笑死井厌,一個胖子當著我的面吹牛蚓庭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仅仆,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼器赞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了墓拜?” 一聲冷哼從身側(cè)響起港柜,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咳榜,沒想到半個月后夏醉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡涌韩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年畔柔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臣樱。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡靶擦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出雇毫,到底是詐尸還是另有隱情玄捕,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布棚放,位于F島的核電站枚粘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏席吴。R本人自食惡果不足惜赌结,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一捞蛋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柬姚,春花似錦拟杉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至撕捍,卻和暖如春拿穴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背忧风。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工默色, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狮腿。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓腿宰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缘厢。 傳聞我的和親對象是個殘疾皇子吃度,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容