美文网首页
手把手debug源码之RecyclerView

手把手debug源码之RecyclerView

作者: leeeyou | 来源:发表于2020-03-20 16:07 被阅读0次

    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对比浅析--缓存机制

    相关文章

      网友评论

          本文标题:手把手debug源码之RecyclerView

          本文链接:https://www.haomeiwen.com/subject/pubjyhtx.html