RecyclerView memory performance is superior, thanks to its unique cache mechanism, the last analysis of “how to reuse entries from the cache?” , this article continues to explore “Which entries are recycled?” by walking through the source code.
This is the second article in the RecyclerView cache mechanism series. The contents of the series are as follows:
RecyclerView caching mechanism | how to reuse table?
What RecyclerView caching mechanism | recycling?
RecyclerView caching mechanism | recycling where?
RecyclerView caching mechanism | scrap the view of life cycle
RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache
RecyclerView interview question | what item in the table below is recycled to the cache pool?
If you want to go straight to the conclusion, go to the end of chapter 4 (you’ll regret it, it’s even better).
The previous article talked about “where to get recycled entries”. This article will analyze “which entries are recycled?” based on the actual recycling scenario. .
Recycling scenario
The most obvious of the many recycling scenarios is “entries that are removed from the screen while scrolling through the list are recycled.” Rolling is a MotionEvent ACTION_MOVE events trigger, as RecyclerView. OnTouchEvent () to look for the timing of the “recycling list items” :
public class RecyclerView extends ViewGroup implements ScrollingView.NestedScrollingChild2 {
@Override
public boolean onTouchEvent(MotionEvent e) {...case MotionEvent.ACTION_MOVE: {
...
// Inner scroll
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true); }... }}break; . }}Copy the code
After removing a lot of displacement assignment logic, a function that handles scrolling appears:
public class RecyclerView extends ViewGroup implements ScrollingView.NestedScrollingChild2 {... LayoutManager mLayout;// Handle the scrolling LayoutManager.boolean scrollByInternal(int x, int y, MotionEvent ev) {...if(mAdapter ! =null) {...if(x ! =0) { // Scroll horizontally
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if(y ! =0) { // Vertical scrollconsumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); unconsumedY = y - consumedY; }... }... }Copy the code
RecyclerView delegates scrolling to LayoutManager:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler.RecyclerView.SmoothScroller.ScrollVectorProvider {
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {...// Update LayoutState (this function is critical for "which entries are recycled?", mentioned later)
updateLayoutState(layoutDirection, absDy, true, state);
// Populate the list with new entries while scrolling
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); .returnscrolled; }... }Copy the code
Further down the call chain, we find a function linearLayOutManager.fill () that we introduced in the previous article, which keeps filling entries as the list rolls.
The last one only focused on the filling logic, and there is also the recycling logic:
public class LinearLayoutManager extends RecyclerView.LayoutManager {
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {...int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// Loop over new entries to fill until no space is filled
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
// Populate the new entrylayoutChunk(recycler, state, layoutState, layoutChunkResult); .if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) {// Append to the current scroll offset the pixels added by the new entry insertion (this is key for "which entries are recycled?")layoutState.mScrollingOffset += layoutChunkResult.mConsumed; .// Reclaim the entryrecycleByLayoutState(recycler, layoutState); }... }...returnstart - layoutState.mAvailable; }}Copy the code
A function that retrieves and retrieves new entries for padding, like a scrolling list that inserts and removes entries at the same time, moves to a function that retrieves and retrieves entries:
public class LinearLayoutManager extends RecyclerView.LayoutManager {...private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if(! layoutState.mRecycle || layoutState.mInfinite) {return;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
// Retrieve from the list header
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
// Retrieve from the end of the listrecycleViewsFromStart(recycler, layoutState.mScrollingOffset); }}.../** * Reclaim entries that are rolled out of the screen when scrolling to the end of the list@paramDt (this parameter is used to detect entries that roll off the screen) */
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
final int limit = scrollingOffset - noRecycleSpace;
// Walk through the LinearLayoutManager from scratch to find the entries that should be recycled
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// If the lower boundary of the entry > limit specifies the threshold
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// Reclaim entries with indexes 0 to i-1
recycleChildren(recycler, 0, i);
return; }}}... }Copy the code
Recycle of RecyclerView can be divided into two directions: 1. Recycle from the list header 2. Recycle from the end of the list.
Take “recycle from the list header” as the research object to analyze how RecyclerView determines “which entries should be recycled?” . (The scenario for “Reclaim entries from the head of the list” is: Swipe up, scroll down the list, insert new entries to the end of the list one by one, and reclaim entries in the head of the list one by one.)
Which entries are reclaimed
To answer this question, just the code had been in recycleChildren (recycler, 0 and I) outside judgment logic is the key: mOrientationHelper. GetDecoratedEnd (child) > limit.
The mOrientationHelper. GetDecoratedEnd (child) code is as follows:
// Mask the direction of the abstract interface, used to reduce the direction if-else
public abstract class OrientationHelper {
// Get the coordinates of the current entry relative to the list header
public abstract int getDecoratedEnd(View view);
// A vertical layout implementation of this interface
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)view.getLayoutParams();
returnmLayoutManager.getDecoratedBottom(view) + params.bottomMargin; }}Copy the code
MOrientationHelper. GetDecoratedEnd (child) said that the current list of the end of the table entries relative to the coordinates of the head, OrientationHelper this abstraction layer shielding the direction of the list, So this sentence can be translated as “the vertical coordinate of the bottom of the current entry relative to the top of the list” in vertical lists.
Judge conditions mOrientationHelper. GetDecoratedEnd (child) > limit of limit is what mean?
In a vertical list, “bottom ordinate of an entry > some value” means that an entry is below a line, meaning that limit is an invisible line in the list, and all entries above that line should be reclaimed.
So how is this line calculated?
public class LinearLayoutManager extends RecyclerView.LayoutManager {
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
final intlimit = scrollingOffset - noRecycleSpace; . }}Copy the code
Limit value is determined by two variables, including noRecycleSpace a value of 0 (this is breakpoint told me, the detailed process can walk RecyclerView animation principle | change the posture to see the source code (pre – layout))
The value of scrollingOffset is passed in externally:
public class LinearLayoutManager {
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
intscrollingOffset = layoutState.mScrollingOffset; . recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace); }}Copy the code
Problem is converted into a layoutState. MScrollingOffset’s value is decided by what? Where it is assigned in global search:
public class LinearLayoutManager {
private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {...int scrollingOffset;
// Get the entry view at the end
final View child = getChildClosestToEnd();
// Calculate the maximum number of pixels the list can scroll without filling it with new entriesscrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding(); . mLayoutState.mScrollingOffset = scrollingOffset; }}Copy the code
UpdateLayoutState () method to obtain the table at the end of the list view, and through the mOrientationHelper. GetDecoratedEnd (child) calculated the distance between the table at the bottom to the top of the list item, and then minus the length of the list. This difference can be interpreted as the maximum number of pixels the list can scroll without filling it with new entries. Slightly abstract, as shown below:
In the figure, the blue border represents the list and the gray rectangle represents the entry.
LayoutManager only loads visible entries. In the figure, entry 6 is half out of the screen, so it is loaded into the list, whereas entry 7 is completely invisible, so it is not loaded. This case, if not continue to fill the table items in a list of 7, the list of most sliding distance is half the distance of the table item 6, list items in the code that is mLayoutState mScrollingOffset values.
If you slide the list very slowly and only slide “half of entry 6” away (that is, entry 7 doesn’t get a chance to show). In this ideal scenario the value of limit = half the length of entry 6. The limit line should be in the following position:
Take a look at the code for retrieving the entry:
public class LinearLayoutManager {
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
final int limit = scrollingOffset - noRecycleSpace;
// Walk through the LinearLayoutManager from scratch to find the entries that should be recycled
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// If the lower boundary of the entry > limit specifies the threshold
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// Reclaim entries with indexes 0 to i-1
recycleChildren(recycler, 0, i);
return; }}}}Copy the code
The recycling logic iterates through the LinearLayoutManager from the beginning. When iterating to entry 1, it finds that its lower boundary is > limit, so the recycling entry index interval is 0 to 0, that is, no entries are recycled. (Consider that entry 1 has not been completely removed from the screen).
What happens if the sliding speed and distance are higher?
The method updateLayoutState() that calculates the limit value is called in scrollBy() :
public class LinearLayoutManager {
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {...// Pass the absolute value of the roll distance to updateLayoutState()
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state); . }private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {...// Calculate the maximum number of pixels the list can scroll without filling it with new entriesscrollingOffset = mOrientationHelper.getDecoratedEnd(child)- mOrientationHelper.getEndAfterPadding(); .// Store the extra space needed for scrolling in mlayoutstate.mavailablemLayoutState.mAvailable = requiredSpace; mLayoutState.mScrollingOffset = scrollingOffset; . }}Copy the code
At this point, two important value is stored in the mLayoutState. MScrollingOffset and mLayoutState mAvailable, respectively is “not populate the new table into the list of cases, the list can roll how many pixels”, and “rolling total pixels.
SrollBy () fills the entry immediately after calling updateLayoutState() to store the two important values:
public class LinearLayoutManager {
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {...final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); . }}Copy the code
Fill the table item
Where fill() is the method of filling the list with entries:
public class LinearLayoutManager {
// Fill the entry according to the remaining space
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {...// Calculate free space = free space + extra space (=0)
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// Continue to fill more entries when the remaining space is greater than 0
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
// Populate a single entry
layoutChunk(recycler, state, layoutState, layoutChunkResult)
...
// Subtract the occupied pixel value of the new entry from the remaining spacelayoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; . }}}Copy the code
Populating a table entry is a while loop that ends with the condition “is the list free space > 0?”, and each time the loop calls layoutChunk() to populate a single table entry into the list:
public class LinearLayoutManager {
// Populate a single entry
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
// 1. Obtain the next entry view to be populated
View view = layoutState.next(recycler);
// 2. Make the entry a child of RecyclerViewaddView(view); .// 3. Measure table item view (RecyclerView inner margin and table item decoration taken into account)
measureChildWithMargins(view, 0.0);
// Get the number of pixels consumed to populate the entry viewresult.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); .// 4. Layout entrieslayoutDecoratedWithMargins(view, left, top, right, bottom); }}Copy the code
LayoutChunk () from the first buffer pool for the next should be populated table item view (detailed analysis about the reuse to the RecyclerView caching mechanism | how to reuse table?) .
Then call addView() to create a RecyclerView child of the RecyclerView. Call chain as follows:
public class RecyclerView {
ChildHelper mChildHelper;
public abstract static class LayoutManager {
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
addViewInt(child, index, false);
}
private void addViewInt(View child, int index, boolean disappearing) {... mChildHelper.attachViewToParent(child, index, child.getLayoutParams(),false); . }}}class ChildHelper {
final Callback mCallback;
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,boolean hidden) {... mCallback.attachViewToParent(child, offset, layoutParams); }}Copy the code
The chain of calls goes from RecyclerView to LayoutManager to ChildHelper and back to RecyclerView:
public class RecyclerView {
ChildHelper mChildHelper;
private void initChildrenHelper(a) {
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
@Override
public void attachViewToParent(View child, int index,ViewGroup.LayoutParams layoutParams) {... RecyclerView.this.attachViewToParent(child, index, layoutParams); }... }}}Copy the code
AddView () the final foothold is ViewGroup attachViewToParent () :
public abstract class ViewGroup {
protected void attachViewToParent(View child, int index, LayoutParams params) {
child.mLayoutParams = params;
if (index < 0) {
index = mChildrenCount;
}
// Add the subview to the array
addInArray(child, index);
// The child view is associated with the parent
child.mParent = this;
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
& ~PFLAG_DRAWING_CACHE_VALID)
| PFLAG_DRAWN | PFLAG_INVALIDATED;
this.mPrivateFlags |= PFLAG_INVALIDATED;
if(child.hasFocus()) { requestChildFocus(child, child.findFocus()); } dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown()); notifySubtreeAccessibilityStateChangedIfNeeded(); }}Copy the code
AttachViewToParent () contains two of the most iconic actions for adding a child view: 1. Add a subview to an array. 2. Associate a subview with its parent.
After making the table into a RecyclerView subview, it was measured:
public class LinearLayoutManager {
// Populate a single entry
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
// 1. Obtain the next entry view to be populated
View view = layoutState.next(recycler);
// 2. Make the entry a child of RecyclerViewaddView(view); .// 3. Measure table item view (RecyclerView inner margin and table item decoration taken into account)
measureChildWithMargins(view, 0.0);
// Get the number of pixels consumed to populate the entry viewresult.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); .// 4. Layout entrieslayoutDecoratedWithMargins(view, left, top, right, bottom); }}Copy the code
After measurement, with the size of the view, you can know how fill the table item will consume pixel values, the value stored in the LayoutChunkResult. MConsumed.
After the size, you can layout the table, that is, determine the table item up and down around four points relative to RecyclerView position:
public class RecyclerView {
public abstract static class LayoutManager {
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
// Locate the entrychild.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin); }}}Copy the code
Call control layout () method is to control positioning, about positioning child controls detailed introduction to the Android custom controls | View drawing principle (pictures?) .
After you’ve filled in an entry, you deduct the space it takes from the remainingSpace (so the while loop ends)
public class LinearLayoutManager {
// Fill the entry according to the remaining space
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {...// Calculate free space = free space + extra space (=0)
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// Continue to fill more entries when the remaining space is greater than 0
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
// Populate a single entry
layoutChunk(recycler, state, layoutState, layoutChunkResult)
...
// Subtract the occupied pixel value of the new entry from the remaining spacelayoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; .// Append the pixel value of the new entry to limitlayoutState.mScrollingOffset += layoutChunkResult.mConsumed; .// Reclaim the entry based on the current staterecycleByLayoutState(recycler, layoutState); }}}}Copy the code
LayoutState. MScrollingOffset will add a new item table occupied the pixel value, namely its value in increasing (limit contact line is down).
At the end of a while loop, the entry is retrieved based on the current position of the limit invisible line:
public class LinearLayoutManager {
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {... ecycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace); }}private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
final int limit = scrollingOffset - noRecycleSpace;
final int childCount = getChildCount();
// Iterate over the entry
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
When the bottom of an entry is behind the limit invisible line, reclaim all entries above it
if (mOrientationHelper.getDecoratedStart(child) > limit || mOrientationHelper.getTransformedStartWithDecoration(child) > limit) {
recycleChildren(recycler, 0, i);
return; }}}}Copy the code
Each time you populate the end of the list with an entry, the position of the limit invisible line moves the number of pixels occupied by the entry down, so that more entries in the head of the list are eligible for collection.
The analysis of the details about recycling, can walk RecyclerView caching mechanism | recycling where? .
The expected slide distance is passed to scrollBy(), which populates the list with the entries that are about to slide onto the screen and reclaims the entries that are about to slide off the screen into the cache pool. Finally, it compares the expected slide value and computes the size of the slide value, whichever is smaller:
public class LinearLayoutManager {
// The first parameter is the expected slip distance
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {...final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
// Calculated scroll value
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
// Finally return the scroll value
final intscrolled = absDelta > consumed ? layoutDirection * consumed : delta; .returnscrolled; }}Copy the code
Look along the scrollBy() call chain:
public class LinearLayoutManager {
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
returnscrollBy(dy, recycler, state); }}public class RecyclerView {
void scrollStep(int dx, int dy, @Nullable int[] consumed) {...if(dy ! =0) { consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); }... }boolean scrollByInternal(int x, int y, MotionEvent ev) {... scrollStep(x, y, mReusableIntPair); . dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,TYPE_TOUCH, mReusableIntPair); . }}Copy the code
Only when executeddispatchNestedScroll()
It actually triggers scrolling, so RecyclerView calculates before scrolling, which entries are going to be on the screen, which ones are going to be off the screen, and fills them into the list or back into the cache. And the basis for these two things isLimit contact line
Finally, a picture is used to summarize the meaning of this line:
The value of limit represents the total distance of this scroll. (This is an ideal situation where the bottom of the newly inserted entry 7 overlaps the bottom of the list after scrolling)
The limit invisible line can be understood as: the current position of the invisible line will coincide with the top of the list after scrolling
Recommended reading
RecyclerView series article directory is as follows:
-
RecyclerView caching mechanism | how to reuse table?
-
What RecyclerView caching mechanism | recycling?
-
RecyclerView caching mechanism | recycling where?
-
RecyclerView caching mechanism | scrap the view of life cycle
-
Read the source code long knowledge better RecyclerView | click listener
-
Proxy mode application | every time for the new type RecyclerView is crazy
-
Better RecyclerView table sub control click listener
-
More efficient refresh RecyclerView | DiffUtil secondary packaging
-
Change an idea, super simple RecyclerView preloading
-
RecyclerView animation principle | change the posture to see the source code (pre – layout)
-
RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache
-
RecyclerView animation principle | how to store and use animation attribute values?
-
RecyclerView list of interview questions | scroll, how the list items are filled or recycled?
-
RecyclerView interview question | what item in the table below is recycled to the cache pool?
-
RecyclerView performance optimization | to halve load time table item (a)
-
RecyclerView performance optimization | to halve load time table item (2)
-
RecyclerView performance optimization | to halve load time table item (3)
-
How does RecyclerView roll? (a) | unlock reading source new posture
-
RecyclerView how to achieve the scrolling? (2) | Fling
-
RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?