Android常考问题(6)-RecyclerView及其复用-源码分析

12 篇文章 0 订阅
10 篇文章 0 订阅

前一篇的ListView只是之前的学习的回顾和复习,RecyclerView才是目前学习的主要内容。这部分内容也比较多,因此详细分两部分来总结一下。而且对比之前的ListView,应该能更好理解RecyclerView的内容。

首先认识一下RecyclerView。这个东西有点杂乱。我们知道ListView可以用来显示很多的重复数据,按照item排列就行了。而RecyclerView综合了ListView和GridView的优点,它的职责也是将Datas中的数据以一定的规则展示在它的上面,说白了还是一个ViewGroup。那么它的功能怎么实现呢?

首先用一个adapter来和datas进行交流,adapter把datas变成ViewHolder的数据内容,这样RecyclerView就可以和数据进行交互了。这也是ListView的逻辑方式。但是RecyclerView同时还使用了一个LayoutManager,这样他就可以像GridView一样随意的变更布局格式了。然后他又有了自己的Recycler用于缓存管理view里面的内容。另外他还有getItemOffsets方法,ItemAnimator等等协助,于是有了动画,刷新,等等不同方面的支持。这些优点让它打败了ListView成为了主流的复用控件。

之前在ListView可以直到,ListView的复用就是从第n个item重新使用第一个item的view加载数据这种,无限循环使用,减少创建view所消耗的巨大开销。而RecyclerView在这一块也是这种原理,循环使用view。在缓存这个层面上,RecyclerView实际上并没有做出太大的创新,最大的创新来源于给每一个ViewHolder增加了一个UpdateOp,通过这个标志可以进行定向刷新指定的Item,并且通过Payload参数可以对Item进行局部刷新,提高了刷新时候的性能。这里也就不多赘述RecyclerView的绘制,直接进入核心的复用过程。

首先还是先放出Recycler类里面的几个关键的,也就是传说中的四级缓存

static final int DEFAULT_CACHE_SIZE = 2;//默认缓存的数量
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;//设置的缓存最大数量,默认为2
int mViewCacheMax = DEFAULT_CACHE_SIZE;//View的缓存的最大数量,默认为2
RecycledViewPool mRecyclerPool;//RecycledView,用来公用RecyclerView
//缓存的扩展,可以用来对指定的position跟Type进行缓存
private ViewCacheExtension mViewCacheExtension;
ArrayList<ViewHolder> mChangedScrap = null;//数据源更改过的AttachedScrap
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();//依附的缓存View
//缓存的全部View,包含可见跟不可见的ViewHolder
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder> mUnmodifiableAttachedScrap  = Collections.unmodifiableList(mAttachedScrap);//mAttachedScrap的不可变集合

mAttachedScrap: 用于缓存显示在屏幕上的 item 的 ViewHolder,RecyclerView 在 onLayout 时会先把 children 都移除掉,再重新添加进去,这个 List 是在布局中临时存放 children 的。和复用其实关系不大。

mViewCacheExtension: 留给开发者扩展

mCachedViews:这个集合里存的 ViewHolder 数据可以直接添加到 RecyclerView 中显示,不需要重新onBindViewHolder()。只有原来的item可以重新复用这个 ViewHolder。这里面存储两个ViewHolder

mRecyclerPool:用来缓存RecyclerView的集合。缓存了viewholder

在LayoutManager里面有这几个关键方法,在最后的next方法中跳回了recycler类,

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
  // layout algorithm:
  // 1) by checking children and other variables, find an anchor coordinate and an anchor
  //  item position.
  // 2) fill towards start, stacking from bottom
  // 3) fill towards end, stacking from top
  // 4) scroll to fulfill requirements like stack from bottom.
   //开始布局
   fill(recycler, mLayoutState, state, false);
}
  

1.fill(recycler, mLayoutState, state, false); //填充itemview
2.layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) 
3.View next(RecyclerView.Recycler recycler)

跟进next方法,进入到recycler.getViewForPosition()方法,继续跟进到下面的代码

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) { 
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            ...
            ...
}

在这里判断isPreLayout() 时,就去 mChangedScrap 中找。而这个isPreLayout() 在 LayoutManager 的 onLayoutChildren 前就会置为false,因此跳过了这一段,往后走

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) { 
          ...

    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是不是当前position的,不是就将viewholder置null
                if (!dryRun) {
           // we would like to recycle this but need to make sure it is not used by
           // animation logic etc.
                     holder.addFlags(ViewHolder.FLAG_INVALID);
                  if (holder.isScrap()) {
                         removeDetachedView(holder.itemView, false);
                         holder.unScrap();
                  } else if (holder.wasReturnedFromScrap()) {
                         holder.clearReturnedFromScrapFlag();
                  }
                  //
                  recycleViewHolderInternal(holder);
                }
                  holder = null;
             } else {
                  fromScrapOrHiddenOrCache = true;
            }
          }
    }       

          ...
}

在上面判断完之后走到了这里,也就是getScrapOrHiddenOrCachedHolderForPosition()方法

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun){ 
        final int scrapCount= mAttachScrap.size();
        //Try first for an exact, non-invalid match for scarp.
        for( int i = 0; i < scarpCount ; i++){
            final ViewHolder holder = mAttachScrap.get(i);
            if(!holder.wasReturnedFromScrap() && holder.getLayoutPosition() ==
                position && !holder.isInvalid() && (mState.mInPreLayout || 
                !holder.isRemoved())){
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
             }
        }
}

首先,去 mAttachedScrap 中寻找 position 一致的 viewHolder,需要确定这个 viewHolder 没有被移除,是否有效等条件,满足就返回这个 viewHolder。在RecyclerView重新layout的时候,比如Resume,会将所有chldren的holder移除掉,放到mAttachedScrap 中。在getScrapOrHiddenOrCachedHolderForPosition()方法中后面还会到hidden和mCached里去找holder。hidden没有比较清晰的描述,但是mCached就是之前说的二级缓存了。  遍历mCachedView,找到 position 一致的 ViewHolder,mCachedViews 里存放的 ViewHolder 的数据信息都保存着,只有原来的item可以重新复用这个 ViewHolder,新item无法从 mCachedViews 里拿 ViewHolder出来用。没有找到的话,就继续再找一遍,刚才是通过 position 来找,那这次就换成id,然后重复上面的步骤再找一遍,就进入了getScrapOrCachedViewForId()方法。

 // 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);
  // invalid view holders may be in cache if adapter has stable ids as they can be
                // retrieved via getScrapOrCachedViewForId
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
          Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }



if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                //依然是到ScrapView和CachedView中找,通过id
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

ViewPool 会根据不同的 viewType 创建不同的集合来存放 ViewHolder,那么复用的时候,只要 ViewPool 里相同的 type 有 ViewHolder 缓存的话,就将最后一个拿出来复用,不用像 mCachedViews 需要各种匹配条件,只要有就可以复用。拿到 ViewHolder 之后,还会再次调用一个方法叫 resetInternal() 来重置 ViewHolder,这样 ViewHolder 就可以当作一个全新的 ViewHolder 来使用了,因此从这里拿的 ViewHolder 都需要重新 onBindViewHolder() 了。

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

这样recyclerview的四级缓存已经找过一遍了,这时候如果还是没找到就只能新创建Holder。调用 Adapter 的 onCreateViewHolder 来创建一个新的 ViewHolder 使用。tryGetViewHolderForPositionByDeadline() 这个方法基本就结束了。这大概就是 RecyclerView 的复用机制。

在 RecyclerView 滑动时,会交由 LinearLayoutManager 的 scrollVerticallyBy() 去处理,然后 LayoutManager 会接着调用 fill() 方法去处理需要复用和回收的卡位,最终会调用 recyclerView() 方法开始进行回收工作。回收的itemView优先存放到mCachedViews,如果mCachedViews已经满了,就移除一个出去到RecycledViewPool中去。

回收复用涉及到的结构体两个:
mCachedViews 和 RecyclerViewPool

mCachedViews 优先级高于 RecyclerViewPool,回收时,最新的 ViewHolder 都是往 mCachedViews 里放,如果它满了,那就移出一个扔到 ViewPool 里好空出位置来缓存最新的 ViewHolder。

复用时,也是先到 mCachedViews 里找 ViewHolder,但需要各种匹配条件,概括一下就是只有原来位置的卡位可以复用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里没有,那么才去 ViewPool 里找。

在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一样,只要 type 一样,有找到,就可以拿出来复用,重新绑定下数据即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值