RecyclerView as a very attractive control, part of the credit is due to its excellent cache mechanism. The cache mechanism of RecyclerView is the core part of RecyclerView, but also the difficult part. As difficult as the caching mechanism is, it can’t resist our curiosity 😂. Today we’re going to look at some of its wonders.
References for this article:
- RecyclerView cache principle, there are figures and the truth
- RecyclerView source code analysis (two) — cache mechanism
- Deep RecyclerView source code explore four: recycling and animation
- Hand touch second play, visual RecyclerView cache mechanism
- RecyclerView source code analysis (a) – RecyclerView three processes
Since this article is related to the first two articles in this series, you can refer to the author’s first two articles in this series for ease of understanding.
Note that all the code in this article is from 27.1.1.
1. An overview of the
Before formally analyzing the source code, I will give an overview of the caching mechanism, but also a unified explanation of some concepts, which will be very helpful for the analysis of the following, because if you do not understand these concepts, it is easy to see the rain and fog behind.
(1) level 4 cache
First of all, I divided RecyclerView cache into four levels, maybe some people will divide it into three levels, these depends on personal understanding. Here’s a general explanation of what each level of caching means.
The cache level | Real variables | meaning |
---|---|---|
Level 1 cache | mAttachedScrap andmChangedScrap |
This is the highest priority cache,RecyclerView In obtainingViewHolder , the priority will be to find these two caches. Among themmAttachedScrap Store what’s currently on the screenViewHolder .mChangedScrap The store is where the data is updatedViewHolder , for example calledAdapter thenotifyItemChanged Methods. There may be some confusion about these two caches, but don’t worry, I’ll explain them in detail later. |
The second level cache | mCachedViews |
The default size is 2 and is usually used to store prefetchViewHolder At the same time in the recyclingViewHolder It is also possible to store some of themViewHolder This partViewHolder In general, the meaning is similar to level 1 cache. |
Three levels of cache | ViewCacheExtension |
Custom caches, which are not usually used, are ignored in this article |
Four levels of cache | RecyclerViewPool |
According to theViewType To cacheViewHolder , eachViewType The array size is 5 and can be changed dynamically. |
As shown in the table above, the meanings and functions of each cache are uniformly explained. Here, I’ll explain some of these caches in more detail.
mAttachedScrap
In the table above, it means that the storage is currently on the screenViewHolder
. It’s actually detached from the screenViewHolder
“, but will soon be added to the screenViewHolder
. For instance,RecyclerView
Slide up and down, slide out a new oneItem
, will be called againLayoutManager
theonLayoutChildren
Method, thus will put all of the screenViewHolder
First,scrap
Drop (meaning discard), add tomAttachedScrap
Go inside and then rearrange eachItemView
, will take precedencemAttachedScrap
So it’s very efficient. The process will not be repeatedonBindViewHolder
.mCachedViews
: The default size is 2, but it is usually 3. 3 consists of the default size 2 + the number of prefetches 1. So in theRecyclerView
On the first load,mCachedViews
thesize
Is 3LinearLayoutManager
Take the vertical layout of the. Generally speaking, yesRecyclerView
thesetItemViewCacheSize
Method sets the size, but this does not include the prefetch size; The prefetch size passesLayoutManager
thesetItemPrefetchEnabled
Method to control.
(2). Several state values of ViewHolder
Call ViewHolder isInvalid, isRemoved, isBound, isTmpDetached, isScrap and isUpdated. I’m going to explain it uniformly here.
The method name | The corresponding Flag | Meaning or timing of state setting |
---|---|---|
isInvalid |
FLAG_INVALID |
Said that the currentViewHolder Whether it is invalid. In general, this happens in three cases: 1Adapter thenotifyDataSetChanged Methods; 2. Manually invoke the callRecyclerView theinvalidateItemDecorations Methods; 3. CallRecyclerView thesetAdapter Methods orswapAdapter Methods. |
isRemoved |
FLAG_REMOVED |
Represents the currentViewHolder Whether to remove. Typically, the data source is partially removed and then calledAdapter thenotifyItemRemoved Methods. |
isBound |
FLAG_BOUND |
Said that the currentViewHolder Whether it has already been calledonBindViewHolder . |
isTmpDetached |
FLAG_TMP_DETACHED |
Represents the currentItemView Whether fromRecyclerView (i.e., the parentView )detach It off. Generally speaking, there are two situations where this happens: 1RecyclerView thedetachView Correlation method; 2. In the frommHideViews To get insideViewHolder , will firstdetach Off theViewHolder The associatedItemView . Here’s another onemHideViews I’ll explain what it is in more detail later. |
isScrap |
No Flag is used to indicate the statusmScrapContainer Check whether the value is null |
Indicates whether themAttachedScrap ormChangedScrap Array, in turn represents the currentViewHolder Whether to be abandoned. |
isUpdated |
FLAG_UPDATE |
Said that the currentViewHolder Whether it has been updated. Generally speaking, there are three situations that can happen: 1.isInvalid There are three cases of method existence; 2. CallAdapter theonBindViewHolder Methods; 3. The callAdapter thenotifyItemChanged methods |
(3). ChildHelper的mHiddenViews
In the four-level cache, we do not count mHiddenViews. Because mHiddenViews only have elements during the animation, when the animation is over, they are naturally empty. So mHiddenViews does not count in the level 4 cache.
Another problem with mChangedScrap is that when the Adapter notifyItemChanged method is called, the updated ViewHolder is reversed into the mChangedScrap array. MChangedScrap or mHiddenViews? While some people may have questions about mChangedScrap and mAttachedScrap, here is a unified explanation:
First, if the notifyItemChanged method of the Adapter is called, it is called back to the onLayoutChildren method of the LayoutManager, and in the onLayoutChildren method, All viewholders on the screen are recycled to mAttachedScrap and mChangedScrap. The ViewHolder is placed on mAttachedScrap and mChangedScrap respectively, and what condition is placed on mAttachedScrap and what condition is placed on mChangedScrap is the difference between them.
Let’s look at some code to tell the difference between mAttachedScrap and mChangedScrap
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
A lot of people, when they see this for the first time, they’re kind of dumbstruck, and I was. Today we are going to look at this method. The fundamental purpose of this is to determine the ViewHolder flag state to decide whether to add mAttachedScrap or mChangedScrap. From the above code, we get:
mAttachedScrap
It has two states in itViewHolder
: 1. Is simultaneously marked asremove
andinvalid
; 2. Completely unchangedViewHolder
. There’s a third judgment here, which is the same asRecyclerView
theItemAnimator
About, ifItemAnimator
Is empty orItemAnimator
thecanReuseUpdatedViewHolder
Method true will also be put intomAttachedScrap
. So normally, when does it return true? fromSimpleItemAnimator
The source code can be seen whenViewHolder
theisInvalid
When the method returns true, it is put intomAttachedScrap
The inside. In other words, ifViewHolder
If it’s not working, it’s going to bemAttachedScrap
The inside.- then
mChangedScrap
What type of flag is in itViewHolder
? Is, of course,ViewHolder
theisUpdated
When the method returns true, it is put intomChangedScrap
Go inside. So, callAdapter
thenotifyItemChanged
Method, andRecyclerView
theItemAnimator
It’s not empty, it puts intomChangedScrap
The inside.
Now that we know the difference between mAttachedScrap and mChangedScrap, let’s look at the difference between The Scrap array and mHiddenViews.
MHiddenViews only stores the ViewHolder of the animation, which is emptied when the animation is finished. The reason mHiddenViews exists, I guess, is that there is the possibility of reuse during animation, and then you can reuse it in mHiddenViews. The Scrap array and mHiddenViews do not conflict at all, so it is possible to have a ViewHolder in both the Scrap array and mHiddenViews. But it doesn’t matter because it will be removed from mHiddenViews at the end of the animation.
In this paper, the analysis of RecyclerView exchange mechanism, intends to start from two major aspects: 1. Reuse; 2. Recycle.
Let’s take a look at some of the logic of reuse, because only by understanding how RecyclerView is used, can we understand recycling more clearly.
2. Reuse
To reuse ViewHolder by RecyclerView, we need to start with LayoutState’s next method. When LayoutManager lays out the itemView, it needs to get a ViewHolder object. This is where the reuse logic is invoked. Let’s take a look:
View next(RecyclerView.Recycler recycler) {
if(mScrapList ! = null) {return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
Copy the code
The next method didn’t actually do anything, just call the getViewForPosition method of RecyclerView to get a View. GetViewForPosition method will call to RecyclerView tryGetViewHolderForPositionByDeadline method. So, RecyclerView is really the core of reuse in this method, we will analyze this method in detail today.
(1). Obtain the ViewHolder by Position
This is a higher priority because each ViewHolder has not been changed. In this case, the ViewHolder corresponding to an ItemView has been updated, so other Viewholers on the screen can quickly correspond to the original ItemView. Let’s look at the relevant source code.
if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! = null; } // 1) Find by position from scrap/hidden list/cacheif (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if(holder ! = null) {if(! validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrapif 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; }}}Copy the code
The above code is divided into two steps:
- from
mChangedScrap
Inside to getViewHolder
This store is updatedViewHolder
.- , respectively,
mAttachedScrap
,mHiddenViews
,mCachedViews
To obtainViewHolder
Let’s briefly analyze these two steps. Let’s look at step one.
if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! = null; }Copy the code
If the current layout phase is pre-layout, get the ViewHolder from the mChangedScrap. So what is the pre-layout stage? Here IS a brief explanation of the concept of pre-layout.
Pre-layout can also be called preLayout, when the current RecyclerView is in the dispatchLayoutStep1 stage, called pre-layout; DispatchLayoutStep2 is called the stage of the real layout; DispatchLayoutStep3 is called the postLayout stage. To enable pre-layout, you must have an ItemAnimator, and the LayoutManager of each RecyclerView must have pre-animation enabled.
Do you feel even more confused after hearing the explanation? In order to explain one concept, it leads to more concepts? As for animation, unsurprisingly, I will analyze it in the next article. This article will not explain animation too much. Here, for simplicity, as long as RecyclerView is dispatchLayoutStep1, we treat it as in the pre-layout stage.
Why only take it from mChangedScrap during pre-layout? First, we need to know what type of ViewHolder is in the mChangedScrap array. From the previous analysis, we know that the ViewHolder changed will be placed in the mChangedScrap array only if the ItemAnimator is not empty. Since the ViewHolder at the same location is different before and after the Chang animation, the mChangedScrap cache is used for pre-layout, and the mChangedScrap cache is not used for formal layout, ensuring that the ViewHolder at the same location is different before and after the chang animation. Why make sure the ViewHolder is different before and after the animation? This is the knowledge related to RecyclerView animation mechanism, which will not be explained in detail here. There will be special articles to analyze it in the future. Here, we only need to remember that the premise of chang animation execution is that the ViewHolder before and after the animation is different.
Then, let’s look at step two.
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if(holder ! = null) {if(! validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrapif 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; }}}Copy the code
This step is easy to understand. Obtain ViewHolder from mAttachedScrap, mHiddenViews, and mCachedViews respectively. If the ViewHolder is invalid, we need to clean it up and put it back into the cache. The corresponding cache is mCacheViews and RecyclerViewPool. RecycleViewHolderInternal method is the method of recycling ViewHolder later recovery related logic will focus on analysis the method, here is not to pursue.
(2). Obtain the ViewHolder by viewType
In the previous section, we examined the method of obtaining a ViewHolder by Position. Here we examine the second method –ViewType. But here, I first make a simple summary of the previous way, RecyclerView through Position to obtain ViewHolder, there is no need to judge whether the ViewType is legal, because if you can obtain ViewHolder through Position, The ViewType itself corresponds correctly.
Here the ViewHolder representation is retrieved by ViewType, at which point the Position cached by the ViewHolder is invalid. The process of getting a ViewHolder in ViewType is divided into 3 steps:
- if
Adapter
thehasStableIds
Method returns true and is preferredViewType
andid
Two conditions to find. If not, proceed to step 2.- if
Adapter
thehasStableIds
The method returns false, in which case the first value of theViewCacheExtension
Inside. If you haven’t found it yet, you’ll end up inRecyclerViewPool
Inside to get the ViewHolder.- If none of the above reuse steps are found appropriate
ViewHolder
, will be called at lastAdapter
theonCreateViewHolder
Method to create a new oneViewHolder
.
Here, we need to note that steps 1 and 2 above have the prerequisite that both must compare viewTypes. Next, I’ll briefly examine each step through the code.
A. Find the ViewHolder by id
By id to find suitable ViewHolder is mainly done by call getScrapOrCachedViewForId method, we simply look at the code:
// 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; }}Copy the code
While getScrapOrCachedViewForId method itself has no necessary, is respectively from mAttachedScrap and looking for the right ViewHolder mCachedViews array.
B. Obtain the ViewHolder from RecyclerViewPool
ViewCacheExtension exists in very rare cases, so I won’t expand it here for the sake of simplicity (actually I don’t understand it either!). So, here, we directly look at the RecyclerViewPool method.
Here, we need to understand the array structure of RecyclerViewPool. We simply analyze the RecyclerViewPool class.
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
Copy the code
Inside the RecyclerViewPool, SparseArray is used to store ViewHolder arrays corresponding to each ViewType, where the maximum size of each array is 5. Isn’t this data structure very simple?
Simple understanding of the RecyclerViewPool data structure, next we look at the reuse of the relevant code:
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); }}}Copy the code
Trust me, this code doesn’t need me to analyze it. It’s very simple.
C. Create a ViewHolder by calling the onCreateViewHolder method of the Adapter
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"); }}Copy the code
The main purpose of the above code is to create a ViewHolder by calling the createViewHolder method of the Adapter, simply counting the time at which a ViewHolder was created.
So much for our understanding of reuse mechanisms. The RecyclerView reuse mechanism is not complicated at all, and I think one of the things that makes people shy away is that we don’t know why we’re doing it, and if we know why we’re doing it, it’s all taken for granted.
Analysis of RecyclerView reuse part, next, we will analyze the recycling part.
3. The recycling
Recycling is very important within RecyclerView reuse mechanism. First of all, if there is a process of reuse, there must be a process of recycling; Secondly, understand the reuse and recycling two processes at the same time, which can help us understand the working principle of RecyclerView in macro; Finally, it is helpful to understand when RecyclerView recycles ViewHolder.
In fact, the recycling mechanism is not so difficult as imagined, this paper intends to analyze the recycling process of RecyclerView from several aspects.
- Scrap array
- MCacheViews array
- MHiddenViews array
- RecyclerViewPool array
Next, we will analyze them one by one.
(1). Scrap array
Recycle ViewHolder into Scrap. We have already analyzed the Recycler’s scrapView method. Let’s see where scrapView is called. There are two places:
- in
getScrapOrHiddenOrCachedHolderForPosition
Method, if frommHiddenViews
To obtain aViewHolder
I would have put this firstViewHolder
frommHiddenViews
Remove from the array and callRecycler
thescrapView
Method takes thisViewHolder
Into thescrap
Array, and tagFLAG_RETURNED_FROM_SCRAP
andFLAG_BOUNCED_FROM_HIDDEN_LIST
Two flag.- in
LayoutManager
The inside of thescrapOrRecycleView
Method is also calledRecycler
thescrapView
Methods. There are two cases where this happens: 1. It is called manuallyLayoutManager
Related methods; 2.RecyclerView
A layout is made (calledrequestLayout
Methods)
(2). MCacheViews array
The mCacheViews array serves as a level 2 cache, with more reclaimed paths than level 1 cache. About mCacheViews array, the emphasis is on Recycler recycleViewHolderInternal method. I have divided the collection paths of mCacheViews array into three categories. Let’s take a look:
- Recycle out in the relayout. This happens mainly in calls
Adapter
thenotifyDataSetChange
Method, and at this timeAdapter
thehasStableIds
Method returns false. You can see it here. WhynotifyDataSetChange
Why is the method so inefficient, but also know why rewritehasStableIds
Methods can improve efficiency. becausenotifyDataSetChange
Approach makesRecyclerView
It recyclesViewHolder
In the second level cache, the efficiency is naturally lower.- When you reuse it, you get it from level 1 cache
ViewHolder
But now thisViewHolder
It is removed from the level 1 cache if the Position is invalid and does not match the ViewTypeViewHolder
, from add tomCacheViews
inside- When calling
removeAnimatingView
Method if the currentViewHolder
Labeled remove, will callrecycleViewHolderInternal
Method to retrieve the correspondingViewHolder
. callremoveAnimatingView
The timing of the method represents the currentItemAnimator
It’s done.
(3). MHiddenViews array
The condition for a ViewHolder to recycle into the mHiddenView array is relatively simple. If the current operation supports animation, the addAnimatingView method of RecyclerView will be called. And in this method we’re going to add the View that we’re animating to the mHiddenView array. This is usually done during animation because mHiddenViews only has elements during animation.
(4). RecyclerViewPool
RecyclerViewPool with mCacheViews, by recycleViewHolderInternal ways to recycle, so the scene with mCacheViews about, only when not satisfied in the mCacheViews, Will put into RecyclerViewPool inside.
(5). Why does hasStableIds returning true improve efficiency?
Once you understand the reuse and recycling mechanism of RecyclerView, the problem becomes simple. I explain the reasons from two aspects.
A. Reuse aspect
Let’s start by looking at how reuse shows that hasStableIds can improve efficiency. Let’s look at the code:
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if(holder ! = null) { // update position holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache =true; }}Copy the code
If the Adapter hasStableIds method returns true when attempting to retrieve a ViewHolder using Position, the ViewHolder will be retrieved from the level 1 or level 2 cache first. Instead of going directly to RecyclerViewPool. From here, we can see that the hasStableIds approach improves efficiency in terms of reuse.
B. Recycling
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); }else{ detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }}Copy the code
From the above code, we can see that if the hasStableIds method returns true, all of the collection will go into the Scrap array. So this is exactly what we saw before.
It’s easy to see why the hasStableIds method returns true to improve efficiency.
4. To summarize
RecyclerView recycling and reuse mechanism is almost analyzed here. So here’s a little summary.
- in
RecyclerView
There are 4 levels of internal cache, the meaning of each level of cache is not the same, and the priority of reuse is also from top to bottom, their recycling is not the same.mHideenViews
The web exists to solve the problem of reuse during animation.ViewHolder
There are a number of internal flags, but before you understand the recycling and reuse mechanisms, it is a good idea to use themViewHolder
Clear the flag of.
Finally, a picture is used to conclude the introduction of this article.
RecyclerView