RecyclerView 的复用机制

RecyclerView 是一个强大又灵活的 View,能够用有限的 View 来展现大量的数据。今天咱们来看下 RecyclerView 内部是经过怎样的缓存复用机制来实现这一功能的。java

Recycler

Recycler 是 RecyclerView 的内部类,也是这套复用机制的核心,显然 Recycler 的主要成员变量也都是用来缓存和复用 ViewHolder 的:程序员

public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();
    
    RecycledViewPool mRecyclerPool;
    
    private ViewCacheExtension mViewCacheExtension;
}
复制代码

这些缓存集合能够分为 4 个级别,按优先级从高到底为:数组

  • 一级缓存:mAttachedScrap 和 mChangedScrap ,用来缓存还在屏幕内的 ViewHolder缓存

    • mAttachedScrap 存储的是当前还在屏幕中的 ViewHolder;按照 id 和 position 来查找 ViewHolder
    • mChangedScrap 表示数据已经改变的 ViewHolder 列表, 存储 notifyXXX 方法时须要改变的 ViewHolder
  • 二级缓存:mCachedViews ,用来缓存移除屏幕以外的 ViewHolder,默认状况下缓存容量是 2,能够经过 setViewCacheSize 方法来改变缓存的容量大小。若是 mCachedViews 的容量已满,则会根据 FIFO 的规则移除旧 ViewHolderide

  • 三级缓存:ViewCacheExtension ,开发给用户的自定义扩展缓存,须要用户本身管理 View 的建立和缓存。我的感受这个拓展脱离了 Adapter.createViewHolder 使用的话会形成 View 建立和数据绑定和其它代码太分散,不利于维护,使用场景不多仅作了解布局

    /* * Note that, Recycler never sends Views to this method to be cached. It is developers * responsibility to decide whether they want to keep their Views in this custom cache or let * the default recycling policy handle it. */
    public abstract static class ViewCacheExtension {
    	public abstract View getViewForPositionAndType(...);
    }
    复制代码
  • 四级缓存:RecycledViewPool ,ViewHolder 缓存池,在有限的 mCachedViews 中若是存不下新的 ViewHolder 时,就会把 ViewHolder 存入 RecyclerViewPool 中。动画

    • 按照 Type 来查找 ViewHolder
    • 每一个 Type 默认最多缓存 5 个
    • 能够多个 RecyclerView 共享 RecycledViewPool

接下来咱们看下这四级缓存是怎么工做的this

复用

RecyclerView 做为一个 “平平无奇” 的 View,子 View 的排列和布局固然是从 onLayout 入手了,调用链:spa

RecyclerView.onLayout(...)
-> RecyclerView.dispatchLayout()    
-> RecyclerView.dispatchLayoutStep2() // do the actual layout of the views for the final state.
-> mLayout.onLayoutChildren(mRecycler, mState) // mLayout 类型为 LayoutManager
-> LinearLayoutManager.onLayoutChildren(...) // 以 LinearLayoutManager 为例
-> LinearLayoutManager.fill(...) // The magic functions :) 填充给定的布局,注释很自信的说这个方法很独立,稍微改动就能做为帮助类的一个公开方法,程序员的快乐就是这么朴实无华。
-> LinearLayoutManager.layoutChunk(recycler, layoutState) // 循环调用,每次调用填充一个 ItemView 到 RV
-> LinearLayoutManager.LayoutState.next(recycler) 
-> RecyclerView.Recycler.getViewForPosition(int) // 回到主角了,经过 Recycler 获取指定位置的 ItemView 
-> Recycler.getViewForPosition(int, boolean) // 调用下面方法获取 ViewHolder,并返回上面须要的 viewHolder.itemView 
-> Recycler.tryGetViewHolderForPositionByDeadline(...) // 终于找到你,还好没放弃~ 
复制代码

能够看出最终调用 tryGetViewHolderForPositionByDeadline ,来看看这个方法是怎么拿到相应位置上的 ViewHolder :code

ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {
    if (mState.isPreLayout()) {
        // 0) 预布局从 mChangedScrap 里面去获取 ViewHolder,动画相关
        holder = getChangedScrapViewForPosition(position);
    }
    
    if (holder == null) {
        // 1) 分别从 mAttachedScrap、 mHiddenViews、mCachedViews 获取 ViewHolder
        // 这个 mHiddenViews 是用来作动画期间的复用
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 若是 Adapter 的 hasStableIds 方法返回为 true
        // 优先经过 ViewType 和 ItemId 两个条件从 mAttachedScrap 和 mCachedViews 寻找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
        }
      
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not know it.
            // 3) 从自定义缓存获取,别问,问就是别用
            View view = mViewCacheExtension getViewForPositionAndType(this, position, type);
            holder = getChildViewHolder(view);
        }
    }
    
    if (holder == null) {
        // 4) 从 RecycledViewPool 获取 ViewHolder
        holder = getRecycledViewPool().getRecycledView(type);
    }
  
    if (holder == null) {
        // 缓存全取过了,没有,那只好 create 一个咯
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
    
    // This is very ugly but the only place we can grab this information
    // 大半夜看到这句注释的时候笑出声!像极了我写出丑代码时的无奈。
    
    // 在这后面有一些刷新 ViewHolder 信息的代码,放这很丑,但又只能放这,为了能走到这,前面有多少 if (holder == null)
}
复制代码

分析完复用的部分,接下来再看一下 ViewHolder 存入缓存的部分

缓存

所谓的缓存,就是看一下是怎么样往前面提到的四级缓存添加数据的

  • mAttachedScrap 和 mChangedScrap
  • mCachedViews
  • ViewCacheExtension 前面说了,这个的建立和缓存彻底由开发者本身控制,系统未往这里添加数据
  • RecycledViewPool

mAttachedScrap 和 mChangedScrap

若是调用了 Adapter 的 notifyXXX 方法,会从新回调到 LayoutManager 的onLayoutChildren 方法里面, 而在 onLayoutChildren 方法里面,会将屏幕上全部的 ViewHolder 回收到 mAttachedScrap 和 mChangedScrap。

// 调用链 
   LinearLayoutManager.onLayoutChildren(...)
-> LayoutManager.detachAndScrapAttachedViews(recycler)
-> LayoutManager.scrapOrRecycleView(..., view)
-> Recycler.scrapView(view);  

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        // 后面会讲,缓存到 mCacheViews 和 RecyclerViewPool
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        // 缓存到 scrap
        recycler.scrapView(view); 
    }
}

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
        || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        // 标记为移除或失效的 || 彻底没有改变 || item 无动画或动画不复用
        mAttachedScrap.add(holder);
    } else {
        mChangedScrap.add(holder);
    }
}
复制代码

其实还有一种状况会调用到 scrapView , 从 mHiddenViews 得到一个 ViewHolder 的话(发生在支持动画的操做),会先将这个 ViewHolder 从 mHiddenViews 数组里面移除,而后调用:

Recycler.tryGetViewHolderForPositionByDeadline(...)
-> Recycler.getScrapOrHiddenOrCachedHolderForPosition(...)
-> Recycler.scrapView(view)
复制代码

mCacheViews 和 RecyclerViewPool

这两级缓存的代码都在 Recycler 的这个方法里:

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();
          if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
              // 1. mCacheViews 满了,最先加入的不要了放 RecyclerViewPool
              recycleCachedViewAt(0); 
          }
           mCachedViews.add(targetCacheIndex, holder);
           cached = true;
        }
        
        if (!cached) { 
            // 2. 不能放进 mCacheViews 的放 RecyclerViewPool
            addViewHolderToRecycledViewPool(holder, true);
        }
    }   
}

// Recycles a cached view and removes the view from the list
void recycleCachedViewAt(int cachedViewIndex) {
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    addViewHolderToRecycledViewPool(viewHolder, true);
    mCachedViews.remove(cachedViewIndex);
}
复制代码

在这咱们知道 recycleViewHolderInternal 会把 ViewHolder 缓存到 mCacheViews ,而不知足加到 mCacheViews 的会缓存到 RecycledViewPool 。那又是何时调用的 recycleViewHolderInternal 呢?有如下三种状况:

  1. 从新布局,主要是调用 Adapter.notifyDataSetChange 且 Adapter 的 hasStableIds 方法返回为 false 时调用。从这边也能够看出为何通常状况下 notifyDataSetChange 效率比其它 notifyXXX 方法低(使用二级缓存及优先级更低的缓存 ),同时也知道了,若是咱们设置 Adapter.setHasStableIds(ture) 以及其它相关须要的实现,则能够提升效率(使用一级缓存)
  2. 在复用时,从一级缓存里面获取到 ViewHolder,可是此时这个 ViewHolder 已经不符合一级缓存的特色了(好比 Position 失效了,跟 ViewType 对不齐),就会从一级缓存里面移除这个 ViewHolder,添加到这两级缓存里面
  3. 当调用 removeAnimatingView 方法时,若是当前 ViewHolder 被标记为 remove ,会调用 recycleViewHolderInternal 方法来回收对应的 ViewHolder。调用 removeAnimatingView 方法的时机表示当前的 ItemAnimator 已经作完了

总结

到这里,RecyclerView 的缓存复用机制就分析完了,总结一下:

  • RecyclerView 的缓存复用机制,主要是经过内部类 Recycler 来实现
  • Recycler 有 4 级缓存,每一级的缓存都有各自的做用,会按优先级使用。
  • ViewHolder 会从某一级缓存移至其它级别的缓存
  • mHideenViews 的存在是为了解决在动画期间进行复用的问题。
  • 缓存复用 ViewHolder 时会针对内部不一样的状态 ( mFlags ) 进行相应的处理。

本文源码基于:recyclerview:1.2.0-alpha03

相关文章
相关标签/搜索