1.RecyclerView緩存原理
2.ListView和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).局部刷新方式
第一次要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),從而實現在滑動過程中不加載,當滾動靜止時,刷新界面,實現加載。