三个场景带你了解RecyclerView

作者: 事多店 | 来源:发表于2017-06-16 20:11 被阅读1014次

    说明: RecyclerView的版本是23.2.1,RecyclerView的布局为match_parent,使用到的LayoutManager为LinearLayoutManager。如果你真想搞懂这几个场景的代码,建议自己写个demo,然后断点调试,再结合这篇文章,效果会更好一点。

    场景一 RecyclerView绘制流程

    RecyclerView虽然很复杂,可它实质上也是一个View,遵循View的绘制流程。所以想要了解RecyclerView,可以从它的绘制流程下手。首先从onMeasure开始。(PS:这里只分析match_parent的情况。)

    
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
    
        // 从23.2.0之后版本的RecyclerView都已经支持自动测量了。
        // 可通过LayoutManager.setAutoMeasureEnabled(true)设置自动测量。
        // 在LinearLayoutManager中默认开启自动测量。
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            
            // 如果RecyclerView的宽和高都指定为match_parent或者具体的值,skipMeasure为true。
            // 在我们分析的这种场景中,skipMeasure为true。
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
    
            // 调用LayoutManager的测量。
            // 默认实现为RecyclerView.defaultOnMeasure
            // LinearLayoutManager使用的是默认的测量方式
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    
            // 完成测量,直接返回。
            if (skipMeasure || mAdapter == null) {
                return;
            }
    
            // 如果RecyclerView的宽和高任意一个指定为wrap_content,还需要进行布局以确定大小。
            // 省略了很多代码
            ......
        } else {
            // 非自动测量的情况
            ......
        }
    }
    
    

    RecyclerView的默认测量方式:

    
    void defaultOnMeasure(int widthSpec, int heightSpec) {
    
        // calling LayoutManager here is not pretty but that API is already public and it is better
        // than creating another method since this is internal.
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
                
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));
    
        setMeasuredDimension(width, height);
    }
    
    

    onMeasure的代码中可以看出,RecyclerView的宽和高最好能指定为match_parent或者具体值,这样可以省下不少测量的工作。

    测量完RecyclerView之后,就到布局了。onLayout内部会调用dispatchLayout方法。dispatchLayoutStep分为三个步骤:

    1. dispatchLayoutStep1: Adapter的更新; 决定该启动哪种动画; 保存当前View的信息; 如果有必要,先跑一次布局并将信息保存下来。
    2. dispatchLayoutStep2: 真正对子View做布局的地方。
    3. dispatchLayoutStep3: 为动画保存View的相关信息; 触发动画; 相应的清理工作。
    void dispatchLayout() {
    
        ...... // 省略代码
        
        mState.mIsMeasuring = false;
    
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
                mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }
    

    dispatchLayoutStep1中会触发processAdapterUpdatesAndSetAnimationFlags方法,如它的名字一样,它的作用是通过AdapterHelper来更新Adapter; 记录动画的标志,决定该启动哪种类型的动画。

    dispatchLayoutStep2是真正的布局,是因为它调用了mLayout.onLayoutChildren,而这也是不同的LayoutManager布局不同的根本原因。我们以LinearLayoutManager为例,它里面有两个比较重要的方法:detachAndScrapAttachedViewsfilldetachAndScrapAttachedViews会回收当前所有屏幕上的子View到Scarp中,虽然该方法很重要,但对于RecyclerViewr的第一次初始化,没有什么好回收的,所以直接忽略。来看看fill方法。

    // fill方法的任务是向RecyclerView中填充子View,终止条件为:
    // 1). 没有空余的空间了
    // 2). stopOnFocusable为true并且遇到了第一个focusable的子View
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    
        final int start = layoutState.mAvailable;
    
        // 表示有滑动,先进行一次回收。
        // 初始化时不会走这里面。
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
    
            // 回收子View。
            recycleByLayoutState(recycler, layoutState);
        }
    
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    
        // 保存着对子View layout之后的结果
        LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
    
        // 不断添加子View,直到结束
        // 一般情况下,layoutState.mInfinite为false
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
    
            layoutChunkResult.resetInternal();
            
            // 重点在该方法里面
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
    
            // 填充结束
            if (layoutChunkResult.mFinished) {
                break;
            }
    
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
    
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
    
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                // 计算剩余的空间
                remainingSpace -= layoutChunkResult.mConsumed;
            }
    
            // 表示有滑动,又进行了一次回收。
            // 初始化时不会走这里面,可以先跳过。
            if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
    
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
    
                // 回收子View。
                recycleByLayoutState(recycler, layoutState);
            }
    
            // 如果新添加的子View是focusable的且设置了stopOnFocusable为true,停止填充子view
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        
        ...... // 省略代码
    
        return start - layoutState.mAvailable;
    }
    
    

    fill方法的重点是layoutChunk方法,该方法大致流程如下:创建子View -> 将子View添加到RecyclerView中 -> 测量子View -> 对子View进行布局操作。

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
    
        // 创建子View的地方
        View view = layoutState.next(recycler);
    
        ...... // 省略代码
    
        LayoutParams params = (LayoutParams) view.getLayoutParams();
    
        // 将子view添加到RecyclerView中
        if (layoutState.mScrapList == null) {
    
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
    
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
    
        // 测量子View
        measureChildWithMargins(view, 0, 0);
    
        // 计算子view占用了多少空间,以便fill方法中计算剩余的空间
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    
        ...... // 省略代码
    
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        // 对子View进行layout过程
        layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
                right - params.rightMargin, bottom - params.bottomMargin);
                
        ...... // 省略代码
    
        result.mFocusable = view.isFocusable();
    
    }
    
    

    创建View:

    子View通过layoutState.next(recycler)被创建出来,next内部最后又会调用到了Recycler.getViewForPosition,Recycler是整个RecyclerView实现回收复用的关键。它会尝试从多个地方获取已缓存起来的ViewHolder,如果最终获取失败,才会去创建ViewHolder,对于第一次初始化来说,最终都会去创建ViewHolder,即回调我们所熟悉的onCreateViewHolder方法。以下的代码只截取了关键部分。

    View getViewForPosition(int position, boolean dryRun) {
    
        boolean fromScrap = false;
        ViewHolder holder = null;
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
    
        if (holder == null) {
            // 创建ViewHolder,方法内部会回调我们熟悉的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);
            // 先设置OwnerRecyclerView,然后绑定ViewHolder
            holder.mOwnerRecyclerView = RecyclerView.this;
            // 内部会回调我们熟悉的onBindViewHolder,并且会设置flag,holder.isBound()将会返回true。
            mAdapter.bindViewHolder(holder, offsetPosition);
        }
    
        // 设置LayoutParams
        final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        final LayoutParams rvLayoutParams;
    
        if (lp == null) {
            rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
            holder.itemView.setLayoutParams(rvLayoutParams);
        } else if (!checkLayoutParams(lp)) {
            rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
            holder.itemView.setLayoutParams(rvLayoutParams);
        } else {
            rvLayoutParams = (LayoutParams) lp;
        }
    
        rvLayoutParams.mViewHolder = holder;
        rvLayoutParams.mPendingInvalidate = fromScrap && bound;
    
        return holder.itemView;
    }
    

    添加View:

    添加,删除等这些与ViewGroup相关的操作都是放在ChildHelper中去进行的,原因是这里除了通知RecyclerView做相应的操作之外,还可能做了其它的操作,比如回调,设置标志位等。

    @Override
    public void addView(View child, int index) {
    
        // 添加子view
        RecyclerView.this.addView(child, index);
        
        // 分发事件,回调Adapter.onViewAttachedToWindow方法。
        dispatchChildAttached(child);
    
    }
    
    

    总结一下,从fill开始,一个View从创建到添加到布局经历了这些流程:

    LinearLayoutManager.layoutChunk() -> Recycler.getViewForPosition() -> Adapter.onCreateViewHolder() -> 设置ownerRecyclerView -> Adapter.onBindViewHolder() -> 设置LayoutParams -> RecyclerView.addView() -> Adapter.onViewAttachedToWindow() -> LayoutManager.measureChildWithMargins() -> LayoutManager.layoutDecorated()

    场景二 RecyclerView的滚动与Recycler的回收

    先看三张截图,第一张是RecyclerView刚初始化完,屏幕上总共有6个子View;第二张是开始滑动了,可以看出,6, 7, 8 都是通过重新创建ViewHolder生成的,在这期间,0, 1, 2 都已经从RecyclerView上移除了。然后从第9开始,RecyclerView开始复用了。但是只有第0和2被回收掉了,这中间漏了1。如果再继续滑动的时候,变成1和4被回收了。看起来好像没什么规律!所以这一小节,通过RecyclerView的滑动来分析它的回收机制。

    当用户触发滑动的时候,RecyclerView会先进行一次View的回收,然后往RecyclerView中填充子View,然后又再进行了一次回收。子View的回收有可能是在填充新的子View之前,也可能是之后,所以进行了两次回收工作。

    以LinearLayoutManager为例, 当滚动的时候,方法调用如下:onTouchEvent -> scrollByInternal -> LinearLayoutManager.scrollVerticallyBy -> LinearLayoutManager.scrollBy -> LinearLayoutManager.fill.

    再来看一次fill的代码:

    // fill方法的任务是向RecyclerView中填充子View,终止条件为:
    // 1). 没有空余的空间了
    // 2). stopOnFocusable为true并且遇到了第一个focusable的子View
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    
        final int start = layoutState.mAvailable;
    
        // 表示有滑动,先进行一次回收。
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
    
            // 回收子View。
            recycleByLayoutState(recycler, layoutState);
        }
    
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    
        // 保存着对子View layout之后的结果
        LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
    
        // 不断添加子View,直到结束
        // 一般情况下,layoutState.mInfinite为false
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
    
            // 重点在该方法里面
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
    
            // 填充结束
            if (layoutChunkResult.mFinished) {
                break;
            }
    
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
    
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
    
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                // 计算剩余的空间
                remainingSpace -= layoutChunkResult.mConsumed;
            }
    
            // 表示有滑动,又进行了一次回收。
            if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
    
                // 回收子View。
                recycleByLayoutState(recycler, layoutState);
            }
    
            // 如果新添加的子View是focusable的且设置了stopOnFocusable为true,停止填充子view
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
    
        ......
    
        return start - layoutState.mAvailable;
    }
    
    

    fill方法可以看出,回收子View的代码在recycleByLayoutState方法中,但它其实也只是一个帮助的方法,用来判断从前面还是后面回收。调用顺序如下: (recycleViewsFromStart / recycleViewsFromEnd) -> recycleChildren -> removeAndRecycleViewAt

    public void removeAndRecycleViewAt(int index, Recycler recycler) {
        final View view = getChildAt(index);
    
        // 调用ChildHelper.removeViewAt((),该方法做了两件事:
        // 1) 通知Adapter和OnChildAttachStateChangeListener,有onViewDetachedFromWindow事件;
        // 2) RecyclerView将相应的子View移除掉。
        removeViewAt(index);
        
        recycler.recycleView(view);
    }
    
    

    Recycler.recycleView方法:

    /**
     * 回收一个已分离的子View,它会被加到缓冲池中以便后续的重绑和复用。
     * 
     * 在回收之前,子View必须完全从RecyclerView中分离出来,如果该View是已废弃的(scrapped), 它会从scrap list中移除。
     *
     * @param view Removed view for recycling
     * @see LayoutManager#removeAndRecycleView(View, Recycler)
     */
    public void recycleView(View view) {
    
        // This public recycle method tries to make view recycle-able since layout manager
        // intended to recycle this view (e.g. even if it is in scrap or change cache)
        ViewHolder holder = getChildViewHolderInt(view);
    
        if (holder.isTmpDetached()) {
            removeDetachedView(view, false);
        }
    
        // 如果该View是已废弃的(scrapped), 它会从Scrap列表中被移除。
        if (holder.isScrap()) {
            holder.unScrap();
        } else if (holder.wasReturnedFromScrap()){
            holder.clearReturnedFromScrapFlag();
        }
    
        recycleViewHolderInternal(holder);
    }
    

    Recycler.recycleViewHolderInternal方法:

    void recycleViewHolderInternal(ViewHolder holder) {
    
        //noinspection unchecked
        final boolean transientStatePreventsRecycling = holder
                .doesTransientStatePreventRecycling();
    
        final boolean forceRecycle = mAdapter != null
                && transientStatePreventsRecycling
                && mAdapter.onFailedToRecycleView(holder);
    
        boolean cached = false;
        boolean recycled = false;
    
        if (forceRecycle || holder.isRecyclable()) {
            if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE)) {
    
                // Retire oldest cached view
                final int cachedViewSize = mCachedViews.size();
    
                // 如果mCachedView满了,从CachedView中取出第一个item,并放进RecycledViewPool
                if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                }
    
                // 先往mCachedView队列加
                if (cachedViewSize < mViewCacheMax) {
                    mCachedViews.add(holder);
                    cached = true;
                }
            }
    
            // 如果还没缓存,存进RecycledViewPool
            if (!cached) {
                addViewHolderToRecycledViewPool(holder);
                recycled = true;
            }
        } else if (DEBUG) {
            Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                    + "re-visit here. We are still removing it from animation lists");
        }
    
        // even if the holder is not removed, we still call this method so that it is removed
        // from view holder lists.
        mViewInfoStore.removeViewHolder(holder);
    
        if (!cached && !recycled && transientStatePreventsRecycling) {
            holder.mOwnerRecyclerView = null;
        }
    }
    

    方法解释:

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    // 定义mCachedViews最大容量,默认为2.
    private int mViewCacheMax = DEFAULT_CACHE_SIZE;
    private static final int DEFAULT_CACHE_SIZE = 2;
    

    recycleViewHolderInternal方法做了两件事:

    1. 如果mCachedView没满,直接将ViewHolder存进mCachedViews中

    2. 如果mCachedViews满了,将第0个ViewHolder取出来放进RecycledViewPool;同时,新的ViewHolder也不会再放到mCachedViews中了。所以此时,mCachedViews的容量为mViewCacheMax - 1,从这也可以看出mCachedViews其实就是在模拟队列(先进先出)。被添加到RecycledViewPool的ViewHolder会回调到Adapter.onViewRecycled方法。

    在上面的截图中,0和1先被放进mCachedView中,当2进来的时候,由于mCachedView已经满了,所以移除并回收0,同时回收2。所以是0和2被回收了,1还在mCachedView队列中。

    回收完之后有了空间,继续往RecyclerView中填充子View,与第一次布局(初始化)时候不一样的是,ViewHolder是从缓存中取出来的,而不是重新去创建出来的。看一下它的流程图:

    从流程图可以看出RecyclerView至少有四级缓存,再用一张表格来总结一下:

    缓存类型 创建ViewHolder 绑定ViewHolder 备注
    mAttachedScrap 快速重建RecyclerView
    mCachedViews 默认容量为2个
    mViewCacheExtension 需要开发者自己实现
    mRecyclerPool 多个RecyclerView可以共用一个

    从滚动的过程来看,并没有涉及到mAttachedScrap,只利用到了mCachedView和RecycledViewPool,即getViewForPosition方法中会先从mCachedView去检查,没有的话再从RecycledViewPool去拿,并重新调用onBindViewHolder。到这里,应该也可以解释为什么打印出的Log会那么奇怪的问题了。

    场景三 插入数据与RecyclerView的快速重绘

    当我们调用notifyItemInserted的时候,一般情况下,最终会触发requestLayout

    1. dispatchLayoutStep1:RecyclerView将屏幕上的所有ViewHolder的位置做一个偏移处理(如果有需要的话),然后回收所有的ViewHolder至Scrap中。

    2. dispatchLayoutStep2:从Scrap中拿出没有改变的ViewHolder,新加入的item从RecycledViewPool中获取或者走createViewHolder流程。

    3. dispatchLayoutStep3:执行动画,在动画结束后回收不可见的View。

    没带前缀的是RecyclerView的方法
    Adapter.notifyItemInserted -> AdapterDataObservable.notifyItemRangeInserted -> RecyclerViewDataObserver.onItemRangeInserted -> AdapterHelper.onItemRangeInserted -> RecyclerViewDataObserver.triggerUpdateProcessor -> requestLayout -> onMeasure -> onLayout -> dispatchLayout

    第一步:
    dispatchLayoutStep1 -> processAdapterUpdatesAndSetAnimationFlags -> AdapterHelper.preProcess -> ... -> offsetPositionRecordsForInsert -> Recycler.offsetPositionRecordsForInsert

    偏移ViewHolder的位置,假设在第二个位置之前插入一个item,那么第0和第1个ViewHolder的位置都无需改变,第2个位置开始,位置都偏移itemCount个位置。以第2个item为例:

    mOldPosition = 2;
    mPosition += itemCount;
    

    偏移ViewHolder的位置之后,Recycler也需要更新mCachedViews中ViewHolder的位置。

    偏移ViewHolder的位置之后,会调用requestLayout,走:dispatchLayoutStep1 -> LinearLayoutmanager.onLayoutChildren -> detachAndScrapAttachedViews。在detachAndScrapAttachedViews中RecyclerView将所有的View分离出来,并放进mAttachedScrap中。然后走fill流程。

    第二步:
    dispatchLayoutStep2,这里才是真正实现布局的地方。dispatchLayoutStep2也会调用LinearLayoutmanager.onLayoutChildren。这一次,ViewHolder从Scrap中取出,新加入的item从RecycledViewPool中获取或者走createViewHolder流程。

    第三步:
    dispatchLayoutStep3,这个方法会触发相应的动画:item插入,其它item偏移,即将不可见的item做完动画后会被回收:ItemAnimatorRestoreListener.onAnimationFinished -> removeAnimatingView -> Recycler.unscrapView -> Recycler.recycleViewHolderInternal

    通过对ViewHolder做位置的偏移处理,并将所有的ViewHolder放到Scrap中,可以使得数据改变的时候,RecyclerView可以快速响应,并且这个过程中,如果插入一个item,那么最糟糕的情况是只需要再走一次创建ViewHolder的流程而已。

    当有数据删除的时候情况与数据插入类似。

    相关文章

      网友评论

      本文标题:三个场景带你了解RecyclerView

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