Android知识Android进阶之路Android技术知识

RecyclerView之三级缓存源码解析

2018-03-29  本文已影响0人  非专业程序员

序言

  1. RecyclerView有三大典型的功能,一个是Recycler的缓存机制,一个LayoutManager的布局管理,一个ItemDecoration的分割线绘制;本文将结合源码讲解其缓存机制
  2. 更多相关的源码解析见RecyclerView之ItemDecoration

正文

缓存机制

(1). RecycledViewPool的缓存

  static class ScrapData {
      ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
      int mMaxScrap = DEFAULT_MAX_SCRAP; //每个View Type默认容量为5
      long mCreateRunningAverageNs = 0;
      long mBindRunningAverageNs = 0;
  }
  SparseArray<ScrapData> mScrap = new SparseArray<>();
  public ViewHolder getRecycledView(int viewType) {
      ...
      return scrapHeap.remove(scrapHeap.size() - 1);
  }

  public void putRecycledView(ViewHolder scrap) {
      final int viewType = scrap.getItemViewType();
       final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
      ...
      scrap.resetInternal();
      scrapHeap.add(scrap);
  }
    /**
    * Pass false to dispatchRecycled for views that have not been bound.
    * @param dispatchRecycled True to dispatch View recycled callbacks.
    */
    void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
        ...
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
            //标志(flag)清除
            holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
            ViewCompat.setAccessibilityDelegate(holder.itemView, null);
        }
        if (dispatchRecycled) {
            //绑定清除
            dispatchViewRecycled(holder);
        }
        //与RecyclerView的联系清除
        holder.mOwnerRecyclerView = null;
        //缓存入pool
        getRecycledViewPool().putRecycledView(holder);
    }
  1. 在View Cache(第一级缓存)中的Item被更新或者被删除时(即从Cache中移出的ViewHolder会进入pool中);可以看出的时,更新和删除操作时,将ViewHolder回收进pool中都是通过recycleCachedViewAt()方法,如下可知,其只是调用了上面的ViewHolder清除工作,同时删除了Cache中的缓存
    //当View Cache中Item更新时
    //但是什么时候会更新呢: 可以想像的一种情况是当有Item缓存进入View Cache中时
    void updateViewCacheSize() {
        ...
        // first, try the views that can be recycled
        for (int i = mCachedViews.size() - 1;
                i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
            recycleCachedViewAt(i);
        }
    }

    //当View Cache中Item删除时
    void recycleAndClearCachedViews() {
        final int count = mCachedViews.size();
        for (int i = count - 1; i >= 0; i--) {
            recycleCachedViewAt(i);
        }
        mCachedViews.clear();
        ...
    }
    
    //该方法中调用了上面所说的回收进pool中的清除工作,同时将Cache中的缓存删除
    void recycleCachedViewAt(int cachedViewIndex) {
        ....
        addViewHolderToRecycledViewPool(viewHolder, true);
        mCachedViews.remove(cachedViewIndex);
    }
  1. LayoutManager在pre_layout过程中添加View,但是在post_layout过程中没有添加该View;当然,在寻找该过程对应的源码的时候,我们首先应该弄清楚的是pre_layout和post_layout是什么(所以在继续讲解之前,笔者打算先讲一个小插曲)

(2) 一个小插曲: pre_layout和post_layout

  1. 关于这两者应该看的是RecyclerView的onMeasure()方法;如下可知,onMeasure中主要是分为两步,即dispatchLayoutStep1()和dispatchLayoutStep2();
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout.mAutoMeasure) {
        ...
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
        // consistency
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        dispatchLayoutStep2();
        ...
    } else {
        ...
    }
}
  1. 我们先来看即dispatchLayoutStep1()中做的事情;该方法的注释中我们知道其做的事情: (1). 处理Adapter的更新; (2). 决定是否是否使用动画; (3). 存储与当前View相关的信息; (4). 进行预布局(pre_layout); 这里很明显,我们关注的重点应该放在预布局上,从下面代码中的注释可以看出,预布局分为两步: 第一步是找到所有没有被remove的Item,进行预布局准备; 第二步是进行真正的预布局,从源代码注释中,我们可以看出,预布局时会使用Adapter改变前的Item(包括其位置和数量)来布局,同时其使用的Layout尺寸也是改变前的尺寸(这点可以从上面onMeasure()方法中对dispatchLayoutStep2()方法的注释可以看出(大意为: 预布局应该发生在旧的尺寸上),这是为了和正真改变后的布局相对比,来决定Item的显示(可能这里读者还是不清楚pre_layout的作用,不要紧,下面会详细解释,这里需要了解的只是在该方法中所做的事情)
/**
 * The first step of a layout where we;
 * - process adapter updates
 * - decide which animation should run
 * - save information about current views
 * - If necessary, run predictive layout and save its information
 */
private void dispatchLayoutStep1() {
    ...
    //情况(1)和(2)
    processAdapterUpdatesAndSetAnimationFlags();
    //情况(3)
    ...
    //情况(4): 预布局
    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
    }
    if (mState.mRunPredictiveAnimations) {
        /**
        Step 1: run prelayout: This will use the old positions of items. The layout  manager is expected to layout everything, even removed items (though not to add removed items back to the container). This gives the pre-layout position of APPEARING views which come into existence as part of the real layout.
        */
    }
}
  1. 接下来是实现真正的布局,即dispatchLayoutStep2()进行的post_layout;可以看出,这里主要是对子View进行Layout,需要注意的是,在onMeasure()中,在进行dispatchLayoutStep2()操作之前,还进行了mLayout.setMeasureSpecs(widthSpec, heightSpec);也就是设置改变后真正的布局尺寸;但是当查看LayoutManager的onLayoutChildren()方法时,我们发现其是一个空方法,所以应该找其实现类(这里以LinearLayoutManager为例)
/**
 * The second layout step where we do the actual layout of the views for the final state.
 * This step might be run multiple times if necessary (e.g. measure).
 */
private void dispatchLayoutStep2() {
    ...
    // Step 2: Run layout
    mLayout.onLayoutChildren(mRecycler, mState);
}
  1. LinearLayoutManager的onLayoutChildren()过程: 在其源码中介绍了Layout算法: (1). 首先找到获得焦点的ItemView; (2). 从后往前布局或者从前往后布局(这个主要是与滚动出屏幕的Item的回收方向相关); (3). 滚动; 其中最主要的是一个fill()方法
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()方法: 从其参数可以猜测的是,该方法与Item的填充和回收相关;其主要过程是通过下面while循环中不断的填充(layoutChunk)和回收Item(recycleByLayoutState)完成;而在recycleByLayoutState()中分为两种情况处理:即向上滚动和向下滚动,其中回收的条件是当Item滚动出屏幕且不可见时(在recycleViewsFromEnd()和recycleViewsFromStart()中都对滚动的边界做了判断),而最终回收调用的是recycleViewHolderInternal()方法;在recycleViewHolderInternal()中,其首先判断了如果第一级缓存满了的话,先将以前存入的Item移出,并存入Pool中,之后再缓存当前Item;这里也就是对应了RecycledViewPool缓存的第一种情况;还需要注意的是,当Item正在执行动画的时,会导致回收失败,此时会在ItemAnimatorRestoreListener.onAnimationFinished()中进行回收
  1. 在我们继续进行下一步分析之前,笔者想先来总结一下上面我们在寻找pre_layout和post_layout区别的时候所经过的过程: 我们主要围绕的是RecyclerView的onMeasure()方法,经过了dispatchLayoutStep1()和dispatchLayoutStep2()两个主要的过程,前一个负责预布局(pre_layout),后一个负责真正的布局(post_layout);其实到这里,布局过程还没有真正的完成,因为我们还没有弄清楚的是Item的滚动动画
  1. onMeasure过程之后,我们应该将目光聚焦在layout过程,在RecyclerView的onLayout()方法中,其关键的是调用了dispatchLayout(),关于该方法,源码注释给出了明确的说明:dispatchLayout()方法中封装了与Item(出入)动画相关的操作,当重新布局(可能原因比如:Adapter改变,Item滑动等)之后,Item的改变类型大概有一下几种: (1). PERSISTENT: 即在pre_layout和post_layout中都是可见的(由animatePersistence()方法处理); (2). REMOVED: 在pre_layout中可见,但是被删除了(对应数据的删除)(由animateChange()方法处理);(3). ADDED: 在pre_layout中不存在,但是被添加进的Item(对应数据的添加)(由animateChange()方法处理); (4). DISAPPEARING: 数据集没有改变,但是Item由可见变为不可见(即Item滑动出屏幕)(由animateDisappearance()方法处理); (5). APPEARING: 数据集没有改变,但是Item由不可见变为可见(对应Item滑动进入屏幕)(由animateAppearance()方法处理);
  1. 但是我们最终追寻下去,可以看出的是在dispatchLayout()中,又将一系列处理完全交给了dispatchLayoutStep3()方法来处理;从下面代码中可以看出,其最终通过回调ViewInfoStore.ProcessCallback来处理上面的四种动画
  private void dispatchLayoutStep3() {
      ...
      // Step 4: Process view info lists and trigger animations
      mViewInfoStore.process(mViewInfoProcessCallback);
  }
  1. 到这里为止,我们对于pre_layout和post_layout的区别应该很清楚了;这里举个例子来进一步理解一下: 考虑一种情况,如果现在界面上有两个Item a,b,并且占满了屏幕,此时如果删除b使得c需要进入界面的话,那么我们虽然知道c的最终位置,但是我们如何知道c该从哪里滑入屏幕呢,很明显,不可能默认都从底部开始滑入,因为很明显的是还有其他情况;所以在这里Google的解决办法是请求两个布局: pre_layout和post_layout; 当Adapter改变即这里的b被删除的时候,作为一个事件触发,此时pre_layout将加载c(但是此时c仍然是不可见的),然后在post_layout中去加载改变后的Adapter的正常布局,通过前后两个布局对c位置的比较,我们就可以知道c该从哪里滑入;另外,还有一种情况是,如果b只是被改变了呢(并没有被删除),那么此时,pre_layout仍然会加载c,因为b的改变可能会引起b高度的改变而使得c有机会进入界面;但是,当Adapter改变完成之后,发现b并没有改变高度,换句话说,就是c还是不能进入界面的时候,此时Item c将被扔进该pool,这种情况也就是上面说的RecycledViewPool进行回收的第2种情况;话不多说,继续分析(万里长征还未过半...)
  1. 我们继续进入mViewInfoStore.process()方法,该方法属于ViewInfoStore类,对于该类的描述是:对View进行跟踪并运行相关动画,进一步解释就是执行Item改变过程中的一些动画;继续看其在process()方法做了什么:其实在该方法中进行了许多的情况的判断,这里笔者只是抽取出了对应当前情况的处理,可以看出,当similar to appear disappear but happened between different layout passes时,只是简单的调用了ProcessCallback.unused(),而在unused()中,也只是对Item进行了回收(如下);但是,值得注意的是,ViewInfoStore.process()方法进行的处理,远不止如此,实际上,我们还有意外收获,这里只需要记住该方法就好了,具体,下面还会再分析
  void process(ProcessCallback callback) {
      ...
      // similar to appear disappear but happened between different layout passes.
      // this can happen when the layout manager is using auto-measure
      callback.unused(viewHolder);
      ...
  }

  @Override
 public void unused(ViewHolder viewHolder) {
      mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
  }
  1. 最后笔者还想附带提一下的是,关于Item出入屏幕动画处理的那几个方法(即上面的animatePersistence(),animateChange()等)都是位于ItemAnimator中,这是一个abstract的类,如果想要自定义的Item的出入动画的话,可以继承该类,并通过recyclerView.setItemAnimator();来进行设置

(1-). 又见RecycledViewPool缓存

  1. 在View Cache中的Item被更新或者被删除时(存满溢出时)
  2. LayoutManager在pre_layout过程中添加View,但是在post_layout过程中没有添加该View(数据集改变,如删除)

(3). View Cache缓存

public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
}
void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        ...
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

(4). ViewCacheExtension

  1. 其position固定(比如广告之类)
  2. 不会改变(view type等)
  3. 数量合理,以便可以保存在内存中
    现在,为了避免这些Item的重复绑定,就可以使用ViewCacheExtension(需要注意的是,这里不能使用RecycledViewPool,因为其缓存的ViewHolder需要重新绑定,同时也能使用View Cache,因为其中的ViewHolder是不区分view type的),比如下面的示例代码
SparseArray<View> specials = new SparseArray<>();
...
recyclerView.getRecycledViewPool().setMaxRecycledViews(SPECIAL, 0);
recyclerView.setViewCacheExtension(new RecyclerView.ViewCacheExtension() {
    @Override
    public View getViewForPositionAndType(RecyclerView.Recycler recycler,
                                            int position, int type) {
        return type == SPECIAL ? specials.get(position) : null;
    }
});
...
class SpecialViewHolder extends RecyclerView.ViewHolder {
        ...     
    public void bindTo(int position) {
        ...
        specials.put(position, itemView);
    }
}

(5). 小结

(6). 参考文章

上一篇 下一篇

猜你喜欢

热点阅读