preface
RecyclerView source code more than 10,000 lines, want to read all learn to be very troublesome, interested can go to see, this article focuses on how to RecyclerView step by step each ItemView display to the screen, and then analysis in the display and sliding process, How cache reuse improves overall performance.
RecyclerView is also a custom control in essence, so we can analyze its onMeasure -> onLayout -> onDraw these three methods to further study the route.
Analysis of drawing process
onMeasure
@Override
protected void onMeasure(int widthSpec, int heightSpec) {...if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
//mLayout (incoming LayoutManager) onMeasure method to measure the width and height of RecyclerView.
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// Return true if the width and height of RecyclerView is match_parent or a specific value
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
// Animation related
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
// Set RecyclerView width and height to wrAP_content
// Measure the size of RecyclerView sub-view, and finally determine the actual width and height of RecyclerView.dispatchLayoutStep2(); . }}Copy the code
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
Copy the code
We call the dispatchLayout() method, and we look inside
dispatchLayout
void dispatchLayout(a) {... mState.mIsMeasuring =false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
// Measure the subview
dispatchLayoutStep2();
} else if(mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) { mLayout.setExactMeasureSpecsFrom(this);
// Measure the subview
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
// Trigger animation
dispatchLayoutStep3();
}
Copy the code
If the dispatchLayoutStep2() method is not executed to measure the child View in the onMeasure phase, it will be executed again in the onLayout phase.
dispatchLayoutStep2
// In this step, we actually lay out the view of the final state.
// Run this step multiple times if necessary (for example, measure).
private void dispatchLayoutStep2(a) {...// Step 2: Run layout
mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); . }public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
Copy the code
The core logic is to call the onLayoutChildren method of mLayout. One of this method in RecyclerView. LayoutManager empty methods, main effect is measured RecyclerView inside the size of the View, and to determine their position. LinearLayoutManager, GridLayoutManager, and StaggeredLayoutManager all duplicate this method and implement different layouts.
LinearLayoutManager.onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {...// Call the fill method to complete the measurement layout of the sub-view;
fill(recycler, mLayoutState, state, false); . }/ / the key
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {...while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
// The sub-view measures the actual implementation of the layout, and needs to recalculate the remainingSpace after each execution.layoutChunk(recycler, state, layoutState, layoutChunkResult); .if(! layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList ! =null| |! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed;// We keep a separate free space because Mavaailable is important for recycling
// After each loop, subtract the size spent from remainingSpaceremainingSpace -= layoutChunkResult.mConsumed; }...return start - layoutState.mAvailable;
}
Copy the code
- Call fill method in onLayoutChildren to complete the measurement layout of the child View;
- The fill method uses a while loop to determine if there is enough space left to draw a complete child View;
- The layoutChunk method is the real implementation of the sub-view measurement layout, and the remainingSpace needs to be recalculated after each execution.
layoutChunk
LayoutChunk is a very core method, this method is executed once to fill an ItemView to RecyclerView, part of the code as follows:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// Remove sub-ItemView from the Recycler.View view = layoutState.next(recycler); . RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();// Then add the subItemView to RecyclerView by calling addView or addDisappearingView.
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0); }}else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0); }}// Measure the width and height of the added subitemView in RecyclerView.
measureChildWithMargins(view, 0.0); .// Set Decoration, Margins and other options to the location of the sub-ItemView.layoutDecoratedWithMargins(view, left, top, right, bottom); . }Copy the code
onDraw
Once the measurement and layout are complete, the final drawing operation is left.
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState); }}Copy the code
If an ItemDecoration is added, the onDraw method of all decorations is looped and displayed. All subItemViews are recursively displayed on the screen using the Android rendering mechanism by calling the draw method of the subItemView.
Draw process summary
RecyclerView will entrust the work of onMeasure and layout onLayout to LayoutManager. Different LayoutManagers will display different styles of layout, which is a strategic mode. The diagram below:
Cache reuse can be Recycler
Cache reuse is another important mechanism in RecyclerView. This mechanism mainly implements cache and reuse of ViewHolder.
Recyer is an internal RecyclerView class. It is used to cache ViewHolder inside the screen and ViewHolder outside the screen as follows:
public final class Recycler {
// ViewHolder list not separated from RecyclerView
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// List ViewHolder separated from RecyclerView
ArrayList<ViewHolder> mChangedScrap = null;
//ViewHolder cache list
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
/ / ViewHolder buffer pool
RecycledViewPool mRecyclerPool;
// The developer can control the help class cached by ViewHolder
private ViewCacheExtension mViewCacheExtension;
// The number of caches is 2 by default
static final int DEFAULT_CACHE_SIZE = 2;
}
Copy the code
Recycler caching is done through the Recycler containers shown in the picture above. Recycler caching is classified into four levels according to access priority from top to bottom, as follows:
- Level 1 cache mAttachedScrap&mChangedScrap
- The second level cache mCachedViews
- The third level of cache ViewCacheExtension
- Level 4 Cache RecycledViewPool
Various levels of caching
RecyclerView divides the cache into so many blocks in order to distinguish some functions and correspond to different use scenarios respectively.
Level 1 cache mAttachedScrap&mChangedScrap
Are two ArrayLists named Scrap, which are mainly used to cache ViewHolder inside the screen. Why would an on-screen ViewHolder need to be cached?
If you’ve done App development, you’re probably familiar with the ability to refresh the list by pulling it down. When the refresh is triggered, you just need to rebind the new data from the ViewHolder, These ViewHolder are stored in mAttachedScrap and mChangedScrap. In fact, when we call the RV notifyXXX method, we populate the two lists, caching the old ViewHolder.
The second level cache mCachedViews
It is used to cache ViewHolder that is removed off-screen. By default, the cache number is two, but you can change the size of the cache by using the setViewCacheSize method. If mCachedViews is full, the old ViewHolder is discarded and a new ViewHolder is added according to a FIFO(first-in, first-out) rule.
In general, ViewHolder that have just been removed from the screen may be used right away, so RecyclerView does not set it to invalid immediately, but stores it in the cache. However, you cannot count all viewholders that remove the screen as valid viewholders, so the default capacity is only two.
The third level of cache ViewCacheExtension
This is an abstract class reserved for developers by RecyclerView, where there is only one abstract method.
public abstract static class ViewCacheExtension {
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
int type);
}
Copy the code
Developers can through inheritance ViewCacheExtension and autotype abstract methods getViewForPositionAndType to achieve their own caching mechanism. We generally do not implement caching logic ourselves and do not recommend adding caching logic ourselves, because this class has a high threshold of usage (please ignore the awesome ones).
Level 4 Cache RecycledViewPool
RecycledViewPool is also used to cache off-screen ViewHolder. When the number of mCachedViews is full (default: 2), ViewHolder discarded from mCachedViews will be cached in RecycledViewPool. When the ViewHolder is cached into the RecycledViewPool, the internal data will be cleaned up. Therefore, a ViewHolder taken out of the RecycledViewPool needs to call onBindViewHolder again to bind data. This is the same as the original ListView using ViewHolder to reuse convertView, so RV is a perfect inheritance of ListView advantages.
RecycledViewPool also has an important function, which has the following official explanation:
RecycledViewPool allows you to share views across multiple RecyclerViews. If you want to cross RecyclerViews recycling view, please create RecycledViewPool instance and use RecyclerView. SetRecycledViewPool (RecycledViewPool). If you don’t, RecyclerView will automatically create a pool for itself.
It can be seen that multiple RecycledViews can share a RecycledViewPool, which will have significant optimization effect on multi-tab interface. Note that RecycledViewPool is used to obtain ViewHolder according to type. The default maximum cache for each type is 5. Therefore, when multiple recyclerViews share RecycledViewPool, it is necessary to ensure that the shared RecyclerView uses the same Adapter or view type is not conflicting.
How does RecyclerView get a ViewHolder from the cache
In the onLayout stage above, it was introduced that the layoutChunk method was used to call the layoutstate. next method to get a subItemView and then add it to RecyclerView.
LayoutState. Next:
LinearLayoutManager.LayoutState.next
View next(RecyclerView.Recycler recycler) {
if(mScrapList ! =null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
// continue with getViewForPosition(mCurrentPosition);
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
// continue with getViewForPosition
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
Copy the code
It can be seen that the final call ViewHolder tryGetViewHolderForPositionByDeadline method to find the appropriate position.
tryGetViewHolderForPositionByDeadline
In this method it looks up from the level 4 cache described above:
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {... ViewHolder holder =null;
// 1) Return position view from Scrap and mCachedViews by position.
if (holder == null) {
// Returns the location view from Scrap and mCachedViews by location.
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
// Get the data type of this position (subview copy method)
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Select scrap and cache by ID
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
}
// Get the ViewHolder from ViewCacheExtension
if (holder == null&& mViewCacheExtension ! =null) {
// Returns views bound to a given location, or NULL if there are no reusable views
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
}
//3) Obtain ViewHolder from RecycledViewPool
if (holder == null) {
//4) Obtain ViewHolder from RecycledViewPool
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
//5) If no ViewHolder is found in each level of the cache, a new ViewHolder is created using the createViewHolder method in the Adapter.
holder = mAdapter.createViewHolder(RecyclerView.this, type); }}...return holder;
}
Copy the code
If no corresponding ViewHolder is found in each level of the cache in code 1-4, a new ViewHolder is created using the createViewHolder method in the Adapter.
When to cache the ViewHolder
Let’s take a look at the scenario where the ViewHolder is stored in each level of cache.
The first layout
When setLayoutManager and setAdapter are called, the RecyclerView goes through the first layout and is displayed on the screen, without any ViewHolder cache. All ViewHolder are created by createViewHolder.
Refresh the list
After the new data is obtained through gesture drop-down refresh, notifyXXX method will be called to notify RecyclerView data changes.
RecyclerView will first save all ViewHolder in the screen in Scrap.
When caching is complete, ViewHolder of position can be retrieved from the cache through Recycler.
The refreshed data is then set to these ViewHolder, and the new ViewHolder is drawn to RecyclerView.
RecyclerView code is extremely large, understand the source code of RecyclerView, help us to quickly locate the cause of the problem, expand the function of RecyclerView, improve the ability to analyze the performance of RecyclerView, interested can go to their own overestimation.
That’s all the content of this article, I hope to help you.