RecyclerView的使用场景非常丰富,而本篇的源码分析基于上下滑动一个列表的场景来观察它的复用-回收机制。本文基于27.0.0版本进行分析,如下是Demo展示:
RecyclerView继承自ViewGroup,属于系统级别的自定义控件,而它的源码长达12000多行,还不包括抽取出去的其他辅助类、管理类等,可想而知其复杂性,本文的分析思路主要是集中在RecyclerView的缓存机制上,通过滑动事件结合源码分析它的复用-回收机制,而RecyclerView的绘制流程、ItemDecoration、LayoutManager、State、Recycler等会一笔带过。
自定义控件三部曲:onMeasure - onLayout - onDraw,RecyclerView也不例外。查看源码可以看到,RecyclerView测量的一部分逻辑委托给了LayoutManager,源码如下所示,进来判断是否存在LayoutManager实例,不存在则调用defaultOnMeasure进行默认测量。然后就是一个if...else...判断是否为AutoMeasure,LinearLayoutManager和GridLayoutManager使用这种模式,而StaggerLayoutManager在一定条件下会使用自定义测量这种模式。
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
//LinearLayoutManager和GridLayoutManager使用这种模式
if (mLayout.mAutoMeasure) {
...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
...
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
...
//而StaggerLayoutManager在一定条件下会使用自定义测量这种模式
}
}
测量之后会执行onLayout,这里我们分析采用垂直布局的LinearLayoutManager,在布局的逻辑中会经过如下三个方法:dispatchLayoutStep1 - dispatchLayoutStep2 - dispatchLayoutStep3,它们各司其职。
dispatchLayoutStep1:处理Adapter的更新和动画相关
dispatchLayoutStep2:真正执行LayoutManager.onLayoutChildren,该函数的实现决定了ChildView将会怎样被布局(layout)
dispatchLayoutStep3:保存动画相关的信息并做必要的清理工作
所以我们重点放到LayoutManager.onLayoutChildren上,直接进入LinearLayoutManager的onLayoutChildren,发现代码很长,里面也有注释信息,布局的逻辑如下:1 首先寻找锚点,2 从锚点开始,底部向上填充,顶部向下填充,3 如果再有剩余空间,再填充一次。下面的LinearLayoutManager配合垂直布局的onLayout代码段:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
ensureLayoutState();
mLayoutState.mRecycle = false;
// 确定布局方向
resolveShouldLayoutReverse();
// 寻找锚点
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 计算锚点的位置和坐标
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
...
detachAndScrapAttachedViews(recycler);//回收view
// 下面是LinearLayoutManager配合垂直布局的代码
// 先向下绘制
updateLayoutStateToFillEnd(mAnchorInfo);
// 填充view
fill(recycler, mLayoutState, state, false);
...
// 再向上绘制
updateLayoutStateToFillStart(mAnchorInfo);
// 填充view
fill(recycler, mLayoutState, state, false);
//还有可用空间
if (mLayoutState.mAvailable > 0) {
...
// 再次填充view
fill(recycler, mLayoutState, state, false);
}
...
}
至此我们大致了解了布局的算法逻辑:先找锚点再多次不同方向上进行填充,而RecyclerView的复用流程和回收流程都在该方法里面发起,所以onLayout是我们分析缓存机制的入口。其中复用流程是fill,回收流程是detachAndScrapAttachedViews。到这里我们先总结下onMeasure和onLayout的内容:
1.RecyclerView是将绘制流程交给LayoutManager处理,如果没有设置不会测量子View
2.绘制流程是区分正向和倒置的
3.绘制是先确定锚点,然后再多次不同方向上进行填充,fill()至少会执行两次,如果绘制完还有剩余空间,则会再执行一次fill()方法
4.LayoutManager获得View(也可理解为复用入口)是从RecyclerView中的onLayout开始的(fill),涉及到RecyclerView的缓存策略,如果没有拿到缓存,则走我们自己重写的onCreateView方法,再调用onBindViewHolder
5.LayoutManager回收View的入口也是RecyclerView中的onLayout开始的(detachAndScrapAttachedViews),涉及到RecyclerView的缓存策略
下面就会详细分析复用流程和回收流程,这里先确定流程的入口是onLayout方法。onDraw的代码这里不再进行分析。这里根据源码的执行顺序会先进行回收再复用,所以下面先分析回收流程。
1. 回收流程
回收流程的入口方法是 LinearLayoutManager - onLayoutChildren - detachAndScrapAttachedViews -scrapOrRecycleView,最后一个方法名翻译一下是:废弃或者回收view,在该方法中会根据一定的策略来决定是scrap还是recycle,下面是scrapOrRecycleView的源码:
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
//从指定的view中获取到对应的viewHolder
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
//viewHolder已经无效,并且还没有被remove,并且没有指定的stableId
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
//remove该项
removeViewAt(index);
//通过recycler执行内部回收流程,主要是将viewHolder放到RecycledViewPool中
recycler.recycleViewHolderInternal(viewHolder);
} else {
//detach该项
detachViewAt(index);
//通过recycler将view从scrap数组中移除
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
上面的代码中牵出了两个比较重要的概念:remove和detach。
detach: 在ViewGroup中的实现很简单,只是将ChildView从ParentView的ChildView数组中移除,ChildView的mParent设置为null,可以理解为轻量级的临时remove,因为View此时和View树还是藕断丝连,这个函数被经常用来改变ChildView在ChildView数组中的次序。View被detach一般是临时的,在后面会被重新attach。
remove: 真正的移除,不光被从ChildView数组中除名,其他和View树各项联系也会被彻底斩断(不考虑Animation/LayoutTransition这种特殊情况),比如焦点被清除,从TouchTarget中被移除等。
所以我们可以将scrapOrRecycleView对应起来:scrap-detach,recycler-remove。同时满足下面的3个条件会被recycler,其余情况下viewHolder都会被scrap:
1、viewHolder本身已经完全无效
2、viewHolder对应的项还没有被remove(这个判断是考虑到预加载的原因,先不具体说)
3、adapter没有指定stableId,因为如果指定,就不存在View绑定内容无效的可能了
Demo案例实操过程中,上下滑动时基本上都是触发recycler;当插入一个元素或者删除一个元素,或者本质上说调用notifyDataSetChanged后,就会触发scrap。
下面再看看recycler执行内部回收流程,大致逻辑是先判断viewHolder的一些标志位,达到回收条件后,先将viewHolder缓存到mCachedViews中,如果mCachedViews已满,则删除mCachedViews中最老的一个元素,并将该元素放到RecycledViewPool中;再接着将本次要回收的元素放到mCachedViews中。 如果未达到条件,则直接将viewHolder放到RecycledViewPool中。下面这段代码是整理之后的源码,描述了上述逻辑:
void recycleViewHolderInternal(ViewHolder holder) {
//进行必要的校验,否则抛出异常
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw new IllegalArgumentException(
"Scrapped or attached views may not be recycled. isScrap:"
+ holder.isScrap() + " isAttached:"
+ (holder.itemView.getParent() != null) + exceptionLabel());
}
//进行必要的校验,否则抛出异常
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed "
+ "from RecyclerView before it can be recycled: " + holder
+ exceptionLabel());
}
//进行必要的校验,否则抛出异常
if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ " should first call stopIgnoringView(view) before calling recycle."
+ exceptionLabel());
}
...
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();
//判断cachedViewSize是否大于最大缓存数量
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//回收最老的元素,即第0号元素
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
//计算出缓存元素的index值
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//未到达条件,又没被缓存,则直接放到RecycledViewPool
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
...
}
根据上面的源码,我们debug的方法调用路径是:recycleViewHolderInternal - recycleCachedViewAt - addViewHolderToRecycledViewPool。下面就是addViewHolderToRecycledViewPool中最关键的源码,将元素放到RecycledViewPool中,可以看到这里区分了type,每个type对应一个ArrayList,同时进入到这里的viewHolder会被重置,主要是重置position以及flags。
public void putRecycledView(ViewHolder scrap) {
//拿到type
final int viewType = scrap.getItemViewType();
//拿到type对应的ViewHolder集合
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
//重置viewHolder
scrap.resetInternal();
//将viewHolder添加到集合中
scrapHeap.add(scrap);
}
看完recycler的流程之后,还有一种回收场景scrap,scrap的场景中会涉及到比较多的全局变量,如mChildHelper,mAttachedScrap,mChangedScrap。首先mAttachedScrap和mChangedScrap都是ArrayList类型的缓存viewHolder变量的。mChildHelper是ChildHelper的实例对象,RecyclerView尽管本身是一个ViewGroup,但是将ChildView管理职责全权委托给了ChildHelper,所有关于ChildView的操作都要通过ChildHelper来间接进行,ChildHelper成为了一个ChildView操作的中间层,getChildCount/getChildAt等函数经由ChildHelper的拦截处理再下发给RecyclerView的对应函数,其参数或者返回结果会根据实际的ChildView信息进行改写。了解了基本的概念之后,看看scrapOrRecycleView中的detach分支,下面是detach分支的关键源码:
//detach下标为index的view
detachViewAt(index);
//在recycler中维护下这个scrapView
recycler.scrapView(view);
detachViewAt中是通过mChildHelper处理view和parentView的关系;而在scrapView中,则通过判断viewHolder是否被removed,是否invalid,是否canReuseUpdatedViewHolder条件来决定是放到mAttachedScrap中还是mChangedScrap中。源码如下所示:
void scrapView(View view) {
//拿到viewHolder
final ViewHolder holder = getChildViewHolderInt(view);
//是否被removed,或者invalid,或者canReuseUpdatedViewHolder
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
...
holder.setScrapContainer(this, false);
//放到mAttachedScrap中
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
//否则放到mChangedScrap中
mChangedScrap.add(holder);
}
}
这里还需要对比下remove和scrap在复用性上的不同,只被detach的View要比被remove的View高,detach的View一般来说代表可以直接复用(其ViewHolder对应于Data的Position还是有效的,只需要重新绑定数据,如果数据也没变化的话,甚至都不用重新绑定数据;View还是有效的,View绑定的数据可能有效的, 比如一个列表有N项,现在删除了其中一项,那么在没有其他变化的前提下,剩余的N-1个项对应的ViewHolder是可以直接复用的),这一点非常关键,避免了不必要的绑定(和ListView等相比),项处理的粒度从整体细化到了单个项,即包含了对View的复用,也包含了对View当前绑定内容的复用。被remove的View复用性上则要差一些,其对应的Position已经无效,这种复用层级和Scrap相比只有View层级的复用(稍带可以复用ViewHolder,只不过里面的信息要重新设置,但起码不用new一个)。
至此回收机制的流程基本完成,回顾一下,首先在RecyclerView的onLayout方法中会在dispatchLayoutStep2中将布局的权利移交给LayoutManger,Demo中对应就是LinearLayoutManager。LinearLayoutManager接管之后,调用自身的onLayoutChildren,然后就会对view进行回收(detachAndScrapAttachedViews)和填充(fill)。detachAndScrapAttachedViews中会根据一定的条件决定该view是被scrap(对应detach)还是被recycler(对应remove)。被recycler的view会先经过mCachedViews再根据条件进入到RecyclerViewPool中,而被scrap的元素会根据具体条件看是放到mAttachedScrap还是mChangedScrap中缓存起来。
2. 复用流程
上面根据LinearLayoutManager的onLayoutChildren中代码的执行顺序,先分析了回收机制的流程,接下来继续分析复用机制的流程,还是遵循上文的思路,先确定入口方法,再确定一条方法调用流程,然后再细细分析。上文提到过LinearLayoutManager配合垂直布局的onLayout代码段,找到锚点,先向下绘制-再填充-再向上绘制-再填充的流程,这里的fill方法便是我们分析复用机制的入口方法了。
// 下面是LinearLayoutManager配合垂直布局的代码
// 先向下绘制
updateLayoutStateToFillEnd(mAnchorInfo);
// 填充view
fill(recycler, mLayoutState, state, false);
...
// 再向上绘制
updateLayoutStateToFillStart(mAnchorInfo);
// 填充view
fill(recycler, mLayoutState, state, false);
//还有可用空间
if (mLayoutState.mAvailable > 0) {
...
// 再次填充view
fill(recycler, mLayoutState, state, false);
}
进入fill后,会根据layoutState是否还有更多项要填充,来循环调用layoutChunk方法,根据layoutChunk这个方法名猜测其作用就是布局块用的,一块一块对应就是一项一项的item。在layoutChunk中,先通过next方法找到view,然后对该view进行再测量和布局,以及边框的确定。这篇文章的重点是关注缓存机制,所以绘制布局这块一笔带过,我们将重点放到next方法上。下面是next方法的源码:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
//通过recycler对象找到一个合适的view
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
上述代码最关键的一句是recycler.getViewForPosition(mCurrentPosition),通过给定的position获取一个view的实例对象,最终会通过tryGetViewHolderForPositionByDeadline方法得到一个viewHolder,再通过viewHolder里面的itemView属性将view实例对象返回。如下源码所示:
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
//先获取viewHolder,再通过itemView属性得到view的实例
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
Recycler一般不会直接作用于View,其操作的对象一般是ViewHolder。如果你有自己debug代码,留意了view和viewHolder之间的关系,你会发现它们之间是双向绑定的,view中持有viewHolder是通过LayoutParams的mViewHolder属性;而viewHolder中持有view是通过itemView属性。在tryGetViewHolderForPositionByDeadline中总的思路是,依次经过RecyclerView中的四级缓存,一级一级找,找到了就返回viewHolder,没有的话,就回调用户的onCreateViewHolder和onBindViewHolder。RecyclerView中的四级缓存更细致的说应该是Recycler中的四级缓存,分别是:mAttachedScrap - mCachedViews - mViewCacheExtension - mRecyclerPool。
mAttachedScrap:对应上述回收机制中的Scrap View,保存在mAttachedScrap或者mChangedScrap中,用于屏幕内的itemView快速复用。
mCachedViews:对应上述回收机制中的remove view,默认上线为2个。
mViewCacheExtension:供使用者自行扩展,让使用者可以控制缓存。
mRecyclerPool:对应于上述回收机制中remove view放到mCachedViews后溢出的view,同时可以用与RecyclerView之间共享ViewHolder的缓存池。
了解了上面四级缓存后,接着看tryGetViewHolderForPositionByDeadline的代码会轻松很多,如下源码:
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
ViewHolder holder = null;
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
//从scrap或hidden或cache中找viewHolder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
//检查找到的holder是不是能够被当前的位置使用,不行的话就要对该viewHolder进行回收
if (!validateViewHolderForOffsetPosition(holder)) {
// dryRun一般为false,表示可以从scrap或者cache中移除
if (!dryRun) {
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;
}
}
}
if (holder == null) {
...
//获取type
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
//有设置stableId,则尝试从scrap或者cache中获取
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//判断是否设置了外部扩展
if (holder == null && mViewCacheExtension != null) {
// 从外部扩展中找
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
...
}
}
if (holder == null) { // fallback to pool
...
//根据tyep从RecycledViewPool中找
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
//回调用户的onCreateViewHolder方法
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//回调用户的onBindViewHolder方法
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
//获取LayoutParams
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
//转成RecyclerView所需类型的LayoutParams
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
//将viewHolder保存到mViewHolder属性中
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
上述源码我对其进行了删减,保留了核心流程代码,从上倒下就是从四级缓存中逐个查找,实在没有则创建一个,最后将viewHolder绑定到view上,完成最后的双向绑定。
至此复用机制的流程基本完成,总结一下,方法的调用流程是:LinearLayoutManager - onLayoutChildren - fill() - layoutChunk() - layoutState.next() - getViewForPosition() - tryGetViewHolderForPositionByDeadline() - 四级缓存 or onCreateViewHolder - onBindViewHolder(未绑定的情况下会触发绑定回调)。这套流程下来要关注两个地方,一个是fill方法,它会被调用多次;一个是tryGetViewHolderForPositionByDeadline方面,里面涉及到RecyclerView复用机制的核心逻辑:四级缓存。
3. RecyclerView的优势
3.1. RecyclerView与ListView对比
RecyclerView强制使用ViewHolder,当然在使用ListView的时候都是自定义ViewHolder配合使用,避免每次createView时调用findViewById。但是RecyclerView在ViewHolder基础上定义了很多flag标识表明当前ViewHolder的可用性状态,这点比ListView中自定义ViewHolder要更加丰富。
在处理离屏缓存这一场景时,RecyclerView与ListView的处理也有很大的不同。RecyclerView会从mCachedViews中获取到一个viewHolder,然后会判断这个viewHolder是否已被绑定,是否不需要更新,是否有效,如果满足其中任何一个条件就不会触发onBindViewHolder。源码如下所示:
//处理预加载的情况
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {//如果已被绑定,或者不需要更新或者是有效的,就不会触发tryBindViewHolderByDeadline方法了
...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
而ListView的处理则是mRecycler得到一个缓存的view,然后重新getView,此处势必会调用onBind触发重新绑定的逻辑。AbsListView源码如下所示:
View obtainView(int position, boolean[] outMetadata) {
...
//拿到缓存的view
final View scrapView = mRecycler.getScrapView(position);
//每次都调用getView,也就意味着每次都调用onBind
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
...
}
下面的gif动图展示了RecyclerView中处理离屏缓存时,onBind方法的执行情况,当用户轻微的来回滑入滑出item时,此时是从mCachedViews中拿到缓存的viewHolder直接复用,不会触发onBind操作。
3.2. 局部刷新功能
处理局部刷新时,ListView是一锅端,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。通过局部刷新能避免调用许多无用的bindView,下面的gif动图展示了局部刷新position位置为4的场景,我们可以观察第二个透明框中的onBind的情况。
参考:
RecyclerView机制分析: Recycler
Android ListView与RecyclerView对比浅析--缓存机制