三个场景带你了解RecyclerView

说明: 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》
《三个场景带你了解RecyclerView》
《三个场景带你了解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》

从流程图可以看出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的流程而已。

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

    原文作者:ETF大战Android
    原文地址: https://www.jianshu.com/p/bd555983d6b8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞