-
概述
之前阅读RecyclerView的时候,大体了解到,和ListView不同,RecyclerView在复用View时并不是每次都会去调用adapter的onCreateViewHolder方法,它会根据在屏幕之外缓存一定数量的View实例,当滑动范围超出这些View的总长度时才会调用onCreateViewHolder去创建新的,而且在复用操作中,用到了像mChildHelper、mCachedViews、mScrapHeap等很多保存动作,这些的意义和作用是什么,值得我们仔细研究一下。
-
从滚动入手
因为保存操作是要显示然后退出屏幕的时候才会执行,这个切入点应该是滚动操作:
case MotionEvent.ACTION_MOVE: { final int index = e.findPointerIndex(mScrollPointerId); if (index < 0) { Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); return false; } final int x = (int) (e.getX(index) + 0.5f); final int y = (int) (e.getY(index) + 0.5f); int dx = mLastTouchX - x; int dy = mLastTouchY - y; if (mScrollState != SCROLL_STATE_DRAGGING) { boolean startScroll = false; if (canScrollHorizontally) { if (dx > 0) { dx = Math.max(0, dx - mTouchSlop); } else { dx = Math.min(0, dx + mTouchSlop); } if (dx != 0) { startScroll = true; } } if (canScrollVertically) { if (dy > 0) { dy = Math.max(0, dy - mTouchSlop); } else { dy = Math.min(0, dy + mTouchSlop); } if (dy != 0) { startScroll = true; } } if (startScroll) { setScrollState(SCROLL_STATE_DRAGGING); } } if (mScrollState == SCROLL_STATE_DRAGGING) { mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; if (dispatchNestedPreScroll( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, mReusableIntPair, mScrollOffset, TYPE_TOUCH )) { dx -= mReusableIntPair[0]; dy -= mReusableIntPair[1]; // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; // Scroll has initiated, prevent parents from intercepting getParent().requestDisallowInterceptTouchEvent(true); } mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) { getParent().requestDisallowInterceptTouchEvent(true); } if (mGapWorker != null && (dx != 0 || dy != 0)) { mGapWorker.postFromTraversal(this, dx, dy); } } } break;如果产生了有效位移,startScroll会变成true,所以mScrollState就会变成SCROLL_STATE_DRAGGING,所以就会调用scrollByInternal方法:
boolean scrollByInternal(int x, int y, MotionEvent ev) { int unconsumedX = 0; int unconsumedY = 0; int consumedX = 0; int consumedY = 0; consumePendingUpdateOperations(); if (mAdapter != null) { mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; scrollStep(x, y, mReusableIntPair); consumedX = mReusableIntPair[0]; consumedY = mReusableIntPair[1]; unconsumedX = x - consumedX; unconsumedY = y - consumedY; } if (!mItemDecorations.isEmpty()) { invalidate(); } mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset, TYPE_TOUCH, mReusableIntPair); unconsumedX -= mReusableIntPair[0]; unconsumedY -= mReusableIntPair[1]; boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0; // Update the last touch co-ords, taking any scroll offset into account mLastTouchX -= mScrollOffset[0]; mLastTouchY -= mScrollOffset[1]; mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) { pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY); } considerReleasingGlowsOnScroll(x, y); } if (consumedX != 0 || consumedY != 0) { dispatchOnScrolled(consumedX, consumedY); } if (!awakenScrollBars()) { invalidate(); } return consumedNestedScroll || consumedX != 0 || consumedY != 0; }这里调用了scrollStep方法:
void scrollStep(int dx, int dy, @Nullable int[] consumed) { startInterceptRequestLayout(); onEnterLayoutOrScroll(); TraceCompat.beginSection(TRACE_SCROLL_TAG); fillRemainingScrollValues(mState); int consumedX = 0; int consumedY = 0; if (dx != 0) { consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); } if (dy != 0) { consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); } TraceCompat.endSection(); repositionShadowingViews(); onExitLayoutOrScroll(); stopInterceptRequestLayout(false); if (consumed != null) { consumed[0] = consumedX; consumed[1] = consumedY; } }这里根据layout方向调用mLayout的不同方法,以纵向为例,看一下LinearLayoutManager的scrollVerticallyBy方法:
@Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (mOrientation == HORIZONTAL) { return 0; } return scrollBy(dy, recycler, state); }scrollBy方法如下:
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getChildCount() == 0 || delta == 0) { return 0; } ensureLayoutState(); mLayoutState.mRecycle = true; final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDelta = Math.abs(delta); updateLayoutState(layoutDirection, absDelta, true, state); final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); if (consumed < 0) { if (DEBUG) { Log.d(TAG, "Don't have any more elements to scroll"); } return 0; } final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta; mOrientationHelper.offsetChildren(-scrolled); if (DEBUG) { Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled); } mLayoutState.mLastScrollDelta = scrolled; return scrolled; }在updateLayoutState中会计算滚动方向末尾的View在屏幕外的距离(末尾指的是包括屏幕外保存的最后一个View实例,并不单指屏幕中未显示完全的最后一个):
// calculate how much we can scroll without adding new children (independent of layout) scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding(); mLayoutState.mScrollingOffset = scrollingOffset;mLayoutState.mAvailable是拖动的距离减去滚动方向末尾的View在屏幕外的距离(末尾指的是包括屏幕外保存的最后一个View实例,并不单指屏幕中未显示完全的最后一个):
mLayoutState.mAvailable -= scrollingOffset;这里的mAvailable得到的就是需要fill的增加区域的长度,也就是说View没显示出来的部分是不作为此次fill的区域的,因为之前已经存在。
fill方法里:
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;layoutState.mExtraFillSpace只有在快速滚动(比如fling或调用smoothScrollToPosition方法时)才会有效,避免麻烦这里不考虑,正常拖动的滑动这里是0,所以如果滑动距离大于屏幕外未显示出的View长度则remainingSpace大于0,否则就小于0。
接下来,通过while循环调用layoutChunk的时候有一个判断条件是remainingSpace > 0,可见,如果滑动距离小于未显示全部分则不需要add新的View。
回到scrollBy,consumed就等于原先末尾View未显示出来的部分的长度加上新添加的View的总长度(末尾指的是包括屏幕外保存的最后一个View实例,并不单指屏幕中未显示完全的最后一个)。
然后调用mOrientationHelper.offsetChildren(-scrolled)方法,这里的scrolled的计算方法是为了处理滑动到末端的情况,取反是因为要移动的是子View,所以方向是相反的。看一下mOrientationHelper的offsetChildren方法,纵向为例:
@Override public void offsetChildren(int amount) { mLayoutManager.offsetChildrenVertical(amount); }public void offsetChildrenVertical(@Px int dy) { if (mRecyclerView != null) { mRecyclerView.offsetChildrenVertical(dy); } }public void offsetChildrenVertical(@Px int dy) { final int childCount = mChildHelper.getChildCount(); for (int i = 0; i < childCount; i++) { mChildHelper.getChildAt(i).offsetTopAndBottom(dy); } }mChildHelper.getChildCount方法如下:
int getChildCount() { return mCallback.getChildCount() - mHiddenViews.size(); }这里的mCallback.getChildCount方法如下:
@Override public int getChildCount() { return RecyclerView.this.getChildCount(); }所以就是所有已attach到RecyclerView的View数量,减去mHiddenViews的数量,这个mHiddenViews存放的是在RecyclerView中attach的但是不显示的View,这个和动态删除有关(界面上删除的表项会存在这里)。这里调用offsetTopAndBottom方法让可见的子View移动:
public void offsetTopAndBottom(int offset) { if (offset != 0) { final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (isHardwareAccelerated()) { invalidateViewProperty(false, false); } else { final ViewParent p = mParent; if (p != null && mAttachInfo != null) { final Rect r = mAttachInfo.mTmpInvalRect; int minTop; int maxBottom; int yLoc; if (offset < 0) { minTop = mTop + offset; maxBottom = mBottom; yLoc = offset; } else { minTop = mTop; maxBottom = mBottom + offset; yLoc = 0; } r.set(0, yLoc, mRight - mLeft, maxBottom - minTop); p.invalidateChild(this, r); } } } else { invalidateViewProperty(false, false); } mTop += offset; mBottom += offset; mRenderNode.offsetTopAndBottom(offset); if (isHardwareAccelerated()) { invalidateViewProperty(false, false); invalidateParentIfNeededAndWasQuickRejected(); } else { if (!matrixIsIdentity) { invalidateViewProperty(false, true); } invalidateParentIfNeeded(); } notifySubtreeAccessibilityStateChangedIfNeeded(); } } -
recycleByLayoutState方法
到现在为止我们可以知道,滚动的时候View是如何移动的,还没看见回收是怎样做的。
在fill方法里,在前面layoutChunk之前有一段代码:
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); }/** * Used when LayoutState is constructed in a scrolling state. * It should be set the amount of scrolling we can make without creating a new view. * Settings this is required for efficient view recycling. */ int mScrollingOffset;mScrollingOffset是末尾Child距离RecyclerView内容底部的距离(末尾指的是包括屏幕外保存的最后一个View实例,并不单指屏幕中未显示完全的最后一个),不等于LayoutState.SCROLLING_OFFSET_NaN,所以会调用recycleByLayoutState方法:
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { if (!layoutState.mRecycle || layoutState.mInfinite) { return; } int scrollingOffset = layoutState.mScrollingOffset; int noRecycleSpace = layoutState.mNoRecycleSpace; if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace); } else { recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace); } }这里有两个变量scrollingOffset和noRecycleSpace,前者我们知道了,后者是什么?
calculateExtraLayoutSpace(state, mReusableIntPair); int extraForStart = Math.max(0, mReusableIntPair[0]); int extraForEnd = Math.max(0, mReusableIntPair[1]); boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END; mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart; mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;calculateExtraLayoutSpace最终调用内部getExtraLayoutSpace方法:
/** * <p>Returns the amount of extra space that should be laid out by LayoutManager.</p> * * <p>By default, {@link LinearLayoutManager} lays out 1 extra page * of items while smooth scrolling and 0 otherwise. You can override this method to implement * your custom layout pre-cache logic.</p> * @return The extra space that should be laid out (in pixels). * @deprecated Use {@link #calculateExtraLayoutSpace(RecyclerView.State, int[])} instead. */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated protected int getExtraLayoutSpace(RecyclerView.State state) { if (state.hasTargetScrollPosition()) { return mOrientationHelper.getTotalSpace(); } else { return 0; } }这里的hasTargetScrollPosition获取的只有在快速滚动时(fling或者smoothScrollToPosition时才会设置),正常滑动的时候为0,noRecycleSpace就是它的值,以纵向网上滑动为例,看recycleViewsFromEnd方法:
final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace; final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedStart(child) < limit || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { // stop here recycleChildren(recycler, childCount - 1, i); return; } }仔细分析一下这个算法可以知道,这个limit就是计算在屏幕之外保存的最外面一个View的最远端距离RecyclerView顶部的距离,这就是保存的最末端的界线,getChildCount方法获取的是所有当前已attch到RecyclerView中的(默认会包含一部分但未显示出来的)View,这里循环调用getChild方法就是依次获取已经attach的子View,判断如果超出保存边界了就recycle。
可见,RecyclerView在屏幕之外的上下方向各有一段用来保存View的距离,超过这个距离的View就会被移除,noRecycleSpace表示在快速滚动情况下保存的距离会额外加上RecyclerView的内容长度,也就是在快速滚动时会尽可能地保存更多View,正常拖动是0。比如往上滚动,上方从屏幕之外保存最大范围处开始比较View是否已超出最大范围,若超出则移除,下方从屏幕外进入的View则会在走layoutChunk流程时去判断是否创建新View。
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { if (startIndex == endIndex) { return; } if (DEBUG) { Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); } if (endIndex > startIndex) { for (int i = endIndex - 1; i >= startIndex; i--) { removeAndRecycleViewAt(i, recycler); } } else { for (int i = startIndex; i > endIndex; i--) { removeAndRecycleViewAt(i, recycler); } } }recycleChildren方法中,会遍历从所有View的第一个到要移除的View之间的所有View,执行:
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) { final View view = getChildAt(index); removeViewAt(index); recycler.recycleView(view); }再次调用getChildAt方法取得每一个要移除的View,然后移除,移除操作分两边,一边是从mBucket中移除,一边是调用mCallback移除,前者是隐藏的View,只是从记录中去除,因为隐藏的View实际上没有attach到RecyclerView上的,后者就是调用RecyclerView的removeViewAt方法把子View移除掉。
最后是回收操作:
public void recycleView(@NonNull View view) { ViewHolder holder = getChildViewHolderInt(view); if (holder.isTmpDetached()) { removeDetachedView(view, false); } if (holder.isScrap()) { holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); if (mItemAnimator != null && !holder.isRecyclable()) { mItemAnimator.endAnimation(holder); } }holder.unScrap方法内部会从mChangedScrap和mAttachedScrap中去除,因为此时View已经detach了。然后会调用recycleViewHolderInternal方法,这个方法里主要是尝试保存到mCachedViews中和从mViewInfoStore中移除,mCachedViews用来保存有效、未remove掉的、未更新的且在Adapter中有效位置的holder,mCachedViews默认只能存两个,超出的话最先保存的就会被替换,可以通过RecyclerView的setItemViewCacheSize进行设置。不符合mCachedViews存储条件的会调用addViewHolderToRecycledViewPool方法存放到RecycledViewPool中。
至此,滑动过程中超出保存范围的View如何保存就清楚了。
-
再看LayoutState.next方法
RecyclerView的一头在退出需要保存,另一头在进入就需要恢复,在RecyclerView的原理分析一文中整理过,这里结合滚动的进出再看一下:
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }这里会先判断mScrapList是不是空,这个集合是调用Recycler的getScrapList方法拿到的集合,里面就是mAttachedScrap的值,这个值是和pre-layout有关,pre-layout和动画相关,我们这里先不管它。
recycler.getViewForPosition最终会走到Recycler的tryGetViewHolderForPositionByDeadline方法,这个方法中会按照以下顺序去找,如果前一步没找到才往下一步查找:
- 需要更新的View集合-mChangedScrap;
- pre-layout时临时暂存的View集合-mAttachedScrap,只比较getPosition相同;
- 隐藏但未移除的View集合-mHiddenViews;
- 已移除的但是最新移除的View集合-mCachedViews;
- 根据Adapter的getViewId和getViewType比较相同在mAttachedScrap和mCachedViews中再查找一遍;
- 根据mViewCacheExtension的getViewForPositionAndType方法尝试获取,这个mViewCacheExtension是一个ViewCacheExtension实例,通过setViewCacheExtension方法设置,ViewCacheExtension是一个抽象类,只有一个getViewForPositionAndType方法,ViewCacheExtension的作用就是给开发者一个入口可以添加自己的缓存类,如果内存许可的情况下可以设置这个来改善性能;
- 根据getRecycledViewPool().getRecycledView(type)方法获取,RecycledViewPool里面有一个SparseArray<ScrapData>类型的变量mScrap,getRecycledView方法就是根据viewType从它里面尝试获取viewholder,每种类型都有一个ArrayList<ViewHolder>类型的变量mScrapHeap存储,这个集合默认容量是5个,可以通过RecycledViewPool的setMaxRecycledViews方法自定义;
- 上述缓存中都没有取到的话就调用Adapter的createViewHolder方法创建,需要更新的话还会调用Adapter的bindViewHolder方法重新绑定数据。
可见,在RecyclerView正在逐步显示的一端,根据mCurrentPosition(这个值是最后一个attachView的下标,注意不一定是最后一个显示的)找到最后一个attach的View的下一个位置的View,这个View如果之前没被缓存过则重新创建。
-
layoutChunk
next方法走完之后,回到layoutChunk方法会addView,不管addView还是addDisappearView方法最终都会调用addViewInt方法,里面有这么一段代码:
if (holder.wasReturnedFromScrap() || holder.isScrap()) { if (holder.isScrap()) { holder.unScrap(); } else { holder.clearReturnedFromScrapFlag(); } mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); if (DISPATCH_TEMP_DETACH) { ViewCompat.dispatchFinishTemporaryDetach(child); } } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child // ensure in correct position int currentIndex = mChildHelper.indexOfChild(child); if (index == -1) { index = mChildHelper.getChildCount(); } if (currentIndex == -1) { throw new IllegalStateException("Added View has RecyclerView as parent but" + " view is not a real child. Unfiltered index:" + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel()); } if (currentIndex != index) { mRecyclerView.mLayout.moveView(currentIndex, index); } } else { mChildHelper.addView(child, index, false); lp.mInsetsDirty = true; if (mSmoothScroller != null && mSmoothScroller.isRunning()) { mSmoothScroller.onChildAttachedToWindow(child); } }这么分三种情况,第一种是已经detach的重新attach,第二种是已经attach的不执行操作,最后一种是从未add过的执行addView添加,这分别对应着已经加载过但是因为滚动出缓存范围而detach的View、目前还处在attach范围内的View和从未展示过的、刚创建的新View。
-
总结
下面总结都是以纵向LinearLayoutManager为例。
- RecyclerView会默认保存屏幕外的七个ViewHolder实例,2个在mCacheViews中,5个在RecycledViewPool中,RecycledViewPool中有一个mScrap,按照viewtype保存的ScrapData,每个ScrapData中有一个mScrapHeap,mScrapHeap默认长度是5,所以每一种viewtype的ViewHolder会保存5个,上面说的7个只是算了单一种viewtype对应的ViewHolder数量,这些ViewHolder都是目前已经attach到RecyclerView中的,滚动时显示他们是不会走Adapter的onCreateViewHolder等方法的;
- mCacheViews保存的是最后不可见的ViewHolder,即刚刚退出的,mScrapHeap保存的是较早退出的;
- mCacheViews和mScrapHeap的保存容量都有API可以设置,这就为我们根据性能和内存自定义最佳配置提供了可塑性;
- mHiddenViews保存的是在attach范围内的但是不显示的ViewHolder,比如Adapter的数据没变化,只是删除某一项之后,这一项就会放在这里面;
- mChangedScrap保存的是发生了变化的ViewHolder,比如某一项数据更新了;
- 可以设置ViewCacheExtension来增加我们自己的ViewHolder缓存类,它会和系统的缓存类共同发挥作用,在有些内存充足但是对滚动渲染性能要求较高的设备中会有操作空间;
- mChildHelper封装了一些渲染数据变化和RecyclerView真实的渲染之间的映射处理逻辑,相当于一个业务provider;
- 滚动时,最开始会先调用recycleByLayoutState方法把滚出attach范围的View给detach掉,原理就是会计算滚出屏幕方向最末端的View的start(纵向就是top或者bottom)到RecyclerView顶部的距离(因为滚出RecyclerView就是不可见了),超出这个距离的就是需要detach的View,把它们detach,这就是滚出屏幕一端的操作;
- 接着是划入屏幕的一端,会尝试获取或创建下一个ViewHolder,这个ViewHolder不一是RecyclerView即将要显示出来的,如果滑入屏幕的一端有之前缓存过的ViewHolder的话那就是获取保存的最远端的那个,即会获取attach的View中在滑入屏幕的一端方向上最末端的ViewHolder;
- layoutState.mAvailable保存的是拖动的距离,拖动距离会减去当前屏幕最后一个显示的View的未显示部分的长度,这个值赋给remainingSpace,remainingSpace大于0的时候才会while调用layoutChunk方法,也就是说如果最后一个显示的View还有未显示的部分的话,会等到未显示的部分显示出来之后才可能走layoutChunk方法,这也就是我们在onCreateViewHolder处打断点的时候,如果创建新的ViewHolder的情况下,一定是滚动到最后一个显示View的末端的时候才会进到断点的原因;
- next方法中除了pre-layout之外会从mAttachViews中直接获取之外,其他的都是走tryGetViewHolderForPositionByDeadline方法去上述那些缓存区中获取ViewHolder的,获取不到就走onCreateViewHolder方法重新构建;
- 获取或创建ViewHolder之后回到layoutChunk之后addView或者addDisappearingView逻辑中最终都会走到addViewInt方法,这里会根据ViewHolder的类型进行不同的操作,如果是新构建的则调用ViewGroup的addView方法添加子View,如果是从detach了的集合中取的则执行attach操作(有一个scrapView方法可以把detach的ViewHolder放到ScrapHeap去,但是默认只有hidden的才会加,其他的detach的没有加进去),如果是已经attach的(不管目前还有没有显示)都不执行任何操作只判断index有效性;
- 最后取最后一个未显示出的子View的未显示部分的长度加上未显示的View显示出来的长度作为便宜总量,调用mOrientationHelper.offsetChildren(int)方法移动全部子View。
至此,结合RecyclerView的滚动分析它的回收复用就结束了。







网友评论