手把手debug源码之RecyclerView

RecyclerView的使用场景非常丰富,而本篇的源码分析基于上下滑动一个列表的场景来观察它的复用-回收机制。本文基于27.0.0版本进行分析,如下是Demo展示:

Demo示例.gif

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操作。

离屏缓存示例.gif

3.2. 局部刷新功能

处理局部刷新时,ListView是一锅端,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。通过局部刷新能避免调用许多无用的bindView,下面的gif动图展示了局部刷新position位置为4的场景,我们可以观察第二个透明框中的onBind的情况。

局部刷新示例.gif

参考:
RecyclerView机制分析: Recycler
Android ListView与RecyclerView对比浅析--缓存机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343