1.RecyclerView緩存原理
2.ListView和RecyclerView區(qū)別
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).局部刷新方式
第一次要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)加載痹仙。