RecyclerView源码分析(六)局部刷新

为什么要局部刷新

记得在第一章,我们说过RecyclerView很有优势的地方就是支持局部刷新,我们可以可以直接渲染单个数据发生变化的卡片。
其实这种可以复用的列表,整体优化就两部分

  1. 减少创建viewHolder的调用
  2. 减少渲染也就是bind的次数

减少创建肯定是利用上一章讲的缓存知识了。减少渲染呢,就是利用精准的刷新,只刷新变化的部分。
现在显示有一个列表,突然有一个数据变化了,这个数据项目每个列表上面都有。举例比如关注数或者阅读数。我们很难知道这个item的精确位置。如果知道显示的是第几个,我们直接拿到ViewHolder刷新即可。但是我们不知道。所以选择全局刷新,也就是常用的notifyDataSetChanged方法,调用这个方法万事具备,不需要关心哪儿发生了变化。最新的数据显示了,也没有什么副作用。
这个方法会重新渲染整屏幕的显示元素,但是我们只想重新渲染一个数字呀。不要小看这重新渲染的多余部分,可能是压死骆驼的最后一颗稻草,所以我们需要尽我们所能减少渲染的次数。所以RecycleView提供了局部刷新的能力。我们知道单独渲染一个item即可,更甚者可以单独标记只渲染单个元素。
关于上面的论述有几个问题:

  1. notifyDataSetChanged会刷新全屏的数据,代码上相对局部刷新多做了什么,对缓存有什么影响?
  2. 局部刷新怎么实现的只刷新局部数据的呢?
  3. setAdapter方法也可以刷新数据,他是怎么做到的?
  4. 日常怎么使用局部刷新?

刷新的方式

更换数据集的操作

  1. setAdapter/swapAdater
  2. notifyDataSetChanged
  3. 局部刷新

这些都可以刷新数据,但是每一个都是怎么实现的,都有什么不同呢?这都是我们需要了解的。刷新数据首选的当然是局部刷新,再者notifyDataSetChanged。最差setAdapter/swapAdater。为什么这么讲呢,我们需要从源码上分析一下。先从最差的看起吧!

setAdapter/swapAdater

setAdapter和swapAdater的区别是,setAdapter默认是替换适配器,默认和现在使用的ViewHolder是不兼容的,既然不兼容就会对所有的缓存赶尽杀绝。swapAdater默认和现在使用的ViewHolder是兼容的,所以现在的缓存还是会加以利用,但是各级缓存都是怎么变化的呢,都做了那儿些具体操作呢。我们详细来看下。 两个方法内部调用的方法大致一样,

public void setAdapter(Adapter adapter) {
    setLayoutFrozen(false);
    setAdapterInternal(adapter, false, true);
    requestLayout();
}
复制代码
public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) {
    setLayoutFrozen(false);
    setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
    requestLayout();
}
复制代码

可以看出主要的差别就是调用setAdapterInternal的参数不同,swapAdapter有一个参数removeAndRecycleExistingViews,从变量名可以看出是否要移除显示的view,并进行recycle操作,这里我们假设传入的true。
两个方法都调用了requestLayout(),所以都进行了刷新操作,所以只要更改了适配器就可以进行刷新了,刷新的逻辑就是这么简单粗暴。我们看下setAdapterInternal更改适配器的逻辑。

setAdapter传入:(adapter, false, true);
swapAdapter传入:((adapter, true, removeAndRecycleExistingViews(true)))

compatibleWithPrevious:是否和现有Adapter兼容
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
        boolean removeAndRecycleViews) {
    if (mAdapter != null) {
        mAdapter.unregisterAdapterDataObserver(mObserver);
        mAdapter.onDetachedFromRecyclerView(this);
    }
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        // 移动一二级缓存到四级缓存,清空一二级缓存
        removeAndRecycleViews();
    }
    mAdapterHelper.reset();
    final Adapter oldAdapter = mAdapter;
    mAdapter = adapter;
    if (adapter != null) {
        adapter.registerAdapterDataObserver(mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (mLayout != null) {
        mLayout.onAdapterChanged(oldAdapter, mAdapter);
    }
    // 通过compatibleWithPrevious是否清空所有缓存
    mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
    mState.mStructureChanged = true;
    // 对二级缓存进行清空
    markKnownViewsInvalid();
}
复制代码
  1. 首先因为替换了Adapter,所以要通过unregister/register和对原来的的观察者监听进行替换。
  2. 调用了removeAndRecycleViews方法remove掉现有add的child item,再进行recycly回收,也就是都放入四级缓存中。这个操作取决于两个传入的参数,通过调用方,可知setAdapter肯定会进行调用,swapAdapter主要是根据removeAndRecycleViews参数。
  3. 进行了引用的替换
  4. mRecycler.onAdapterChanged内部主要根据compatibleWithPrevious变量,判断是否清空第四级缓存。如果setAdapter调用过来,可以看到直接清空了所有缓存

所以这里也看到了刷新UI的主要原理,就是通过更改adapter的引用,判断是否和原来的adapter兼容,左缓存的处理。最后通过requestLayout进行刷新。
如果通过setAapter刷新数据,可以看到直接清空了所有缓存,相当缓存直接没有,影响很大。没有缓存的RecycleView是没有任何意义的。如果通过swapAdapter,我们也会清空二级缓存,所以影响也是比较大的。
可见通过替换adapter进行刷新是很蠢的

notifyDataSetChanged

直接调用adapter的notifyDataSetChanged的逻辑很简单,就是触发观察者监听的逻辑。这个观察者和被观察者是在上面的setAdapterInternal方法内部进行绑定的。观察者直接调用RecyclerViewDataObserver的onChanged方法。

public final void notifyDataSetChanged() {
    mObservable.notifyChanged();
}

@Override
public void onChanged() {
    assertNotInLayoutOrScroll(null);
    mState.mStructureChanged = true;

    processDataSetCompletelyChanged(true);
    if (!mAdapterHelper.hasPendingUpdates()) {
        requestLayout();
    }
}
复制代码

onChanged内部的逻辑比较简单,主要的逻辑都在processDataSetCompletelyChanged中,hasPendingUpdates方法判断是否调用了局部刷新。如果没有调用局部刷新,这里会直接requestLayout方法。看下processDataSetCompletelyChanged,内部直接调用了markKnownViewsInvalid方法。

void markKnownViewsInvalid() {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder != null && !holder.shouldIgnore()) {
            holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
        }
    }
    markItemDecorInsetsDirty();
    mRecycler.markKnownViewsInvalid();
}
复制代码

markKnownViewsInvalid方法内部主要主要把屏幕上的viewHolder状态设置INVALID和UPDATE。通过上一篇缓存的分析,这里表示在进行回收时,打了这些标记的缓存不能放入一集、二级缓存,直接到四级。可怜。 调用markItemDecorInsetsDirty方法标记ItemDecorator的各种属性标记为无效。对缓存的重头戏在markKnownViewsInvalid方法中。

void markKnownViewsInvalid() {
    final int cachedCount = mCachedViews.size();
    for (int i = 0; i < cachedCount; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder != null) {
            holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
            holder.addChangePayload(null);
        }
    }

    if (mAdapter == null || !mAdapter.hasStableIds()) {
        // we cannot re-use cached views in this case. Recycle them all
        recycleAndClearCachedViews();
    }
}
复制代码

首先把二级缓存中的ViewHolder拿出来,状态设置为INVALID和UPDATE。表示从缓存中取出时,要重新bind。 如果没有设置hasStableIds,那么会调用recycleAndClearCachedViews清空二级缓存并下放到四级缓存中。

看完notifyDataSetChanged的操作。感觉比setAdapter之类的强不少,但也不咋地。对缓存来说只是清空了二级缓存,都下放到了四级缓存中。并且在进行回收时,也是直接放到了四级缓存中。 一二级缓存完全失效。四级缓存还在工作。 这样所有的item不管变没变,都要重新bind一下,如果我们只想刷新一个数字,使用这种方式,却要刷新整个区域。效率可想而知的低。
至于notifyDataSetChanged怎么实现刷新的,比较简单。就是利用缓存的全部失效,并调用requestLayout方法,进行重新的bind,即完成了刷新。

局部刷新

局部刷新的方法就那么几个

  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemMoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)
  • notifyItemRangeMoved(int, int)

前三个方法指单个item的刷新,后三个是整体区间的刷新。单个刷新也是调用后者区域刷新的,只是刷新的长度是1。

局部刷新的方法和notifyDataSetChanged方法的调用机制一样,都是通过观察者模式通知观察着,调用RecyclerViewDataObserver的onItemRangeChanged/onItemRangeInserted/onItemRangeRemoved/onItemRangeMoved方法。内部调用的机制都是一致的。

@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
        triggerUpdateProcessor();
    }
}

@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
        triggerUpdateProcessor();
    }
}

remove和move的逻辑类似,这里就不列出了。
复制代码

我们这里拿notifyItemChanged举例,其他的调用过程类似,分析调用过程就用一个举例就可以。notifyItemChanged会通过观察者调用onItemRangeChanged。 onItemRangeChanged内部通过判断AdapterHelper的onItemRangeChanged,是否需要执行triggerUpdateProcessor方法,逻辑很简单。我们先看下AdapterHelper的onItemRangeChanged方法。

boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
   if (itemCount < 1) {
       return false;
   }
   mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
   mExistingUpdateTypes |= UpdateOp.UPDATE;
   return mPendingUpdates.size() == 1;
}
复制代码

内部新建了一个UpdateOp插入mPendingUpdates中,mPendingUpdates存储了所有局部刷新的标记数据,比如change就对应UPDATE类型的UpdateOp,Inserted对应ADD类型的UpdateOp。以后处理局部刷新,都是从这个集合mPendingUpdates中,拿局部刷新的数据进行处理。 这个函数的返回值是mPendingUpdates里面的数据是否等于1,在里面只有一个数据的情况下,才调用triggerUpdateProcessor。如果我们调用多次局部刷新方法,那么只有第一次才调用triggerUpdateProcessor。

void triggerUpdateProcessor() {
    POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16
    
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
复制代码

triggerUpdateProcessor这个方法,内部的逻辑主要分两个部分,如果当前安卓版本大于16并且RecyclerView是FixedSize的,并且已经attach到了window。那么会在下一个周期执行mUpdateChildViewsRunnable。反之就直接requestLayout。看来刷新的套路都是一样的,设置数据,调用requestLayout进行刷新。只是局部刷新只是插入了一个UpdateOp信息单元。看来是里面数据的处理是在下一次布局时处理的。
triggerUpdateProcessor里的两种情况,主要的变量在mHasFixedSize中。如果设置了mHasFixedSize,表示RecyclerView的高度不受adapter数据的影响,高度是不变的。如果高度不变,在数据变化时,我们不用调用measure的,也就是不用直接调用requestLayout方法。如果不是mHasFixedSize,肯定是要requestLayout重新测量高度。可想而知,两个分支,主要测不测量高度为主的,也就是在mUpdateChildViewsRunnable这个runnable中不会执行measure,直接进行layout。
我们具体看下猜想是否正确。这个runnable内部直接执行了consumePendingUpdateOperations方法。

mHasFixedSize下的局部刷新

void consumePendingUpdateOperations() {
    if (!mAdapterHelper.hasPendingUpdates()) {
        // 没有局部刷新,直接return
        return;
    }
    // 如果它只是一个项目更改(没有 add-remove-notifyDataSetChanged)
    // 我们可以检查是否有任何可见项目受到影响,如果没有,则忽略更改。
    if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper
            .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
                    | AdapterHelper.UpdateOp.MOVE)) {
        // 只进行notifyItemChanged操作,没有其他
        。。。
        mAdapterHelper.preProcess();
        if (!mLayoutWasDefered) {
            if (hasUpdatedView()) {
                dispatchLayout();
            } else {
                mAdapterHelper.consumePostponedUpdates();
            }
        }
        。。。
    } else if (mAdapterHelper.hasPendingUpdates()) {
        。。。
        dispatchLayout();
        。。。
    }
}
复制代码

看上面的代码,验证了我们之前的猜想。内部的刷新逻辑直接调用了dispatchLayout方法,直接进行了layout的操作,结合第二章对绘制的讲解。layout步骤也是执行调用的dispatchLayout。
consumePendingUpdateOperations方法内部有两个分支

  1. 一种是只进行了notifyItemChanged操作,没有其他的局部刷新操作。
  2. 一种是有其他操作的情况。

不单有Changed的操作情况下

直接调用dispatchLayout重新布局,没什么可说的。内部会分别调用dispatchLayout的三个步骤,内部会处理局部刷新的数据。这个逻辑下面会讲到。

单有Changed的操作情况下

如果只有Changed的操作的情况下。代码上大体的操作是,先调用preProcess进行预处理,内部主要是进行mPendingUpdates集合的处理,集合只有Changed的类型,对标记的ViewHolder加UPDATE标记。再通过hasUpdatedView方法进行判断,这个方法取屏幕上的ViewHolder,看是否有设置了UPDATE标记的ViewHolder

  1. 如果有就调用dispatchLayout方法进行重新布局。
  2. 如果没有直接调用consumePostponedUpdates清空mPendingUpdates。

有两种情况,第一种是notifyItemChanged可见的item,还有一种是notifyItemChanged不可见的item。比如现在可见的position0到3,那么notify:5的position就是第二种情况。刷新不可见的item,我们肯定不需要重新布局的,毕竟看不到。
所以这里的逻辑可以理解为,检查是否notify一个可见的item,如果是是可见的,那么需要重新布局,也就是调用dispatchLayout,如果不可见呢,不刷新布局。
我们线详细看下preProcess方法,这里对局部刷新的数据进行了预处理。

注意下列方法上下文,mPendingUpdates中只有Changed的操作,对应状态就是UpdateOp.UPDATE。

void preProcess() {
    mOpReorderer.reorderOps(mPendingUpdates);
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            。。。
            case UpdateOp.UPDATE:
                applyUpdate(op);
                break;
            。。。
        }
    }
    mPendingUpdates.clear();
}
复制代码

由于只有UpdateOp.UPDATE的数据,隐藏了其他操作的逻辑。
先调用了mOpReorderer.reorderOps对mPendingUpdates进行重新排序,内部逻辑是:由于移动操作破坏了连续性,需要把移动操作放到最后。这里对只有更新操作的集合没影响。最后清空了mPendingUpdates集合。 主要的逻辑都在AdapterHelper#applyUpdate中。

private void applyUpdate(UpdateOp op) {
    int tmpStart = op.positionStart;
    int tmpCount = 0;
    int tmpEnd = op.positionStart + op.itemCount;
    int type = -1;
    for (int position = op.positionStart; position < tmpEnd; position++) {
        RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
        if (vh != null || canFindInPreLayout(position)) {
            if (type == POSITION_TYPE_INVISIBLE) {
                // 变成不可见
                UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
                        op.payload);
                dispatchAndUpdateViewHolders(newOp);
                tmpCount = 0;
                tmpStart = position;
            }
            type = POSITION_TYPE_NEW_OR_LAID_OUT;
        } else {
            if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
                // 变成可见
                UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
                        op.payload);
                postponeAndUpdateViewHolders(newOp);
                tmpCount = 0;
                tmpStart = position;
            }
            type = POSITION_TYPE_INVISIBLE;
        }
        tmpCount++;
    }
    if (tmpCount != op.itemCount) {
        // 不相等,说明发生了可见性切换,处理后段的ViewHolder
        Object payload = op.payload;
        recycleUpdateOp(op);
        op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
    }
    if (type == POSITION_TYPE_INVISIBLE) {
        dispatchAndUpdateViewHolders(op);
    } else {
        postponeAndUpdateViewHolders(op);
    }
}
复制代码

applyUpdate方法内部处理了两个状态POSITION_TYPE_INVISIBLE和POSITION_TYPE_NEW_OR_LAID_OUT,可以理解为可见和不可见。代码中通过mCallback.findViewHolder(position)获取ViewHolder时,不为空表示可见,当前的状态被设置成POSITION_TYPE_NEW_OR_LAID_OUT,为空不可见时,状态则为POSITION_TYPE_INVISIBLE。

applyUpdate方法内部处理了从可见item到不可见item,再到可见的item的处理。我们看到了状态不同时的处理。因为notifyItemRangeChanged的区间可能非常大,跨越了可见不可见。可能从不可见跨越可见再到不可见。tmpCount变量主要存储了相同区域的连续个数,当可见状态切换时,

  1. 变为可见则执行postponeAndUpdateViewHolders
  2. 变为不可见则执行dispatchAndUpdateViewHolders

通过上面的分析,applyUpdate方法的内部大体结构比较清晰了。先处理可见不可见转变过程中的前段的viewHolder,当tmpCount != op.itemCount时,后段还有没有处理的ViewHolder,这时新建一个单独这段的UpdateOp,根据当前的可见状态,调用不同的方法进行处理。
主要的逻辑都集中在dispatchAndUpdateViewHolders(处理不可见)和postponeAndUpdateViewHolders(处理可见)中了,它们有什么区别呢,我们具体看下。

postponeAndUpdateViewHolders方法直接调用了RecyclerView的viewRangeUpdate方法。

void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    final int positionEnd = positionStart + itemCount;

    for (int i = 0; i < childCount; i++) {
        final View child = mChildHelper.getUnfilteredChildAt(i);
        final ViewHolder holder = getChildViewHolderInt(child);
        if (holder == null || holder.shouldIgnore()) {
            continue;
        }
        if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
            holder.addFlags(ViewHolder.FLAG_UPDATE);
            holder.addChangePayload(payload);
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
    }
    mRecycler.viewRangeUpdate(positionStart, itemCount);
}

复制代码

viewRangeUpdate的代码中先把显示的所有ViewHolder在notify的区间内设置FLAG_UPDATE的标记位。这样在插入缓存中提取的时候,会进行重新bind,达到了刷新的目的。
最后调用了mRecycler.viewRangeUpdate方法,对缓存进行了更新。

void viewRangeUpdate(int positionStart, int itemCount) {
    final int positionEnd = positionStart + itemCount;
    final int cachedCount = mCachedViews.size();
    for (int i = cachedCount - 1; i >= 0; i--) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder == null) {
            continue;
        }

        final int pos = holder.mPosition;
        if (pos >= positionStart && pos < positionEnd) {
            holder.addFlags(ViewHolder.FLAG_UPDATE);
            recycleCachedViewAt(i);
        }
    }
}
复制代码

这里对缓存的处理,主要是对二级缓存的处理,对所有区间内的二级缓存设置FLAG_UPDATE标记位。即提取需要重新bind。最后调用recycleCachedViewAt方法下放缓存到四级缓存,并在二级缓存中去除。

局部刷新总体逻辑

由于局部刷新的逻辑比较繁琐,到这里我们可以初步总结下局部刷新的逻辑。
我们调用局部刷新的方法,不管是增删改移,都会往mPendingUpdates加入待处理的UpdateOp数据,内部封装了我们调用局部刷新的各项数据,包括动作的标记cmd、有效载荷payload、生效起始位置itemCount等。加入完成后,只有集合在只有一个元素的情况下,会调用triggerUpdateProcessor方法。这里为什么要过滤只有一个元素才生效呢?因为triggerUpdateProcessor内部会调用requestLayout,多次调用时没有意义的。triggerUpdateProcessor方法内部有分了两种情况。如果没有固定高度,就需要调用requestLayout,进行重新的测绘流程了。有固定高度,说明不需要重新measure,直接layout即可,会执行mUpdateChildViewsRunnable力的逻辑。这个runnable里面也有两种情况,局部刷新是否只有改操作也就是changed操作的两个分支。不单有change会直接调用dispatchLayout进行重新layout。单有changed的话,就会调用preProcess处理单独的changed事件。并清空所有局部刷新的数据。这种情况下只会刷新区间内的可见ViewHolder的状态和下放二级缓存到四级缓存并更改状态。不会调用dispatchLayout重新layout。

总结为以下流程图

graph TD
调用局部刷新 --> 有固定高度不需要重新measure --> 单有changed操作 --> 1.执行preprocess处理changed数据 --> change了显示的holder --> 更改标记并调用dispatchLayout
 1.执行preprocess处理changed数据 --> change了没有显示的holder  --> 更新缓存标记并放入四级缓存
有固定高度不需要重新measure --> 不单有changed操作 --> 调用dispatchLayout重新layout
调用局部刷新 --> 没有固定高度  --> 调用requestLayout重新走测绘流程

可以看出总共就三个分支,第一个我们已经讲过了。第二个第三个的去呗主要是是否执行了measure测量流程,我们主要还是分析局部刷新的问题,实在layout布局阶段处理的。所以我们直接分析dispatchLayout的关于局部刷新的处理。

RecyclerView#dispatchLayout下的局部刷新

dispatchLayout方法的dispatchLayoutStep2中主要通过调用mAdapterHelper.consumeUpdatesInOnePass()进行局部刷新数据的处理。

// 省略了一些无关的代码
void consumeUpdatesInOnePass() {
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
            case UpdateOp.REMOVE:
                mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                break;
            case UpdateOp.UPDATE:
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            case UpdateOp.MOVE:
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
        }
    }
    recycleUpdateOpsAndClearList(mPendingUpdates);
}
复制代码

consumeUpdatesInOnePass方法内部的实现比较简单,先遍历mPendingUpdates处理所有的局部刷新数据,再调用recycleUpdateOpsAndClearList回收清空mPendingUpdates。
每个局部刷新的操作都在对应的switch里。我们依次看下

notifyItemInserted - UpdateOp.ADD

void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
            holder.offsetPosition(itemCount, false);
            mState.mStructureChanged = true;
        }
    }
    mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
    requestLayout();
}

复制代码

对应UpdateOp.ADD内部调用offsetPositionRecordsForInsert进行处理,内部获取生效区间后面的ViewHolder,因为做增加item操作,其后的item肯定是要后移的,所以调用offsetPosition方法更新mPosition,这个变量表示显示的位置,后移itemCount个位置。这里很好理解,就是插入n个,后面的item肯定要后移n位。
对一级二级缓存的提取就是通过匹配mPosition进行的。所以新增加的item从一二级缓存是拿不到的,需要从四级缓存或者走onCreate。而后面的item,还是可以从一二级缓存拿到的,也不用重新bind,不错的操作。
后面又通过mRecycler.offsetPositionRecordsForInsert方法对缓存进行操作。最后调用requestLayout重新执行测绘流程。

void offsetPositionRecordsForInsert(int insertedAt, int count) {
    final int cachedCount = mCachedViews.size();
    for (int i = 0; i < cachedCount; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder != null && holder.mPosition >= insertedAt) {
            holder.offsetPosition(count, true);
        }
    }
}
复制代码

内部的逻辑也比较简单,把二级缓存中,在生效位置后面的ViewHolder调用offsetPosition做移位操作,和上面对屏幕上可见ViewHolder的操作一致。这个操作比较关键,如果我们操作的是不可见的holder,那么也要保证缓存是有效的,所以也要更新它的position。

notifyItemChanged - UpdateOp.UPDATE

Update的具体操作也是调用viewRangeUpdate方法进行的,这个方法上面已经讲过了。这里不在复述了。

notifyItemRemoved - UpdateOp.REMOVE

REMOVE的具体操作在offsetPositionRecordsForRemove中,我们具体看下

void offsetPositionRecordsForRemove(int positionStart, int itemCount,
        boolean applyToPreLayout) {
    final int positionEnd = positionStart + itemCount;
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder != null && !holder.shouldIgnore()) {
            if (holder.mPosition >= positionEnd) {
                //处理删除后面的holder,做移位
                holder.offsetPosition(-itemCount, applyToPreLayout);
                mState.mStructureChanged = true;
            } else if (holder.mPosition >= positionStart) {
                //处理被remove的holder
                holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
                        applyToPreLayout);
                mState.mStructureChanged = true;
            }
        }
    }
    mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
    requestLayout();
}

void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
    addFlags(ViewHolder.FLAG_REMOVED);
    offsetPosition(offset, applyToPreLayout);
    mPosition = mNewPosition;
}
复制代码

offsetPositionRecordsForRemove中的操作和ADD类似。被删除后面的移位操作是类似的,都是调用offsetPosition更新对应的position。只是需要额外处理呗remove的holder。flagRemovedAndOffsetPosition中设置标记为REMOVED,并更新内部holder的position。
因为被删除后面的position进行了向前的移位,所以从缓存提取是,会直接匹配position进行使用。也不用重新bind。
那设置了FLAG_REMOVED状态的正在显示holder,会怎么处理呢?它也会正常进入一级缓存,如果被提取,因为状态是FLAG_REMOVED,所以不合法,不可使用,继续从后面的缓存提取,不合法的缓存也会收到四级缓存中。如果没有被提取出来,那么在dispatchLayoutStep3中调用removeAndRecycleScrapInt会对其进行回收,同样是回收到四级缓存中。 对缓存的操作调用offsetPositionRecordsForRemove进行处理。同样是调用requestLayout()重新布局。

void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) {
    final int removedEnd = removedFrom + count;
    final int cachedCount = mCachedViews.size();
    for (int i = cachedCount - 1; i >= 0; i--) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder != null) {
            if (holder.mPosition >= removedEnd) {
                holder.offsetPosition(-count, applyToPreLayout);
            } else if (holder.mPosition >= removedFrom) {
                holder.addFlags(ViewHolder.FLAG_REMOVED);
                recycleCachedViewAt(i);
            }
        }
    }
}
复制代码

同样的操作也是对缓存进行处理,使之内部数据刷新。对remove的holder,设置FLAG_REMOVED标记位。并调用recycleCachedViewAt方法直接下放到四级缓存中。

notifyItemMoved - UpdateOp.MOVE

UpdateOp.MOVE的操作主要在offsetPositionRecordsForMove方法中处理。

void offsetPositionRecordsForMove(int from, int to) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    final int start, end, inBetweenOffset;
    if (from < to) {
        start = from;
        end = to;
        inBetweenOffset = -1;
    } else {
        start = to;
        end = from;
        inBetweenOffset = 1;
    }

    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder == null || holder.mPosition < start || holder.mPosition > end) {
            continue;
        }
        if (holder.mPosition == from) {
            holder.offsetPosition(to - from, false);
        } else {
            holder.offsetPosition(inBetweenOffset, false);
        }

        mState.mStructureChanged = true;
    }
    mRecycler.offsetPositionRecordsForMove(from, to);
    requestLayout();
}
复制代码

上面的逻辑处理交换的逻辑,遍历显示的holder,找到要交换的两个holder,调用offsetPosition方法刷新新的合法的postion。后面的代码也是老套路了,先调用Recycle的offsetPositionRecordsForMove对二级缓存进行刷新,同样也是更新到合法的position,这里就不贴代码了。最后调用requestLayout重新进行测绘流程。

到此局部刷新的所有逻辑就讲完了,通过上面的流程图和下面重新布局对局部刷新的处理,应该对整个局部的逻辑有了比较清晰的认识。对比之前的两种刷新方法,显然局部刷新更加的高效,不但充分利用了缓存,也尽可能的减少了bind的调用。所以我们还是要充分的使用RecyclerView的这项不错的功能。

总结

  1. RecycleView的刷新,都是通过对holder的标记,提取时代表不合法,自然就会重新bind渲染,达到了刷新的目的。而其他没有影响的holder自然不需要做什么的。好与不好的刷新分方式就在于是否伤及无辜。

  2. 局部缓存相比 notifyDataSetChanged和更改Adapter充分利用了缓存

  3. 我们使用局部刷新时,可以搭配DiffUtil,效果更佳。DiffUtil使用了差分算法,算出两个集合替换的最短路径,进而产生的增删改操作。正好对应我们的局部刷新逻辑

猜你喜欢

转载自juejin.im/post/7035470573685702693