你了解recyclerView
如何实现的复用,要不要一起来探索一下,剥开recyclerView
神秘面纱?
1. 一次滑动
为了找到recyclerView
如何实现的复用,先从最常见的场景,手指滑动屏幕开始入手。
1.1. 从事件起点入手
手指接触屏幕滑动时,首先触发到onTouchEvet()
,在ACTION_MOVE
事件中调用到scrollByInternal()
,之后继续传递进入了scrollStep()
。
而在scrollStep()
中,将滚动直接委托分派给了LayoutManger
处理。
class RecyclerView {
LayoutManager mLayout;
...
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
} break;
}
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...
if (mAdapter != null) {
...
scrollStep(x, y, mReusableIntPair);
}
}
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
...
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
}
}
复制代码
1.2. 处理滚动
以纵向的LinearLayoutManager
,向上滑动为例。接着往下面走,看看究竟是怎么处理的。
首先LinearLayoutManager.scrollVerticallyBy()
中,判断是不是纵向的,然后向后传递到scrollBy()
。
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
复制代码
传递到scrollBy()
,终于开始处理滚动:
-
首先把
dy
分解成方向和偏移值,其中方向对应两个值,分别是LAYOUT_END
=1,LAYOUT_START
=-1。前面说过我们目前看的向上滑动,dy > 0
,对应LAYOUT_END
。 -
调用到
updateLayoutState()
,这个方法是给LayoutState
中的参数赋值,而LayoutState
就是如何进行填充的配置参数。 -
计算
consumed
,可以理解为计算出RecyclerView
支持滚动的距离,其中调用到的fill()
,就是控制表项的创建、回收、复用等的地方。 -
最后就是对
Children
做偏移,mOrientationHelper.offsetChildren()
最终会回传到RecyclerView
中对所有表项做位移。这里有个判断条件absDelta > consumed
,就是滑动的距离和RecyclerView
支持滚动的距离比较,实际的滚动距离,就是两者中较小的一个。
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
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);
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
}
复制代码
继续往后走,进入到updateLayoutState()
,计算的几个关键值,给LayoutState
中的参数赋值。
layoutDirection
:滚动的方向,前面计算的,直接赋值。scrollingOffset
:不添加新子项的情况下可滚动的距离。现在看的是向上滑动,这个值就是最底部的子项在屏幕外的高度。也就是最底部的子项的底部位置与RecyclerView
底部位置的差值。available
:需要填充的高度。滑动距离与scrollingOffset
的差值(可能是负值)。
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
...
mLayoutState.mLayoutDirection = layoutDirection;
boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
int scrollingOffset;
if (layoutToEnd) {
...
final View child = getChildClosestToEnd();
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
}
mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
mLayoutState.mAvailable -= scrollingOffset;
}
mLayoutState.mScrollingOffset = scrollingOffset;
}
复制代码
再回头进入到了fill()
,控制表项的创建、回收、复用等的地方。
在这里可以看到两个关键函数,recycleByLayoutState()
与layoutChunk()
,从名字上就可以看出对应的就是回收以及复用方法。
这里可以看到,在最开始先调用了一次回收方法,接着走进了一个while
循环,循环的条件就是有需要填充的高度,以及可以有足够的控件可以用于填充。而在循环中的流程可以分为三步:
- 添加下一个控件,
layoutChunk()
。 - 重新计算
available
。 - 回收,
recycleByLayoutState()
。
对比循环前以及循环中的回收,都对scrollingOffset
做了一定计算,回到前面对scrollingOffset
的定义,不添加新子项的情况下可滚动的距离。在这里需要更新一下理解,scrollingOffset
是循环中每一次添加表项的时候重新计算出来的实际滚动距离,也就是每一次添加表项时候,滑动的距离和支持滚动的距离两者的最小值。
最后,函数的返回值换算一下,就等于循环中添加的所有控件的高度和,也就是填充的高度。传递给外面计算支持滚动的距离。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state))
...
//step0
layoutChunk(recycler, state, layoutState, layoutChunkResult);
//step1
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
//step2
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
return start - layoutState.mAvailable;
}
复制代码
1.3. 回收
前面分析到在fill()
方法中,调用recycleByLayoutState()
方法实现表项的回收,进入其中探索一下。
recycleByLayoutState()
中根据滚动的方向,分发到不同的方法,前面说到分析的是向上滑动,对应的方法就是recycleViewsFromStart()
。这里也将前面所分析到的每一次的实际滚动距离scrollingOffset
的值传递了下去。recycleViewsFromStart()
从头遍历子控件,通过控件底部的位置与实际滚动长度比较,找出第一个底部位置大于滚动长度的控件,表示这些控件将会滚出屏幕,然后跳出到下一个方法recycleChildren()
。recycleChildren()
中的方法,遍历上一步选择出来的控件,这里使用到的其实是上一步找到的控件位置减一的位置,也就是说回收的是底部位置小于滚动长度的控件。- 最后就是调用到了
removeAndRecycleViewAt()
,里面分别调用了remove
方法移除控件,以及调用Recycler
的方法,对控件就行回收。看到这里,RecyclerView
的回收操作是委托给内部类RecyclerView.Recycler
的,下一节会深入到这个类。
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
...
int scrollingOffset = layoutState.mScrollingOffset;
int noRecycleSpace = layoutState.mNoRecycleSpace;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
...
final int limit = scrollingOffset - noRecycleSpace;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
recycleChildren(recycler, 0, i);
return;
}
}
}
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
if (startIndex == endIndex) {
return;
}
...
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
复制代码
1.4. 添加
回过头最后看一下layoutChunk()
是如何添加控件的。首先是获取控件,最终获取表项的地方还是进入到了RecyclerView.Recycler
中。获取到表项之后,就添加进来以及走了measure
和layout
流程。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
...
View view = layoutState.next(recycler);
//add
addView(view);
//measure
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//layout
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
//LayoutState方法
View next(RecyclerView.Recycler recycler) {
...
final View view = recycler.getViewForPosition(mCurrentPosition);
return view;
}
复制代码
1.5. 思考
源码阅读到这里,心中不禁画了一个巨大的问号,这不是在滚动吗?为什么还没滚动就已经开始回收?那之后的滚动操作不就会有一部分空白展示出来??
回答这个问题,首先要先回答另一个问题,我们是如何实现滚动或者动画这种连续的操作的?这种连续,在计算机的世界中都是连续值采样得到的离散值。换个通俗的话,滚动并不是不是连续,从显示上是一帧一帧的快速变换的画面构成,从操作上是一次一次的快速扫描位置构成。
再回到最初的问题,现在处理的问题真的是滚动吗?我们拿到的dy
,是在一个采样时间段内拿到的手指的位移,我们要做的事情并不是要从第一个位置连续的播放滚动动画到下一个位置,而是根据滚动距离dy
,直接从第一个位置切换到第二个位置。
所以对滚动的操作实际上是简单的一段一段偏移操作,将连续的动作简单化为两个瞬态样式的转换 。
这也就很好的解释了,为什么在滚动之前,就根据滚动距离,对顶部的控件进行了回收。这只是在对下一个瞬态的样式做准备罢了。
分析到这里,一次滚动的整体流程就结束了。不过也留下了一个更大的问题,截止的地方都是进入到了RecyclerView.Recycler
中,接下来进入其中一探究竟,它是如何实现的回收以及复用。
2. RecyclerView.Recycler
前面看到,表项的复用和回收都是委托给了RecyclerView.Recycler
处理。
为了更顺畅的阅读,先介绍一些后面分析得到的结论。
首先REcycler
中的缓存分了很多级,分别是scrap
、cache
、pool
以及extension
。这四级的缓存分别存储在这五个对象中:
attachedScrap
:布局过程中屏幕可见表项的回收和复用。布局时,先将所有表项进行回收,然后再添加进来,数量没有上限,一般数量就是屏幕中所有的可见表项,布局走到最后会全部清除。会在调用添加(notifyItemInserted()
)、删除(notifyItemRemoved()
)、移动(notifyItemMoved()
)和修改(notifyItemChanged()
)表项的时候起作用。changedScrap
:布局过程中屏幕可见并且发生改变表项的回收和复用。只会存储被修改(notifyItemChanged()
)的表项。同样也会在布局结束时全部清除。cachedViews
:数量默认上限2个,先进先出的队列结构,当有新的表项需要存入但数量达到上限时,会将最早的存入recyclerPool
。相当于recyclerPool
的预备队列。recyclerPool
:根据viewType
分类存储,每个类型上限默认5个。viewCacheExtension
:缓存扩展,用户可自定义的缓存策略。
在添加、删除和修改表项的时候,RecyclerView
会进行两次布局,第一次布局会对所有表项进行布局,第二次会对更改后的状态进行布局。根据两次布局之间的区别判断如何执行动画。这里的第一次布局,就是预布局preLayout
。
了解这些之后,我们首先先进入到复用的方法看看。
2.1. 复用
根据上一节复用的方法recycler.getViewForPosition()
,查看调用链会走到tryGetViewHolderForPositionByDeadline()
中,再回头查看,所有的创建方法最后都会调用到这个方法。我们直接从这个方法开始分析。
在这个方法中为了得到使用的表项,进行了六次次尝试。
getChangedScrapViewForPosition()
,尝试在changeScrap
中搜索。getScrapOrHiddenOrCachedHolderForPosition()
,尝试通过position
在attachedScrap
、隐藏的表项以及cachedView
中搜索。getScrapOrCachedViewForId()
,尝试通过id
在attachedScrap
、cachedView
中搜索。- 尝试通过
viewCacheExtension
回调从自定义缓存中搜索。 - 尝试通过
recyclerPool
获取表项。 - 直接创建表项。
这里先忽略掉自定义缓存和直接创建,对其他的一探究竟,继续往里面走。
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) 尝试从changedScrap中搜索
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1)通过position在attachedScrap、隐藏的表项以及cachedView中搜索
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
...
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) 尝试通过id在attachedScrap、cachedView中搜索
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// 3) 尝试在viewCacheExtension中搜索
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
// 4) 尝试在recyclerPool中搜索
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
}
}
// 5) 直接创建
if (holder == null) {
holder = mAdapter.createViewHolder(this, type);
}
}
...
return holder;
}
复制代码
2.1.1. getChangedScrapViewForPosition()
尝试在changeScrap
中搜索。
上一步可以看到,进入这个方法必须是preLayout
,再结合changeScrap
中只会存储被修改的表项,所以这个表项也就只能在preLayout
过程中复用被修改的表项。
其中的逻辑就是先后通过position
和id
在changeScrap
中搜索。
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null;
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
...
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
return null;
}
复制代码
2.1.2. getScrapOrHiddenOrCachedHolderForPosition()
首先通过position
在attchedScrap
中搜索。然后查看当前position
有没有隐藏的表项。最后通过position
在cachedView
中搜索。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
// Try first for an exact, non-invalid match from scrap.
final int scrapCount = mAttachedScrap.size();
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
...
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
...
return holder;
}
}
return null;
}
复制代码
2.1.3. getScrapOrCachedViewForId()
这个方法与上一个十分相似,把position
替换成了id
,以及少了从隐藏的表项中查找。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
...
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
if (type == holder.getItemViewType()) {
...
return holder;
}
}
}
return null;
}
复制代码
2.1.4. recyclerPool
复用池中,不同的type
存储在不同的scrapData
对象中,每个scrapData
可缓存的数量上限默认是5个。
从复用池中获取复用的组件,只需要type
匹配,并且这个组件目前没有绑定到当前RecyclerView
即可。
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
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<>();
public ViewHolder getRecycledView(int viewType) {
final RecycledViewPool.ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
}
复制代码
2.2. 复用
分析一下上一节所说的几个缓存的入口都在那里。
2.2.1. scrap
寻找attachScrap
和changedScrap
的调用链,会看到两个方法都只有一个地方调用add()
方法,并且都在scrapView()
方法中。在这个方法中,可以看到判断条件中,有一个holder.isUpdated()
,正如之前所说,changedScrap
只在存储修改的表项。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
...
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
mChangedScrap.add(holder);
}
}
复制代码
继续往上查看调用scrapView()
的地方,会走到LayoutManger.onLayoutChildren()
中,在其中先将所有表项回收,然后再调用fill()
添加进来,fill()
就是分析滚动时候的那个fill()
方法。
再接着往上找调用,onLayoutChildren()
这个方法也就是RecyclerView
在layout
过程中会调用到的方法,也就是scrap
缓存数据的添加是在布局过程中。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
...
} else {
detachViewAt(index);
recycler.scrapView(view);
}
}
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
public void LinearLayoutManager.onLayoutChildren(Recycler recycler, State state) {
...
detachAndScrapAttachedViews(recycler);
...
fill();
}
复制代码
再来看一下attachScrap
和changedScrap
的clear()
方法的调用链,除了一些初始化时的会调用,就只有在布局最后的调用了,结合前面的内容,确定了scrap
缓存的生命周期只存在于布局过程。
void clearScrap() {
mAttachedScrap.clear();
if (mChangedScrap != null) {
mChangedScrap.clear();
}
}
void removeAndRecycleScrapInt(Recycler recycler) {
...
recycler.clearScrap();
}
private void dispatchLayoutStep3() {
...
mLayout.removeAndRecycleScrapInt(mRecycler);
}
复制代码
2.2.2. cach
接下来看cach
缓存是在哪里添加的,它也只有一个add()
调用的地方,在recyclerViewHolderInternal()
方法中,在这个方法中可以看到添加之前对数量进行了判断,如果已经超过了上限,就会先将其中的第一个表项回收进recyclerPool
中。
void recycleViewHolderInternal(ViewHolder holder) {
...
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
...
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}
void recycleCachedViewAt(int cachedViewIndex) {
...
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
复制代码
继续往上寻找调用链,就会找到分析滚动时说到的回收方法recyclerView()
。
public void recycleView(@NonNull View view) {
...
ViewHolder holder = getChildViewHolderInt(view);
recycleViewHolderInternal(holder);
}
复制代码
2.2.3. recyclerPool
上面在分析cach
缓存的时候,看到回收到recyclerPool
的方法addViewHolderToRecycledViewPool()
,调用走到了RecyclerPool.putRecycledView()
方法,将表项存入对应type
的集合之中,如果数量已经到了上限就直接放弃。
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
...
getRecycledViewPool().putRecycledView(holder);
}
public void putRecycledView(ViewHolder scrap) {
...
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
复制代码
2.3. 思考
changeScrap
是很特殊的情况,必须在preLayout
步骤,以及必须是被更改的控件,我理解只是对attachedScrap
的一个补充,先忽略掉。attachedScrap
的生命周期是在布局过程中,先将所有可见的组件进行回收,然后再重新添加。添加的过程中如果组件的位置以及数据完全没有变换,才会复用,这种情况下不需要重新加载组件的数据。首次布局的时候没有显示的组件,所以只有在添加、删除、移动、修改部分组件的时候,请求重新布局,才不会将所有显示的组件标记废弃。- 在
scrap
和catch
搜索会有两种方法,分别是通过position
和id
,id
在我理解为是另一个唯一性的标识,比如列表数据与数据库id
相绑定,一般情况下并不会用到,只会用到position
搜索。 cachedView
作为recyclerPool
的预备队列,它们的使用却还是差了很多,cachedView
的使用与attachedScrap
比较类似,都是匹配position
并且没有数据的更新才会复用,这也就是cachedView
作为recyclerPool
两者最主要的差距,cachedView
复用不需要重新绑定数据,recyclerPool
复用需要重新绑定数据。- 对上一条再深入思考,
cachedView
和recyclerPool
一般是在滚动的时候回收进来,存放的都是滚出屏幕的表项,而cachedView
又只能通过position
匹配,也就是说cachedView
中表项的复用只在一种情况下,就是滚出屏幕的表项又重新滚回屏幕。