RecyclerView 源码分析(一):Recycler

前言

RecyclerView 是一个好用又复杂的控件,其功能的高度解耦化,规范化的 ViewHolder 写法,以及对动画的友好支持,都是它与传统 ListView 的区别。

它有几大模块:

  • LayoutManager:控制 item 的布局
  • RecyclerView.Adapter:为 RecyclerView 提供数据
  • ItemDecoration:为 RecyclerView 添加分割线
  • ItemAnimator:控制 item 的动画
  • Recycler:负责回收和提供 View,和 RecyclerView 的复用机制相关

下面就从源码(API 28)角度分析 RecyclerView,RecyclerView 的源码很复杂,很难在一篇文章内讲完,所以打算分几篇来讲,本文是第一篇,将围绕 RecyclerView 的内部类 Recycler 展开分析:

RecyclerView.Recycler

首先看一下它的作用,源码上是这样写的:

A Recycler is responsible for managing scrapped or detached item views for reuse.

意思就是 Recycler 负责管理废弃或被 detached 的 item 视图,以便重复利用。

它有以下几个成员变量:

主要成员变量


    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;

这几个成员变量都和 RecyclerView 的缓存相关,如果按照四级缓存的话,它们可以这样划分:

第一级缓存:mAttachedScrap、mChangedScrap

第二级缓存:mCachedViews

第三级缓存:ViewCacheExtension

第四级缓存:RecycledViewPool

后面再介绍 mAttachedScrap、mChangedScrap、mCachedViews 具体存的是哪些 ViewHolder。

现在先了解下 RecycledViewPool 和 ViewCacheExtension这两个类:

RecycledViewPool

继续先看官方注释:

RecycledViewPool lets you share Views between multiple RecyclerViews.

RecycledViewPool 用于在多个 RecyclerView 间共享 View。

在使用时,只需创建 RecycledViewPool 实例,然后调用 RecyclerView 的 setRecycledViewPool(RecycledViewPool) 方法即可。

RecycledViewPool 存储在 Recycler 中,通过 Recycler 存取。

成员变量

RecycledViewPool 有一个重要的成员变量:

    // SparseArray 类似于 key 为 int 类型 的 HashMap
    SparseArray<ScrapData> mScrap = new SparseArray<>();

其中 ScrapData 的定义如下:

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;  // 5
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }

mScrap 是一个 <int, ScrapData> 的映射,其中 int 代表了 viewType,ScrapData 则存储了一个 ViewHolder 集合。

主要方法

getScrapDataForType

    private ScrapData getScrapDataForType(int viewType) {
        ScrapData scrapData = mScrap.get(viewType);
        if (scrapData == null) {
            scrapData = new ScrapData();
            mScrap.put(viewType, scrapData);
        }
        return scrapData;
    }

该方法根据 viewType 获取相应的 ScrapData,如果该 viewType 还没有绑定 ScrapData,就新创建一个 ScrapData 并绑定到该 viewType。

setMaxRecycledViews

    public void setMaxRecycledViews(int viewType, int max) {
        ScrapData scrapData = getScrapDataForType(viewType);
        scrapData.mMaxScrap = max;
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        // 从后面开始删除,直到满足新的容量
        while (scrapHeap.size() > max) {
            scrapHeap.remove(scrapHeap.size() - 1);
        }
    }

该方法可以设置相应 viewType 的 View 容量,超出容量时,从后面开始删除,直到满足新的容量。

getRecycledView

    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            return scrapHeap.remove(scrapHeap.size() - 1);
        }
        return null;
    }

该方法根据 viewType 获取一个 ViewHolder,获取到的 ViewHolder 将会被移除出 Scrap 堆。获取不到则返回 null。

putRecycledView

    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;
        }
        // 重置 ViewHolder,例如清空 flags
        scrap.resetInternal();
        // 将该 ViewHolder 添加到对应 viewType 的 集合中缓存起来
        scrapHeap.add(scrap);
    }

该方法也很好理解,根据 ViewHolder 的 viewType 放入 RecycledViewPool 的相应集合中,如果集合已满,不再添加。

接下来看另一个类:

ViewCacheExtension

ViewCacheExtension 是一个由开发者控制的 View 缓存帮助类,其定义如下:

    public abstract static class ViewCacheExtension {

        /**
         * Returns a View that can be binded to the given Adapter position.
         */
        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);
    }

开发者可以实现这个抽象类,通过调用 RecyclerView 的 setViewCacheExtension(ViewCacheExtension) 方法设置,最终将 ViewCacheExtension 存储在 Recycler 中。

当调用 Recycler 的 getViewForPosition 方法时,如果 attached scrap 和 已经缓存都没有找到合适的 View,就会调用 ViewCacheExtension 的 getViewForPositionAndType 方法来获取 View。

需要注意的是,Recycler 不会对这个类做任何缓存处理,是否需要缓存 View 由开发者自己控制。

主要方法

看完这两个类,现在回到 Recycler 中,看一下 Rcycler 的主要方法:

getViewForPosition

getViewForPosition 方法比较重要,用于获取某个位置需要展示的 View,如下:

    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }

继续看 tryGetViewHolderForPositionByDeadline 方法,该方法会依次从几个缓存中获取,分别来看一下:

    // 如果是处于预布局阶段(先简单理解为执行 dispatchLayoutStep1 方法)
    // (其实下面方法要返回 ture 还需要开启“预处理动画”,这跟动画有关,先不多说)
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }

第一步,从 mChangedScrap 中获取,获取不到就返回 null。

如果 holder 还是为 null,执行下面代码:

    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                // recycle holder (and unscrap if relevant) since it can't be used
                // 回收无效的 ViewHolder
                // ...
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }

第二步,根据 position 依次从 mAttachedScrap、mHiddenViews(存储在 ChildHelper 类)、mCachedViews 中获取缓存的 ViewHolder。

可以从 mHiddenViews 获取到缓存的话,就将其从 mHiddenViews 移除并添加到 Scrap 缓存(根据情况添加到 mAttachedScrap 或 mChangedScrap)。可以从 mCacheViews 中获取到缓存的话,就将其从 mCacheViews 移除。

获取到后,发现无效的话,将对获取到的 ViewHolder 进行清理并回收(放入 mCachedViews 或 RecycledViewPool)。

获取不到,就继续往下执行:

    // 默认返回 false,可通过 Adapter.setHasStableIds 方法设置该值
    if (mAdapter.hasStableIds()) {
        // 根据 id 依次在 mAttachedScrap、mCachedViews 中获取缓存
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                type, dryRun);
        if (holder != null) {
            holder.mPosition = offsetPosition;
            fromScrapOrHiddenOrCache = true;
        }
    }

第三步,根据 id 依次从 mAttachedScrap、mCachedViews 中获取缓存,还没有获取到就继续往下:

    // 如果用户设置了 ViewCacheExtension
    if (holder == null && mViewCacheExtension != null) {
        // We are NOT sending the offsetPosition because LayoutManager does not
        // know it.
        final View view = mViewCacheExtension
                .getViewForPositionAndType(this, position, type);
        if (view != null) {
            holder = getChildViewHolder(view);
            // ...
        }
    }

第四步,从用户设置的 ViewCacheExtension 中获取缓存,没有获取到就继续往下:

    if (holder == null) { // fallback to pool
        holder = getRecycledViewPool().getRecycledView(type);
        // ...
    }

第五步,根据 viewType 从 RecycledViewPool 中得到缓存。

RecycledViewPool 已经是最后一级缓存了,如果这里也没有获取到,只能通过 Adapter 的 createViewHolder 方法创建一个 ViewHolder:

    if (holder == null) {

        holder = mAdapter.createViewHolder(RecyclerView.this, type);

        // ...
    }

最后小结一下获取某个位置的 View 的过程:

  1. 先后根据 position 或 id 从 mChangedScrap 中获取缓存
  2. 根据 position 依次从 mAttachedScrap、mHiddenViews(存储在 ChildHelper 类)、mCachedViews 中获取缓存
  3. 根据 id 依次从 mAttachedScrap、mCachedViews 中获取缓存
  4. 从用户设置的 ViewCacheExtension 中获取缓存
  5. 从 RecycledViewPool 中得到缓存的废弃 ViewHolder
  6. 通过 Adapter 的 createViewHolder 方法创建一个 ViewHolder

recycleView

既然叫 Recycler,那肯定要做回收工作了,recycleView 方法就完成了这些工作,下面看一下该方法的实现:

    public void recycleView(@NonNull View view) {
        ViewHolder holder = getChildViewHolderInt(view);
        // ...
        recycleViewHolderInternal(holder);
    }

继续看 recycleViewHolderInternal:

    void recycleViewHolderInternal(ViewHolder holder) {
        // ...

        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                        
                int cachedViewSize = mCachedViews.size();
                // 若 CacheViews 达到最大容量(2),将最老的缓存从 CacheViews 移除,并添加到 RecycledViewPool 中
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }

                // ...
                
                // 将 View 缓存到 mCachedViews 中
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                // 没有添加到 mCachedViews 的话,就添加到 RecycledViewPool 中
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        }

        // ...
    }

可以看到,回收过程主要涉及到两层缓存,第一层缓存是 CacheViews,在添加时,如果发现原来的 CacheViews 已经达到最大容量,就将最老的缓存从 CacheViews 移除,并添加到 RecycledViewPool。第二层缓存是 RecycledViewPool,如果不能添加到 mCacheViews,就会添加到 RecycledViewPool 中。

补充

mChangedScrap 和 mAttachedScrap 中的 View 从何而来

从前面可以得知,在执行 Recycler 的 recycleView 方法时,会将回收的 View 缓存到 mCahceViews 或 recycledViewPool 中,那么另外两个 Scrap 缓存(mChangedScrap 和 mAttachedScrap)中的 View 是何时添加进来的呢?

无论是 mAttachedScrap 还是 mChangedScrap ,它们获得 View 的途径都只有一个,那就是通过 Recycler 的 scrapView 方法。先看下该方法:

Recycler#scrapView

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        
        // 满足这几个条件中的一个就可以进入 if 循环,有机会将 View 缓存到 mAttachedScrap
        // 1. ViewHolder 设置了 FLAG_REMOVED 或 FLAG_INVALID
        // 2. ViewHolder 没有设置 FLAG_UPDATE
        // 3. 没有设置动画或者动画可以重用该 ViewHolder
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                throw new IllegalArgumentException("Called scrap view with an invalid view."
                        + " Invalid views cannot be reused from scrap, they should rebound from"
                        + " recycler pool." + exceptionLabel());
            }
            // 给 ViewHolder 绑定 Recycler
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);
        } 
        // 不满足上述任意一个条件时,将 View 缓存到 mChangedScrap 中
        else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }

该方法通过判断 ViewHolder 的 flag 以及是否设置 ItemAnimator 等,决定将 View 缓存到 mAttachedScrap 还是 mChangedScrap。

那么该方法在何时调用呢?有两种情况:

  1. 以 LinearLayoutManager 为例,在它的 onLayoutChildren 方法中,会调用
    detachAndScrapAttachedViews(recycler);

该方法定义在 RecyclerView 的 LayoutManager 中,它继续调用 scrapOrRecycleView 方法,如果在该方法符合条件就调用 Recycler 的 scrapView 方法。

  1. 通过 mHiddenViews 获取到缓存时,也会调用 scrapView 方法。

场景分析

下面就根据一些场景来分析下 Recycler 是如何进行回收和复用的。

第一次 layout

由于这里不是专门分析 layout 过程的,就不从 onLayout 开始说了,中间的过程省略掉,它最终会调用到 LayoutManager 的 onLayoutChildren,这里以 LinearLayoutManager 为例:

LinearLayoutManager#onLayoutChildren

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // ...

        // 找到锚点(具体过程等到分析 layout 时再说)
        
        // (1)
        detachAndScrapAttachedViews(recycler);
        
        if (mAnchorInfo.mLayoutFromEnd) {
            // ...
        } else {
            
            // (2)
            fill(recycler, mLayoutState, state, false);

            // ...
        }
        
        // ...
    }

首先看(1)处,detachAndScrapAttachedViews 方法会根据情况将子 View 回收到相应缓存,具体过程之后再看,由于现在是第一次 layout,RecyclerView 中没有子 View,所以现在该方法没啥用。

接下来看(2)处,这里的 fill 方法比较重要,它的作用是填充布局。看一下该方法:

LinearLayoutManager#fill

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {

        // 进行 layout 时 layoutState.mScrollingOffset 的值被设置为
        // LayoutState.SCROLLING_OFFSET_NaN,不会进入此 if 块
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // ...
            recycleByLayoutState(recycler, layoutState);
        }
        
        // 需要填充的空间
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        // 还有需要填充的空间并且 item 数未满
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // ...
            
            // (1)
            layoutChunk(recycler, state, layoutState, layoutChunkResult);

            // 计算剩余空间

            // 同上,在 layout 时不会进入 if 块中
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                // ...
                recycleByLayoutState(recycler, layoutState);
            }
            
            // ...
        }
    }

主要看(1)处的 layoutChunk 方法,只要还有需要填充的空间,就会不断调用该方法:

LinearLayoutManager#layoutChunk

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        // (1)
        View view = layoutState.next(recycler);

        // ...
        
        // 默认情况下,layoutState.mScrapList 等于 null
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                // (2)
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            // ...
        }
    }

(2)处的 addView 方法就不多说了,该方法将得到的子 View 添加到 RecyclerView 中。主要看(1)处,看看子 View 从何而来:

    View next(RecyclerView.Recycler recycler) {
        // ...
        
        final View view = recycler.getViewForPosition(mCurrentPosition);

        return view;
    }

这个方法是不是很熟悉呢?没错,它就是之前分析的 Recycler 的 getViewForPosition 方法。

不过由于现在没有任何缓存,所以第一次 layout 的时候是通过 Adapter 的 createViewHolder 来创建子 View的,并且没有添加任何缓存。

更新列表

更新列表可以使用 Adapter 的一系列 notify 方法,这里分析其中两个方法:notifyDataSetChanged 和 notifyItemChanged(int)。

Adapter#notifyDataSetChanged

该方法最终调用了 RecyclerViewDataObserver 的 onChanged 方法:

    @Override
    public void onChanged() {
        // ...

        // 该方法主要做了这两件事
        // 1. 给所有 ViewHolder 添加了 FLAG_UPDATE 和 FLAG_INVALID
        // 2. 默认情况下(mHasStableIds 为 false)清空 CacheViews
        processDataSetCompletelyChanged(true);
        
        if (!mAdapterHelper.hasPendingUpdates()) {
            // 进行视图重绘
            requestLayout();
        }
    }

该方法会进行视图重绘,又来到了 layout 过程,继续以 LinearLayoutManager 为例,从它的 onLayoutChildren 方法看起,由于分析第一次 layout 时已经看过一遍了,这次主要看下不同之处:

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // ...
        
        detachAndScrapAttachedViews(recycler);
        
        // ...
    }

主要区别在于 detachAndScrapAttachedViews 方法,这次它开始起作用了,该方法在 RecyclerView 的 LayoutManager 中定义,看下它的实现:

LayoutManager#detachAndScrapAttachedViews

    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);
        }
    }

由于不是第一次 layout,RecyclerView 这时已经有子 View 了,该方法遍历子 View,调用 scrapOrRecycleView 方法:

    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        // 不能回收添加了 FLAG_IGNORE 标记的 ViewHolder
        // 可通过 LayoutManager 的 ignoreView 为相应的 View 添加该标记
        if (viewHolder.shouldIgnore()) {
            return;
        }
        // 这些条件都满足,进入 if 块
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            // ...
        }
    }

这里将子 View 移除并通过 Recycler 的 recycleViewHolderInternal 方法进行回收:

Recycler#recycleViewHolderInternal

        void recycleViewHolderInternal(ViewHolder holder) {
            // ...
            boolean cached = false;
            boolean recycled = false;

            if (forceRecycle || holder.isRecyclable()) {
                // 由于此时的 ViewHolder 有 FLAG_INVALID 标记,不会进入此 if 块
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    //...
                }
                // cached 仍为 false,进入此 if 块
                if (!cached) {
                    // 通过 RecycledViewPool 的 putRecycledView 方法缓存该 ViewHolder
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } 
            
            // ...
        }

最终被移除的子 View 缓存到了 RecycledViewPool 中。

后面在调用 fill 方法进行布局填充时,就可以从 RecycledViewPool 中拿取缓存的 View。

Adapter#notifyItemChanged

该方法传入一个 int 参数,表示要数据有更新的 item 的 position。

    public final void notifyItemChanged(int position) {
        mObservable.notifyItemRangeChanged(position, 1);
    }

最终调用 RecyclerViewDataObserver 的 onItemRangeChanged 方法:

    @Override
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        // 会在 mAdapterHelper 中创建一个 UpdateOp,将信息保存起来
        if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
            // 如果可以进行更新操作,执行该方法
            triggerUpdateProcessor();
        }
    }

继续看 triggerUpdateProcessor 方法:

    void triggerUpdateProcessor() {
        // 判断条件默认为 false,执行 else 块
        if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
            // ...
        } else {
            mAdapterUpdateDuringMeasure = true;
            requestLayout();
        }
    }

在保存了一些信息后,还是进行视图重绘。来到了 layout 过程后,还是以 LinearLayoutManager 为例,这次先看下布局过程的 step1,也就是 dispatchLayoutStep1 方法:

RecyclerView#dispatchLayoutStep1

    private void dispatchLayoutStep1() {
        // ...
        
        processAdapterUpdatesAndSetAnimationFlags();
        
        // ...
    }

主要看 processAdapterUpdatesAndSetAnimationFlags 方法,从名字也可以看出,它负责更新 adapter 的信息:

    private void processAdapterUpdatesAndSetAnimationFlags() {
        // ...

        if (predictiveItemAnimationsEnabled()) {
            mAdapterHelper.preProcess();
        } else {
            mAdapterHelper.consumeUpdatesInOnePass();
        }

        // ...
    }

这里借助了 mAdapterHelper,它最终又通过接口回调(回调了 markViewHoldersUpdated 方法)调用了 RecyclerView 的 viewRangeUpdate 方法:

    void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
        // ...

        for (int i = 0; i < childCount; i++) {
            // ...
            
            if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
                // (1)
                holder.addFlags(ViewHolder.FLAG_UPDATE);
                // ...
            }
        }
    }

该方法就是遍历所有子 View,找到所有发生了改变的子 View,进行相关操作。这里重点看注释(1),为改变的 ViewHolder 添加了 FLAG_UPDATE 标记。先记住这点,在后面会用到。

接下来看 onLayoutChildren 方法,和 notifyDataSetChanged 一样,主要的不同之处也是在于 detachAndScrapAttachedViews 方法,该方法遍历子 View,调用 scrapOrRecycleView 方法,下面看一下该方法:

LayoutManager#scrapOrRecycleView

    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        // ...
        
        // 这次 ViewHolder 没有添加 FLAG_INVALID 标记,进入 else 块
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            // ...
        } else {
            detachViewAt(index);
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

这里就和 notifyDataSetChanged 时不一样了,由于在视图重绘前没有给 ViewHolder 添加 FLAG_INVALID 标记,这次进入的是 else 块。

首先将 View 从 RecyclerView 中 detach 掉(而不是 remove 掉)。然后在回收时,调用的是 Recycler 的 scrapView 方法。该方法在前面也分析过了,这里再看一次:

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        
        // 满足这几个条件中的一个就可以进入 if 循环
        // 1. ViewHolder 设置了 FLAG_REMOVED 或 FLAG_INVALID 
        // 2. ViewHolder 没有设置 FLAG_UPDATE 
        // 3. 没有设置动画或者动画可以重用该 ViewHolder 
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            // ...
            
            mAttachedScrap.add(holder);
        } 
        // 不满足上述任意一个条件时,将 View 缓存到 mChangedScrap 中
        else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }

重点看判断里面的条件 2,从前面的分析可以得知,对于发生改变的 ViewHolder,给它设置了 FLAG_UPDATE,所以它现在三个条件都不满足,进入 else 块,而对于其他的 ViewHolder,由于没有设置 FLAG_UPDATE,所以满足条件 2,进入 if 循环。

所以通过 notifyItemChanged 方法更新列表时,发生了改变的子 View 将被缓存到 ChangedScrap 中,而没有发生改变的子 View 则缓存到 AttachedScrap 中,之后通过填充布局的时候对于不同 item 就可以从相应的 Scrap 缓存中得到子 View。

另外,Scrap 缓存只作用于布局阶段,在 layout 的 step3 中将会清空 mAttachedScrap 和 mChangedScrap。

其实还有一个常见的场景是滑动操作,滑动出屏幕的子 View 将会缓存到 mCachedView,不过这里就不详细说了,在之后会在其他文章专门分析滑动这块。

后记

本文围绕 Recycler 展开叙述,重点是要通过它的几个成员变量了解它的缓存机制,四级缓存分别是什么,是在何时调用的,各自起到的作用,不同场景下使用哪种缓存等。

Recycler 和 LayoutManager 的布局以及动画都有联系,例如 LayoutManager 负责布局,它决定获取子 View 和回收子 View 的时机,具体的工作就交由 Recycler 负责。这些会在之后对 RecyclerView 的其他方面作分析时进行更详细的说明。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容