学习笔记-浅析RecyclerView复用机制

你了解recyclerView如何实现的复用,要不要一块儿来探索一下,剥开recyclerView神秘面纱?java

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,向上滑动为例。接着往下面走,看看到底是怎么处理的。markdown

首先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(),终于开始处理滚动:oop

  1. 首先把dy分解成方向和偏移值,其中方向对应两个值,分别是LAYOUT_END=1,LAYOUT_START=-1。前面说过咱们目前看的向上滑动,dy > 0,对应LAYOUT_END布局

  2. 调用到updateLayoutState(),这个方法是给LayoutState中的参数赋值,而LayoutState就是如何进行填充的配置参数。post

  3. 计算consumed,能够理解为计算出RecyclerView支持滚动的距离,其中调用到的fill(),就是控制表项的建立、回收、复用等的地方。动画

  4. 最后就是对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中的参数赋值。

  1. layoutDirection:滚动的方向,前面计算的,直接赋值。
  2. scrollingOffset:不添加新子项的状况下可滚动的距离。如今看的是向上滑动,这个值就是最底部的子项在屏幕外的高度。也就是最底部的子项的底部位置与RecyclerView底部位置的差值。
  3. 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循环,循环的条件就是有须要填充的高度,以及能够有足够的控件能够用于填充。而在循环中的流程能够分为三步:

  1. 添加下一个控件,layoutChunk()
  2. 从新计算available
  3. 回收,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()方法实现表项的回收,进入其中探索一下。

  1. recycleByLayoutState()中根据滚动的方向,分发到不一样的方法,前面说到分析的是向上滑动,对应的方法就是recycleViewsFromStart()。这里也将前面所分析到的每一次的实际滚动距离scrollingOffset的值传递了下去。
  2. recycleViewsFromStart()从头遍历子控件,经过控件底部的位置与实际滚动长度比较,找出第一个底部位置大于滚动长度的控件,表示这些控件将会滚出屏幕,而后跳出到下一个方法recycleChildren()
  3. recycleChildren()中的方法,遍历上一步选择出来的控件,这里使用到的实际上是上一步找到的控件位置减一的位置,也就是说回收的是底部位置小于滚动长度的控件。
  4. 最后就是调用到了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中。获取到表项以后,就添加进来以及走了measurelayout流程。

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中的缓存分了不少级,分别是scrapcachepool以及extension。这四级的缓存分别存储在这五个对象中:

  • attachedScrap:布局过程当中屏幕可见表项的回收和复用。布局时,先将全部表项进行回收,而后再添加进来,数量没有上限,通常数量就是屏幕中全部的可见表项,布局走到最后会所有清除。会在调用添加(notifyItemInserted())、删除(notifyItemRemoved())、移动(notifyItemMoved())和修改(notifyItemChanged())表项的时候起做用。
  • changedScrap:布局过程当中屏幕可见而且发生改变表项的回收和复用。只会存储被修改(notifyItemChanged())的表项。一样也会在布局结束时所有清除。
  • cachedViews:数量默认上限2个,先进先出的队列结构,当有新的表项须要存入但数量达到上限时,会将最先的存入recyclerPool。至关于recyclerPool的预备队列。
  • recyclerPool:根据viewType分类存储,每一个类型上限默认5个。
  • viewCacheExtension:缓存扩展,用户可自定义的缓存策略。

在添加、删除和修改表项的时候,RecyclerView会进行两次布局,第一次布局会对全部表项进行布局,第二次会对更改后的状态进行布局。根据两次布局之间的区别判断如何执行动画。这里的第一次布局,就是预布局preLayout

了解这些以后,咱们首先先进入到复用的方法看看。

2.1. 复用

根据上一节复用的方法recycler.getViewForPosition(),查看调用链会走到tryGetViewHolderForPositionByDeadline()中,再回头查看,全部的建立方法最后都会调用到这个方法。咱们直接从这个方法开始分析。

在这个方法中为了获得使用的表项,进行了六次次尝试。

  1. getChangedScrapViewForPosition(),尝试在changeScrap中搜索。
  2. getScrapOrHiddenOrCachedHolderForPosition(),尝试经过positionattachedScrap、隐藏的表项以及cachedView中搜索。
  3. getScrapOrCachedViewForId(),尝试经过idattachedScrapcachedView中搜索。
  4. 尝试经过viewCacheExtension回调从自定义缓存中搜索。
  5. 尝试经过recyclerPool获取表项。
  6. 直接建立表项。

这里先忽略掉自定义缓存和直接建立,对其余的一探究竟,继续往里面走。

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过程当中复用被修改的表项。

其中的逻辑就是前后经过positionidchangeScrap中搜索。

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()

首先经过positionattchedScrap中搜索。而后查看当前position有没有隐藏的表项。最后经过positioncachedView中搜索。

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

寻找attachScrapchangedScrap的调用链,会看到两个方法都只有一个地方调用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()这个方法也就是RecyclerViewlayout过程当中会调用到的方法,也就是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();
}
复制代码

再来看一下attachScrapchangedScrapclear()方法的调用链,除了一些初始化时的会调用,就只有在布局最后的调用了,结合前面的内容,肯定了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. 思考

  1. changeScrap是很特殊的状况,必须在preLayout步骤,以及必须是被更改的控件,我理解只是对attachedScrap的一个补充,先忽略掉。
  2. attachedScrap的生命周期是在布局过程当中,先将全部可见的组件进行回收,而后再从新添加。添加的过程当中若是组件的位置以及数据彻底没有变换,才会复用,这种状况下不须要从新加载组件的数据。首次布局的时候没有显示的组件,因此只有在添加、删除、移动、修改部分组件的时候,请求从新布局,才不会将全部显示的组件标记废弃。
  3. scrapcatch搜索会有两种方法,分别是经过positionidid在我理解为是另外一个惟一性的标识,好比列表数据与数据库id相绑定,通常状况下并不会用到,只会用到position搜索。
  4. cachedView做为recyclerPool的预备队列,它们的使用却仍是差了不少,cachedView的使用与attachedScrap比较相似,都是匹配position而且没有数据的更新才会复用,这也就是cachedView做为recyclerPool二者最主要的差距,cachedView复用不须要从新绑定数据,recyclerPool复用须要从新绑定数据。
  5. 对上一条再深刻思考,cachedViewrecyclerPool通常是在滚动的时候回收进来,存放的都是滚出屏幕的表项,而cachedView又只能经过position匹配,也就是说cachedView中表项的复用只在一种状况下,就是滚出屏幕的表项又从新滚回屏幕。

3. 参考文章

  1. RecyclerView 缓存机制 | 如何复用表项?
  2. RecyclerView 面试题 | 滚动时表项是如何被填充或回收的?
  3. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
  4. RecyclerView缓存机制 | scrap view 的生命周期
  5. RecyclerView的复用缓存机制
  6. RecyclerView的缓存分析
相关文章
相关标签/搜索