preface
Based on the 2021-01-22 revised edition, source code implementation ‘androidx. Recyclerview: recyclerview: 1.1.0’
In the last article, we talked about ItemDecoration. In this article, we talked about recycle logic of RecyclerView.
- RecyclerView ItemDecoration (a)
- RecyclerView Cache (2)
- 【Android advanced 】RecyclerView drawing process (three)
- RecyclerView group list add top effect (4)
The problem
How many Viewholders will be created when RecyclerView slides if there are 100 items and the first screen displays a maximum of 2 and a half items (a screen displays a maximum of 4 items at the same time)?
Before you answer, let’s write a demo
First, the layout of the item
<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>
Copy the code
Then there is the RepeatAdapter, where the native Adapter is used
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(a) {
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); }}}Copy the code
Used in an Activity
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("The first" + i + "Item");
}
RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);
rvRepeat.setLayoutManager(new LinearLayoutManager(this));
rvRepeat.setAdapter(repeatAdapter);
Copy the code
When we swipe, log looks like this:As you can see, it was executed seven timesonCreateViewHolder
That is, out of a total of 100 items, only 7 were createdviewholder
(The length is not 100, if you are interested, you can try it yourself.)
According to?
By reading the source code, we found that is viewholder RecyclerView cache unit, and ultimately the method called viewholder is Recycler# tryGetViewHolderForPositionByDeadline source code is as follows:
@Nullable
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {... Omit code... holder =this.getChangedScrapViewForPosition(position); . Omit code...if (holder == null) {
holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); }... Omit code...if (holder == null) {
View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);
if(view ! =null) {
holder = RecyclerView.this.getChildViewHolder(view); }}... Omit code...if (holder == null) {
holder = this.getRecycledViewPool().getRecycledView(type); }... Omit code...if (holder == null) {
holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type); }... Omit code... }Copy the code
From top to bottom, 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;
Copy the code
mChangedScrap
The complete source code is as follows:
if (RecyclerView.this.mState.isPreLayout()) {
holder = this.getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! =null;
}
Copy the code
Since the isPreLayout method depends on mInPreLayout, mInPreLayout defaults to false, and when is mInPreLayout set to True? The answer is in onMeasure
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
stopInterceptRequestLayout(false);
}
Copy the code
MAdapterUpdateDuringMeasure is increased in Adapter deletion method will only be set to true, the details can see Android advanced RecyclerView 】 the drawing process of (3)
When did he add it? Let’s move on
mAttachedScrap
When was the Viewholder added to mAttachedScrap?
Add Recycler#scrapView()
/** * Mark an attached view as scrap. * * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible * for rebinding and reuse. Requests for a view for a given position may return a * reused or rebound scrap view instance.</p> * *@param view View to scrap
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if(holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {if(holder.isInvalid() && ! holder.isRemoved() && ! mAdapter.hasStableIds()) {throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this.false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this.true); mChangedScrap.add(holder); }}Copy the code
When do I call scrapView()? Continue to global search, found that is ultimately Recycler# detachAndScrapAttachedViews () method, this method is when is called? The answer is LayoutManager#onLayoutChildren().
We know onLayoutChildren is responsible for the layout of the item behind this part (say), so the mAttachedScrap should deposit is the current viewhoder is displayed on the screen, we’ll look at the source code of 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); }}Copy the code
ChildCount is the number of items displayed on the screen. It performs the above series of judgments only when it is greater than 0. When was it added? It’s in RecyclerView#addViewInt, and its call chain is
RecyclerView#addView
->LinearLayoutManager#layoutChunk
->LinearLayoutManager#fill
->LinearLayoutManager#onLayoutChildren
Eventually returned to onLayoutChildren method, but embarrassed is detachAndScrapAttachedViews is before the fill method calls!!!!!!
That is to say, under normal circumstances the above two are not involved in RecycleView recycling and reuse.
mCachedViews
The complete code is as follows:
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);
}
returnholder; }}Copy the code
Let’s first find out when the Viewholder was added to mCachedViews. In Recycler# recycleViewHolderInternal () method
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()); }}Copy the code
At the top is RecyclerView#removeAndRecycleViewAt
public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
View view = this.getChildAt(index);
this.removeViewAt(index);
recycler.recycleView(view);
}
Copy the code
Where is this method called? So the answer is LayoutManager, so let’s do a little demo that looks pretty intuitive and define MyLayoutManager, rewrite removeAndRecycleViewAt, and then add 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); }}Copy the code
Set it toRecyclerView
, and then swipe to see the log output
As you can see, this is called every time an item slides off the screenremoveAndRecycleViewAt()
Method, it is important to note that thisindex
That’s what this isitem
inchlid
Subscript, which is in the current screen, not inRecyclerView
.
Is that the case? The LinearLayoutManager defaults to vertical sliding, and the method that controls its sliding distance is scrollVerticallyBy(), which calls the scrollBy() method
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;
returnscrolled; }}else {
return 0; }}Copy the code
The key code is recycleByLayoutState() in the fill() method, which determines the slide direction and recycles from the first or last one.
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); }}}Copy the code
Pull some far, let’s review the recycleViewHolderInternal () method, when cachedViewSize > = this. MViewCacheMax, would remove the first, also is the first to join the viewholder, What’s mViewCacheMax?
public Recycler(a) {
this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap);
this.mRequestedCacheMax = 2;
this.mViewCacheMax = 2;
}
Copy the code
MViewCacheMax is 2, so the initial size of mCachedViews is 2, and beyond that, the viewholer will be removed, where will it go? With that in mind we move on
mViewCacheExtension
This class requires the user to pass in the setViewCacheExtension() method, and RecyclerView itself does not implement it, nor is it used for normal use.
mRecyclerPool
MCachedViews has an initial size of 2. After this size, the first viewholder to be added is removed. Where is the viewholder removed? Let’s look at the recycleCachedViewAt() method source
void recycleCachedViewAt(int cachedViewIndex) {
RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);
this.addViewHolderToRecycledViewPool(viewHolder, true);
this.mCachedViews.remove(cachedViewIndex);
}
Copy the code
AddViewHolderToRecycledViewPool () method
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);
}
Copy the code
As you can see, the Viewholder is added to mRecyclerPool
We continue to look at the RecycledViewPool source code
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(a) {}... Omit code... }Copy the code
static class ScrapData {
final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList();
int mMaxScrap = 5;
long mCreateRunningAverageNs = 0L;
long mBindRunningAverageNs = 0L;
ScrapData() {
}
}
Copy the code
As you can see, there is a SparseArray inside that holds the Viewholder.
conclusion
- A total of
mAttachedScrap
,mCachedViews
,mViewCacheExtension
,mRecyclerPool
Level 4 cache, wheremAttachedScrap
Save only the layout when displayed on the screenviewholder
, generally does not participate in recycling and reuse (Drag sort participates); mCachedViews
Mainly save just removed screenviewholder
, the initial size is 2;mViewCacheExtension
The reserved cache pool needs to be implemented by itself.mRecyclerPool
Is the last level cache, whenmCachedViews
When it’s full,viewholder
Will be storedmRecyclerPool
Continue reuse.
Where, mAttachedScrap and mCachedViews are exact matches, that is, viewholder corresponding to position will be reused. MRecyclerPool is a fuzzy match that only matches viewType. Therefore, onBindViewHolder needs to be called to set new data for mRecyclerPool.
Answer the previous question
When the sixth item is slid out, the first and second items are stored in mCachedViews, and the third, fourth, fifth and sixth items are displayed on the screen. When the seventh item is slid out, there is no viewholder that can be reused. So calling onCreateViewHolder creates a new Viewholder and puts the first viewholder into mRecyclerPool for reuse.