前言
上一篇,說了ItemDecoration
,這一篇,我們來說說RecyclerView
的回收複用邏輯。
- 【Android進階】RecyclerView之ItemDecoration(一)
- 【Android進階】RecyclerView之快取(二)
- 【Android進階】RecyclerView之繪製流程(三)
問題
假如有100個item
,首屏最多展示2個半(一屏同時最多展示4個),RecyclerView
滑動時,會建立多少個viewholder
?
先別急著回答,我們寫個 demo 看看
首先,是item
的佈局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_repeat"
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center" />
<TextView
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/colorAccent" />
</LinearLayout>
複製程式碼
然後是RepeatAdapter
,這裡使用的是原生的Adapter
public class RepeatAdapter extends RecyclerView.Adapter<RepeatAdapter.RepeatViewHolder> {
private List<String> list;
private Context context;
public RepeatAdapter(List<String> list, Context context) {
this.list = list;
this.context = context;
}
@NonNull
@Override
public RepeatViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(context).inflate(R.layout.item_repeat, viewGroup, false);
Log.e("cheng", "onCreateViewHolder viewType=" + i);
return new RepeatViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RepeatViewHolder viewHolder, int i) {
viewHolder.tv_repeat.setText(list.get(i));
Log.e("cheng", "onBindViewHolder position=" + i);
}
@Override
public int getItemCount() {
return list.size();
}
class RepeatViewHolder extends RecyclerView.ViewHolder {
public TextView tv_repeat;
public RepeatViewHolder(@NonNull View itemView) {
super(itemView);
this.tv_repeat = (TextView) itemView.findViewById(R.id.tv_repeat);
}
}
}
複製程式碼
在Activity
中使用
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("第" + i + "個item");
}
RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);
rvRepeat.setLayoutManager(new LinearLayoutManager(this));
rvRepeat.setAdapter(repeatAdapter);
複製程式碼
當我們滑動時,log如下:
可以看到,總共執行了7次onCreateViewHolder
,也就是說,總共100個item,只建立了7個viewholder
(篇幅問題,沒有截到100,有興趣的同學可以自己試試)
WHY?
通過閱讀原始碼,我們發現,RecyclerView
的快取單位是viewholder
,而獲取viewholder
最終呼叫的方法是Recycler#tryGetViewHolderForPositionByDeadline
原始碼如下:
@Nullable
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
...省略程式碼...
holder = this.getChangedScrapViewForPosition(position);
...省略程式碼...
if (holder == null) {
holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
...省略程式碼...
if (holder == null) {
View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = RecyclerView.this.getChildViewHolder(view);
}
}
...省略程式碼...
if (holder == null) {
holder = this.getRecycledViewPool().getRecycledView(type);
}
...省略程式碼...
if (holder == null) {
holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type);
}
...省略程式碼...
}
複製程式碼
從上到下,依次是mChangedScrap
、mAttachedScrap
、mCachedViews
、mViewCacheExtension
、mRecyclerPool
最後才是createViewHolder
ArrayList<RecyclerView.ViewHolder> mChangedScrap = null;
final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList();
final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList();
private RecyclerView.ViewCacheExtension mViewCacheExtension;
RecyclerView.RecycledViewPool mRecyclerPool;
複製程式碼
mChangedScrap
完整原始碼如下:
if (RecyclerView.this.mState.isPreLayout()) {
holder = this.getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
複製程式碼
由於isPreLayout
方法取決於mInPreLayout
,而mInPreLayout
預設為false
,即mChangedScrap不參與回收複用邏輯。
mAttachedScrap
完整原始碼如下:
RecyclerView.ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
int scrapCount = this.mAttachedScrap.size();
int cacheSize;
RecyclerView.ViewHolder vh;
for(cacheSize = 0; cacheSize < scrapCount; ++cacheSize) {
vh = (RecyclerView.ViewHolder)this.mAttachedScrap.get(cacheSize);
if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) {
vh.addFlags(32);
return vh;
}
}
}
複製程式碼
這段程式碼什麼時候會生效呢,那得找找什麼時候將viewholder
新增到mAttachedScrap
的
我們在原始碼中全域性搜尋mAttachedScrap.add
,發現是Recycler#scrapView()
方法
void scrapView(View view) {
...省略程式碼...
this.mAttachedScrap.add(holder);
...省略程式碼...
}
複製程式碼
什麼時候呼叫scrapView()
方法呢?
繼續全域性搜尋,發現最終是Recycler#detachAndScrapAttachedViews()
方法,這個方法又是什麼時候會被呼叫的呢?
答案是LayoutManager#onLayoutChildren()
。我們知道onLayoutChildren
負責item的佈局工作(這部分後面再說),所以,mAttachedScrap應該存放是當前螢幕上顯示的viewhoder
,我們來看下detachAndScrapAttachedViews
的原始碼
public void detachAndScrapAttachedViews(@NonNull RecyclerView.Recycler recycler) {
int childCount = this.getChildCount();
for(int i = childCount - 1; i >= 0; --i) {
View v = this.getChildAt(i);
this.scrapOrRecycleView(recycler, i, v);
}
}
複製程式碼
其中,childCount
即為螢幕上顯示的item數量。那同學們就要問了,mAttachedScrap
有啥用?
答案當然是有用的,比如說,拖動排序,比如說第1個item和第2個item 互換,這個時候,mAttachedScrap就派上了用場,直接從這裡通過position
拿viewholder
,都不用經過onCreateViewHolder
和onBindViewHolder
。
mCachedViews
完整程式碼如下:
cacheSize = this.mCachedViews.size();
for(int i = 0; i < cacheSize; ++i) {
RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
this.mCachedViews.remove(i);
}
return holder;
}
}
複製程式碼
我們先來找找viewholder
是在什麼時候新增進mCachedViews
?是在Recycler#recycleViewHolderInternal()
方法
void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
if (!holder.isScrap() && holder.itemView.getParent() == null) {
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel());
} else {
boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
boolean forceRecycle = RecyclerView.this.mAdapter != null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) {
int cachedViewSize = this.mCachedViews.size();
if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) {
this.recycleCachedViewAt(0);
--cachedViewSize;
}
int targetCacheIndex = cachedViewSize;
if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
int cacheIndex;
for(cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) {
int cachedPos = ((RecyclerView.ViewHolder)this.mCachedViews.get(cacheIndex)).mPosition;
if (!RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
}
targetCacheIndex = cacheIndex + 1;
}
this.mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
this.addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
RecyclerView.this.mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
} else {
throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + RecyclerView.this.exceptionLabel());
}
}
複製程式碼
最上層是RecyclerView#removeAndRecycleViewAt
方法
public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
View view = this.getChildAt(index);
this.removeViewAt(index);
recycler.recycleView(view);
}
複製程式碼
這個方法是在哪裡呼叫的呢?答案是LayoutManager
,我們寫個demo效果看著比較直觀
定義MyLayoutManager
,並重寫removeAndRecycleViewAt
,然後新增log
class MyLayoutManager extends LinearLayoutManager {
public MyLayoutManager(Context context) {
super(context);
}
@Override
public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
super.removeAndRecycleViewAt(index, recycler);
Log.e("cheng", "removeAndRecycleViewAt index=" + index);
}
}
複製程式碼
將其設定給RecyclerView
,然後滑動,檢視日誌輸出情況
removeAndRecycleViewAt()
方法,需要注意的是,此index
表示的是該item
在chlid
中的下標,也就是在當前螢幕中的下標,而不是在RecyclerView
的。
事實是不是這樣的呢?讓我們來看看原始碼,以LinearLayoutManager
為例,預設是垂直滑動的,此時控制其滑動距離的方法是scrollVerticallyBy()
,其呼叫的是scrollBy()
方法
int scrollBy(int dy, Recycler recycler, State state) {
if (this.getChildCount() != 0 && dy != 0) {
this.mLayoutState.mRecycle = true;
this.ensureLayoutState();
int layoutDirection = dy > 0 ? 1 : -1;
int absDy = Math.abs(dy);
this.updateLayoutState(layoutDirection, absDy, true, state);
int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
if (consumed < 0) {
return 0;
} else {
int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
this.mOrientationHelper.offsetChildren(-scrolled);
this.mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
} else {
return 0;
}
}
複製程式碼
關鍵程式碼是fill()
方法中的recycleByLayoutState()
,判斷滑動方向,從第一個還是最後一個開始回收。
private void recycleByLayoutState(Recycler recycler, LinearLayoutManager.LayoutState layoutState) {
if (layoutState.mRecycle && !layoutState.mInfinite) {
if (layoutState.mLayoutDirection == -1) {
this.recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
this.recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
}
複製程式碼
扯的有些遠了,讓我們回顧下recycleViewHolderInternal()
方法,當cachedViewSize >= this.mViewCacheMax
時,會移除第1個,也就是最先加入的viewholder
,mViewCacheMax
是多少呢?
public Recycler() {
this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap);
this.mRequestedCacheMax = 2;
this.mViewCacheMax = 2;
}
複製程式碼
mViewCacheMax
為2,也就是mCachedViews的初始化大小為2,超過這個大小時,viewholer
將會被移除,放到哪裡去了呢?帶著這個疑問我們繼續往下看
mViewCacheExtension
這個類需要使用者通過 setViewCacheExtension()
方法傳入,RecyclerView
自身並不會實現它,一般正常的使用也用不到。
mRecyclerPool
我們帶著之前的疑問,繼續看原始碼,之前提到mCachedViews初始大小為2,超過這個大小,最先放入的會被移除,移除的viewholder
到哪裡去了呢?我們來看recycleCachedViewAt()
方法的原始碼
void recycleCachedViewAt(int cachedViewIndex) {
RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);
this.addViewHolderToRecycledViewPool(viewHolder, true);
this.mCachedViews.remove(cachedViewIndex);
}
複製程式碼
addViewHolderToRecycledViewPool()
方法
void addViewHolderToRecycledViewPool(@NonNull RecyclerView.ViewHolder holder, boolean dispatchRecycled) {
RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(16384)) {
holder.setFlags(0, 16384);
ViewCompat.setAccessibilityDelegate(holder.itemView, (AccessibilityDelegateCompat)null);
}
if (dispatchRecycled) {
this.dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
this.getRecycledViewPool().putRecycledView(holder);
}
複製程式碼
我們繼續看看RecycledViewPool
的原始碼
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray();
private int mAttachCount = 0;
public RecycledViewPool() {
}
...省略程式碼...
}
複製程式碼
static class ScrapData {
final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList();
int mMaxScrap = 5;
long mCreateRunningAverageNs = 0L;
long mBindRunningAverageNs = 0L;
ScrapData() {
}
}
複製程式碼
可以看到,其內部有一個SparseArray
用來存放viewholder
。
總結
RecycledView
總共有mAttachedScrap
、mCachedViews
、mViewCacheExtension
、mRecyclerPool
4級快取,其中mAttachedScrap
只儲存佈局時,螢幕上顯示的viewholder
,一般不參與回收、複用(拖動排序時會參與);mCachedViews
主要儲存剛移除螢幕的viewholder
,初始大小為2;mViewCacheExtension
為預留的快取池,需要自己去實現;mRecyclerPool
則是最後一級快取,當mCachedViews
滿了之後,viewholder
會被存放在mRecyclerPool
,繼續複用。其中mAttachedScrap
、mCachedViews
為精確匹配,即為對應position
的viewholder
才會被複用,mRecyclerPool
為模糊匹配,只匹配viewType
,所以複用時,需要呼叫onBindViewHolder
為其設定新的資料。
回答之前的疑問
當滑出第6個item時,這時mCachedViews
中存放著第1、2個item,螢幕上顯示的是第3、4、5、6個item,再滑出第7個item時,不存在能複用的viewholder
,所以呼叫onCreateViewHolder
建立了一個新的viewholder
,並且把第1個viewholder
放入mRecyclerPool
,以備複用。