RecyclerView源码解析之缓存机制

序言

RecyclerView有三大典型的功能,一个是Recycler的缓存机制,一个LayoutManager的布局管理,一个ItemDecoration的分割线绘制;本文将结合源码讲解其缓存机制
此为笔者暂时性文章,完整博客请访问RecyclerView之三级缓存源码解析

正文

一. 缓存机制

(1). RecycledViewPool的缓存

  • RecycledViewPool也叫第三级缓存

  • 文档中说的是: 为多个RecyclerView提供的一个共用缓存池,如果想要通过RecyclerView缓存View的话,可以自己提供一个RecycledViewPool实例,并通过RecyclerView的setRecycledViewPool()方法来设置,如果不主动提供的话,RecyclerView会为自己主动创建一个

  • 首先来看其缓存方式: 其中有一个 SparseArray 类型的mScrap来缓存ViewHolder,每一个View Type 类型的Item都会有一个该缓存(源码如下),默认最大容量为5,但是可以通过recyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);来设置;

-

static class ScrapData {

    ArrayList mScrapHeap = new ArrayList<>();

    int mMaxScrap = DEFAULT_MAX_SCRAP; //每个View Type默认容量为5

    long mCreateRunningAverageNs = 0;

    long mBindRunningAverageNs = 0;

}

SparseArray mScrap = new SparseArray<>();

-

  • 至于这里的SparseArray,它是Android中的一个工具类,因为Android内存限制,所以产生了这样一个比HashMap轻量的类(具体可以参考博客)

  • 接下来看一下RecycledViewPool的存取方法;从这两个方法中,我们可以看出,在RecycledViewPool中缓存的ViewHolder之间是依靠 View Type 来区分的,也就是说,同一个View Type之间的ViewHolder缓存在RecycledViewPool中是没有区别的;如果我们没有重写ViewHolder的getItemViewType()方法,那么就默认只有一种View Type,默认为-1

-

public ViewHolder getRecycledView(int viewType) {

    ...

    return scrapHeap.remove(scrapHeap.size() - 1);

}

public void putRecycledView(ViewHolder scrap) {

    final int viewType = scrap.getItemViewType();

    final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;

    ...

    scrap.resetInternal();

    scrapHeap.add(scrap);

}

-

  • 下面我们看一下在将一个ViewHolder放进RecycledViewPool之前,都会做什么处理(主要代码如下);需要注意的是,下面的注释中有这样一句话:Pass false to dispatchRecycled for views that have not been bound.,大意为:当一个ViewHolder没有绑定view的时候传递false给dispatchRecycled;换句话说就是,下面dispatchViewRecycled(holder);的功能就是清除ViewHolder相关绑定的操作;另外我们再来看一下对于RecycledViewPool的文档描述中有这样一句话:RecycledViewPool lets you share Views between multiple RecyclerViews.,即通过RecycledViewPool可以在不同的RecyclerView之间共享View(实际上是ViewHolder),所以,这里我们也就可以理解下面holder.mOwnerRecyclerView = null清除与原来RecyclerView关联的操作了(因为不清除的话,在多个RecyclerView之间共享就会出现问题);那么到这里我们对于RecycledViewPool中的ViewHolder就有了大致的了解了,总结一下就是: 当一个ViewHolder被缓存进入该pool的时候,除了其自身的View Type以外,其自身与外界的绑定关系,flag标志,与原来RecyclerView的联系等信息都被清除了,那么理所当然的是,对于处于pool中的ViewHolder的查询,就应该通过View Type来确定了,也就是上面我们所说的

-

扫描二维码关注公众号,回复: 3457344 查看本文章
/**

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

}

-

  • 下面我们应该顺着这条线索,继续搜索哪种情况下会将一个ViewHolder扔进RecycledViewPool中;这里笔者找到以下几种情况:
  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()中进行回收

-

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,

            RecyclerView.State state, boolean stopOnFocusable) {

    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {

        ...

        layoutChunk(recycler, state, layoutState, layoutChunkResult);

        ...

        recycleByLayoutState(recycler, layoutState);

    }

}

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {

    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {

        recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);

    } else {

        recycleViewsFromStart(recycler, layoutState.mScrollingOffset);

    }

}

void recycleViewHolderInternal(ViewHolder holder) {

    ...

    int cachedViewSize = mCachedViews.size();

    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {

        recycleCachedViewAt(0); //回收进pool中

        cachedViewSize--;

    }

    /存入第一级缓存

    mCachedViews.add(targetCacheIndex, holder);

    ...

}

-

  1. 在我们继续进行下一步分析之前,笔者想先来总结一下上面我们在寻找pre_layout和post_layout区别的时候所经过的过程: 我们主要围绕的是RecyclerView的onMeasure()方法,经过了dispatchLayoutStep1()和dispatchLayoutStep2()两个主要的过程,前一个负责预布局(pre_layout),后一个负责真正的布局(post_layout);其实到这里,布局过程还没有真正的完成,因为我们还没有弄清楚的是Item的滚动动画

  2. 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()方法处理);

  3. 但是我们最终追寻下去,可以看出的是在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种情况;话不多说,继续分析(万里长征还未过半…)

  2. 我们继续进入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缓存

  • 这里插曲可能稍微长了一点,但是,笔者感觉这是值得的;现在,让我们继续最初的话题: 什么情况下一个ViewHolder会被扔进Pool中呢?这里笔者再次回顾一下:
  1. 在View Cache中的Item被更新或者被删除时(存满溢出时)

  2. LayoutManager在pre_layout过程中添加View,但是在post_layout过程中没有添加该View(数据集改变,如删除)

  • 到这里RecyclerView的第三级缓存差不多就分析完了,接下来,我们再看一下与其紧密相关的第一级缓存

猜你喜欢

转载自blog.csdn.net/HusterYP/article/details/79689383
今日推荐