RecycylerView回收和复用机制解析

首先,我们先创建一个RecyclerView的小例子:

效果如图:

这里写图片描述

我们可以看到,一个屏幕一次性只能显示10个item,其实我一共创建了30个item,那么这30个item是一次性创建的吗?我们看一下log:

D/BaseAdapter: onCreateViewHolder: 
D/BaseAdapter: onBindViewHolder: 
    ViewHolder{401cc06 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}----0    
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{391e2c7 position=1 id=-1, oldPos=-1, pLpos:-1 no parent}----1
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{52060f4 position=2 id=-1, oldPos=-1, pLpos:-1 no parent}----2
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{d0bd01d position=3 id=-1, oldPos=-1, pLpos:-1 no parent}----3
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{adea292 position=4 id=-1, oldPos=-1, pLpos:-1 no parent}----4
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{53fe463 position=5 id=-1, oldPos=-1, pLpos:-1 no parent}----5
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{b5c9860 position=6 id=-1, oldPos=-1, pLpos:-1 no parent}----6
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{71a5119 position=7 id=-1, oldPos=-1, pLpos:-1 no parent}----7
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{d4055de position=8 id=-1, oldPos=-1, pLpos:-1 no parent}----8
D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
    ViewHolder{64263bf position=9 id=-1, oldPos=-1, pLpos:-1 no parent}----9

我们可以看到,只创建了10个item,而且是创建一个,绑定一个

那么现在,我们向下滑动6个:

 D/BaseAdapter: onCreateViewHolder: 
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{d730bb7 position=10 id=-1, oldPos=-1, pLpos:-1 no parent}----10
 D/BaseAdapter: onCreateViewHolder: 
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{e17ef24 position=11 id=-1, oldPos=-1, pLpos:-1 no parent}----11
 D/BaseAdapter: onCreateViewHolder: 
    onBindViewHolder: 
ViewHolder{4e5fa8d position=12 id=-1, oldPos=-1, pLpos:-1 no parent}----12
 D/BaseAdapter: onCreateViewHolder: 
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{e50d442 position=13 id=-1, oldPos=-1, pLpos:-1 no parent}----13
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{401cc06 position=14 id=-1, oldPos=-1, pLpos:-1 no parent}----14
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{52060f4 position=15 id=-1, oldPos=-1, pLpos:-1 no parent}----15
D/BaseAdapter: onBindViewHolder: 
ViewHolder{52060f4 position=16 id=-1, oldPos=-1, pLpos:-1 no parent}----16

我们可以看到这个时候onCreateViewHolder调用了4次,onBindViewHolder调用了7次,而且我如果继续向后滑动,也不会再去调用onCreateViewHolder,这是为什么呢?

现在我们向上滑动:

D/BaseAdapter: onBindViewHolder: 
ViewHolder{d0bd01d position=3 id=-1, oldPos=-1, pLpos:-1 no parent}----3
D/BaseAdapter: onBindViewHolder: 
ViewHolder{52060f4 position=2 id=-1, oldPos=-1, pLpos:-1 no parent}----2
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{391e2c7 position=1 id=-1, oldPos=-1, pLpos:-1 no parent}----1
 D/BaseAdapter: onBindViewHolder: 
ViewHolder{401cc06 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}----0

只有3,2,1,0调用了onBindViewHolder,我们明明是滑动了6个啊,这又是为什么呢?

而且接下来,无论我是向上滑动还是向下滑动,都不会有onCreateViewHolder方法的调用,也就是说对于这个例子,一共只创建了14个ViewHolder,这是为什么呢?

我们带着问题来探究:

我们知道在RecyclerView中,有一个内部类Recycler:

 public final class Recycler 

A Recycler is responsible for managing scrapped or detached item views for reuse.

根据这句话,我们知道就是它来管理RecyclerView的复用的。

下面是google对它注释:

A “scrapped” view is a view that is still attached to its parent RecyclerView but that has been marked for removal or reuse.
“scrapped” view 仍然attach在RecyclerView,但是被标记被删除了或者重复使用的。

Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for an adapter’s data set representing the data at a given position or item ID.
LayoutManager对Recycler的典型使用是获取适配器在给定位置或者item ID的视图
If the view to be reused is considered “dirty” the adapter will be asked to rebind it.
如果复用的视图被视为“dirty”,那么adapter将会重新绑定它
If not, the view can be quickly reused by the LayoutManager with no further work.
如果不是,就会被LayoutManager很快的重新复用,并且没有更多操作
Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
may be repositioned by a LayoutManager without remeasurement.
清除view时没有request layout,那么将被LayoutManger重新定位,但是不会重新测量。

我们知道RecyclerView的measure和layout过程都是全部交给了LayoutManager的,在之前的博客里面有分析到,其实到onMeasure的时候,就已经调用了Layout.onLayoutChildren方法,这个方法是在子类实现的,而且上面注释中有提到LayoutManager会对Recycler进行操作,
我们知道,在使用RecyclerVierw的时候,会先设置LayoutManager,那么就先看看这个吧:

public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        stopScroll();
        // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
        // chance that LayoutManagers will re-use views.
        //下面这些操作就是当mLayout!=null时,进行清除操作
        //第一次创建时,不会进入。
        if (mLayout != null) {
            // end all running animations
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            mLayout.removeAndRecycleAllViews(mRecycler);
            mLayout.removeAndRecycleScrapInt(mRecycler);
            mRecycler.clear();

            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        // this is just a defensive measure for faulty item animators.
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;

        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout
                        + " is already attached to a RecyclerView:"
                        + layout.mRecyclerView.exceptionLabel());
            }
            mLayout.setRecyclerView(this);
            //判断RecyclerView是否attach到了window
            //RecyclerView继承自ViewGroup,而且重写了onAttachedToWindow方法
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        //更新mCachedView的大小
        mRecycler.updateViewCacheSize();
        //重新布局
        requestLayout();
    }

我们来看一下mLayout.dispatchAttachedToWindow(this);

void dispatchAttachedToWindow(RecyclerView view) {
            mIsAttachedToWindow = true;
            onAttachedToWindow(view);
        }

 @CallSuper
        public void onAttachedToWindow(RecyclerView view) {
        }

此onAttachToWindow并且RecyclerView中的onAttachToWindow,此方法是在LayoutManager中,我们可以看到是一个空实现,所以就去看看子类咯,但是我找的LinearLayout中没有重写这个方法。。。。

然后我们重点看mRecycler.updateViewCacheSize();这个方法:


  private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
  int mViewCacheMax = DEFAULT_CACHE_SIZE;
  static final int DEFAULT_CACHE_SIZE = 2;

 void updateViewCacheSize() {
 //在dispatchLayoutStep3中,赋值mLayout.mPrefetchMaxCountObserved为0
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                    //将其放入mRecyclerPool中
                recycleCachedViewAt(i);
            }
        }

我们在上面代码中可以看到mCachedViews,它又是什么呢?

 public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = 
                new ArrayList<ViewHolder>();

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

 RecycledViewPool mRecyclerPool;


mAttachedScrap:用于缓存显示在屏幕上的item的ViewHolder,RecyclerView在onLayout的时候,会先把children都移除掉,再重新添加进去,所以这个List是用在布局过程中临时存放children的

mChangedScrap:不知道干嘛用的,RecyclerView的复用机制中没有看到它的存在

mCachedViews:滑动过程中的回收和复用都是先处理这个List,这个集合里存的是VewiHolder的原本数据,可以直接添加到RecyclerView中显示,不需要再次重新onBindViewHolder的。

mRecyclerPool:存在这里面的ViewHolder的数据信息会被重置掉,相当于ViewHolder是一个重新创建的一样,所以需要重新调用obBindViewHolder来绑定数据。

我们回到上面的方法,如果,mCachedScrap中有数据的话,就会重后往前一次recycler这些ViewHolder,直到只剩 mViewCacheMax个,如果只按英语翻译的话,可以知道 mViewCacheMax它应该是最大View的缓存量,目前来说,是2

虽然在setLayoutManager方法中,最后调用了requestLayout

@Override
    public void requestLayout() {
        if (mEatRequestLayout == 0 && !mLayoutFrozen) {
            super.requestLayout();
        } else {
            mLayoutRequestEaten = true;
        }
    }

但是我们现在还没有数据啊,然后我们继续看setAdapter方法:

 public void setAdapter(Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        //重新onMeasure和onLayout
        requestLayout();
    }

我们重点看看setAdapterInternal(adapter, false, true);方法:

private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            removeAndRecycleViews();
        }
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
        /注册观察者
            adapter.registerAdapterDataObserver(mObserver);
            //一个空实现
            adapter.onAttachedToRecyclerView(this);
        }
        if (mLayout != null) {
            //一个空实现
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        mState.mStructureChanged = true;
        setDataSetChangedAfterLayout();
    }

//recycler的
    void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
                boolean compatibleWithPrevious) {
              //清空mAttachedScrap
              //清空mCachedViews  
            clear();
            //告知RecyclerPool,adpter改变了
            getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
        }


        /**
         * Clear scrap views out of this recycler. Detached views contained within a
         * recycled view pool will remain.
         */
        public void clear() {
            mAttachedScrap.clear();
            //清空mCachedViews
            recycleAndClearCachedViews();
        }

可以看到mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);这个方法,是做了一些清除操作

然后我们继续往下看,setDataSetChangedAfterLayout();

//setAdapter,swapAdapter,或者notifyDataSetChanged都会调用这个方法
void setDataSetChangedAfterLayout() {
        mDataSetHasChangedAfterLayout = true;
       //让所有现在已知的视图无效
        markKnownViewsInvalid();
    }

这样数据就有了,那么调用requestLayout就会重新onMeasure和onLayout,我们知道在onMeasure的时候,会调用dispatchLayoutStep2方法,这个方法中会调用mLayout.onLayoutChildren(mRecycler, mState);那么我们来看看LinearLayout中的onLayoutChildren

/**在LinearLayout中*/
@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:   布局算法
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.      
        //通过检查孩子和其他变量,找到锚坐标和锚点项目位置   mAnchor为布局锚点 
        //mAnchor包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)
        // 2) fill towards start, stacking from bottom ,  开始填充, 从底部堆叠    从开始位置开始,向结束为位置堆叠填充itemView
        // 3) fill towards end, stacking from top    结束填充,从顶部堆叠
        // 4) scroll to fulfill requirements like stack from bottom.  //滚动以满足堆栈从底部的要求
        // create layout state

     ... ...

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        //这个方法会根据LinearLayoutManger构造中传入的布局方向给mShouldReverseLayout赋值
        //如果是竖直方向(VERTICAL),mShouldReverseLayout为false
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
           //重置mAchorInfo
            mAnchorInfo.reset();
            //得到堆叠方向   mShouldReverseLayout为false   mStackFromEnd默认为false
        //我们假定传入的方向是垂直方向,所以mAnchorInfo.mLayoutFromEnd = false
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {

         ... ...
        }
      ... ... 

        //重要的堆叠Item的方法,根据堆叠方向进行堆叠
        //如果是end方向    从底部开始堆叠
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start  
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end  
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            //如果排列方向是VERTICAL,走这里
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }

       ... ...
    }

上面的代码,会计算mAnchorInfo.mLayoutFromEnd的值,这个值是通过mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;进行计算的, mShouldReverseLayout的值在resolveShouldLayoutReverse();中获取,其中会根据布局朝向去给mShouldReverseLayout赋值,如果布局朝向是VERTICAL,就为false,反之true.mStackFromEnd是通过public void setStackFromEnd(boolean stackFromEnd)方法进行赋值,这个方法需要调用者调用,我们一般不调用,所以为初始值false.所以根据或运算,如果是竖直方向mAnchorInfo.mLayoutFromEnd为false.

得到了布局方向,就会调用相应的逻辑进行布局,最后填充的方法为fill.

/**在LinearLayout中,具体的填充方法*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        ... ...
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
           ... ...
           //填充核心方法,从复用集合中取ViewHolder
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ... ...
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;

          ... ...

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                //向复用集合中存ViewHolder 
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }

        return start - layoutState.mAvailable;
    }

fill方法中核心的填充方法layoutChunk,他会先从缓存中取ViewHolder,如果没有,就回去创建,之后会将创建好的ViewHolder放入复用集合中.我们先看layoutChunk如何填充的:

这个方法就是核心的布局方法,layoutState.next(recycler);是从缓存机制从取Item的具体方法

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
            //获取ItemView,会先从Scrap中取,如果没有会从一级缓存,二级缓存,
            //最后检查RecyclerViewPool,如果都没有就创建新的ViewHolder,
            //也就是调用了mAdapter.createViewHolder
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        //如果mSrapList为null,就将view填充进去,
        //这个mScrapList就是Item被移除屏幕被缓存起来的集合,
        //如果没有在mSrapList集合中,
        //就说明需要添加到RecyclerView中
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                    //将子View加到RecyclerView,
                    //其实就是ViewGroup的addView方法
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        //测量子控件的大小
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        //布局子view,zhege方法里面调用了child.layout方法,
        //参数就是计算出来的child的位置
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

这个方法会先从RecyelrVIew缓存中View,然后判断layoutState.mScrapList是否为null,如果为空,就表示这个view不在移除屏幕的位置,就要进行填充,调用addView方法,将view填充进来,这个方法内部就式调用viewGroup的addView方法.

之后调用measureChildWithMargins(view, 0, 0);对view进行测量,这就是为什么在RecyerlView的构造方法中没有看到对子view的测量,原来在这里测量.

之后调用layoutDecorated对view进行layout布局, 这个方法里面就是调用了child.layout方法对控件进行布局.

到了这里,recycleView的填充就此结束了,所有应该在recycleView可见区域的view就被填充到屏幕上了.

View被填充到屏幕上后,我们开始滑动,RecyclerView的滑动状态可以分为2个阶段:手指在屏幕上移动,使RecyclerView滑动的过程,可以称为scroll;手指离开屏幕,RecyclerView继续滑动一段距离的过程,可以称为fling。

跟手指有关,那么就可以看onTouchEvent了:

先看scroll,那么就是手指在屏幕上了:

 case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }
                //计算出手指移动的距离
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        //mTouchSlop 滑动阀值
                        if (dx > 0) {
                            dx -= mTouchSlop;
                        } else {
                            dx += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop;
                        } else {
                            dy += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (startScroll) {
                    //如果滑动距离大于滑动阀值的话,scollState就为SCROLL_STATE_DRAGGING
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //滑动  第一阶段scroll完成
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

我们可以看到,只要滑动有效,就会调用scrollByInternal方法:

 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            eatRequestLayout();
            onEnterLayoutOrScroll();
            TraceCompat.beginSection(TRACE_SCROLL_TAG);
            fillRemainingScrollValues(mState);
            if (x != 0) {
            //调用LayoutManager的scollHorzontallBy方法
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
            //调用LayoutManager的scrollVerticallyBy方法
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            TraceCompat.endSection();
            repositionShadowingViews();
            onExitLayoutOrScroll();
            resumeRequestLayout(false);
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

      ... ...
        return consumedX != 0 || consumedY != 0;
    }

我们可以看到具体的滑动逻辑还是交给了LayoutManager:

@Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }


int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        updateLayoutState(layoutDirection, absDy, true, state);
        //关注一下
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
        //上面讲到了fill()方法,作用就是向可绘制区域填充ItemView,
        //那么在这里,可绘制区域就是滑动偏移量!
        //mOrientationHelper.offsetChildren方法的作用就是平移ItemView。
        //平移了ItemView,在这里就完成了滑动
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }


 @Override
 public void offsetChildren(int amount) {
         mLayoutManager.offsetChildrenVertical(amount);
 }

最终还是交给了LayoutManager:

public void offsetChildrenVertical(int dy) {
            if (mRecyclerView != null) {
                mRecyclerView.offsetChildrenVertical(dy);
            }
        }

public void offsetChildrenVertical(int dy) {
        final int childCount = mChildHelper.getChildCount();
        for (int i = 0; i < childCount; i++) {
            mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
        }
    }
    //让每一个子view移动dy,然后重绘

还记得我们上面分析的fill方法吗?里面有一个layoutChunk方法,它调用layoutState.next(recycler)方法,这个方法里面又会调用recycler.getViewForPosition(mCurrentPosition);,我们View,就是从这里取的了:

public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }

        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

这个就是复用机制的入口,也就是Recycler开放给外部使用复用机制的api:

@Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
                //判断position是否在item范围内
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()
                        + exceptionLabel());
            }
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            //如果是isPreLayout,就在mChangedScrap中找
            //但是在layoutDispatchStep2中就将isPreLayout = false
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
               ... ... //省略,等会在看
            return holder;
        }

对于getChangedScrapViewForPosition(position);这个方法:

首先它会在mAattchedScrap中寻找:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();

            // Try first for an exact, non-invalid match from scrap.
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }

先在mAttachedSracp中寻找position一致的viewholder,这个viewholder没有被移除,是有效的。mAttachSracp中存是到底是什么呢?
onLayout,那要 layout 的话,RecyclerView 会先把所有 children 先 remove 掉,然后再重新 add 上去,完成一次 layout 的过程。那么这暂时性的 remove 掉的 viewHolder 要存放在哪呢,就是放在这个 mAttachedScrap 中了,这就是我的理解了。

所以,感觉这个 mAttachedScrap 中存放的 viewHolder 跟回收和复用关系不大。

if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    // This View is good to be used. We just need to unhide, detach and move to the
                    // scrap list.
                    final ViewHolder vh = getChildViewHolderInt(view);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    if (layoutIndex == RecyclerView.NO_POSITION) {
                        throw new IllegalStateException("layout index should not be -1 after "
                                + "unhiding a view:" + vh + exceptionLabel());
                    }
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }

然后在mHiddenViews中寻找该位置下隐藏了,但是还有效没有被remove掉的Viewholder,
中需要将它加进mAttachedScrap或者mChangedScrap中。

如果没有找到,就到mCachedViews中寻找:

  // Search in our first-level recycled view cache.
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                // invalid view holders may be in cache if adapter has stable ids as they can be
                // retrieved via getScrapOrCachedViewForId
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }

遍历 mCachedViews,找到 position 一致的 ViewHolder,之前说过,mCachedViews 里存放的 ViewHolder 的数据信息都保存着,所以 mCachedViews 可以理解成,只有原来的卡位可以重新复用这个 ViewHolder,新位置的卡位无法从 mCachedViews 里拿 ViewHolder出来用。

如找到了viewholder后:

if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                //再一次判断该ViewHodler是否符合
                    if (!validateViewHolderForOffsetPosition(holder)) {
                    如果不满足条件,就清除这个viewholder
                        // recycle holder (and unscrap if relevant) since it can't be used
                        if (!dryRun) {
                            // we would like to recycle this but need to make sure it is not used by
                            // animation logic etc.
                            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;
                    }
                }
            }

如果还是没有找到:

// 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

这异步一般不会执行,除非我们重写了Adapter 的 setHasStableIds()。getScrapOrCachedViewForId() 做的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其实差不多,只不过一个是通过 position 来找 ViewHolder,一个是通过 id 来找。而这个 id 并不是我们在 xml 中设置的 android:id, 而是 Adapter 持有的一个属性,默认是不会使用这个属性的

继续往下:

 if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder"
                                    + exceptionLabel());
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view." + exceptionLabel());
                        }
                    }
                }

这个就是常说扩展类了,RecyclerView 提供给我们自定义实现的扩展类,我们可以重写 getViewForPositionAndType() 方法来实现自己的复用策略。不过,也没用过,那这部分也当作不会执行,略过。继续往下

这个就是重点了:


 final int type = mAdapter.getItemViewType(offsetPosition);

 if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

 public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }

ViewPool会根据不同的view type创建不同的集合来存放ViewHolder,那么复用的时候,只要ViewPool里相同的type有ViewHolder缓存的话,就将最后一个拿出来复用。

拿到 ViewHolder 之后,还会再次调用 resetInternal() 来重置 ViewHolder,这样 ViewHolder 就可以当作一个全新的 ViewHolder 来使用了,这也就是为什么从这里拿的 ViewHolder 都需要重新 onBindViewHolder() 了。

 public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;

        static class ScrapData {
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

如果还是没有找到,就会重新创建:

if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS
                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        // abort - we have a deadline we can't meet
                        return null;
                    }
                    //重新创建
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) {
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        }
                    }

                    long end = getNanoTime();
                    mRecyclerPool.factorInCreateTime(type, end - start);
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                    }
                }

到这里,所有寻找holder的步骤就结束了,然后都要继续往下判断是否需要重新绑定数据,还有检查布局参数是否合法。

if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //在tryBindViewHolderByDeadline方法中会调用bindViewHolder,
                //在bindViewHolder中会调用onBindViewHolder
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

            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 = fromScrapOrHiddenOrCache && bound;
            return holder;

到这里,tryGetViewHolderForPositionByDeadline() 这个方法就结束了。这大概就是 RecyclerView 的复用机制,中间我们跳过很多地方,因为 RecyclerView 有各种场景可以刷新他的 view,比如重新 setLayoutManager(),重新 setAdapter(),或者 notifyDataSetChanged(),或者滑动等等之类的场景,只要重新layout,就会去回收和复用 ViewHolder,所以这个复用机制需要考虑到各种各样的场景。

回收机制

在上面分析中,我们知道在滑动的时候,最终会调用LayoutManagerscrollVerticallyBy对于竖直滑动来说,然后会调用scrllBy方法,在这个方法中,会调用fill方法去处理需要复用和回收的卡位:

上面分析中,我们知道在fill方法里面调用了layoutChunk方法,这个方法里面有关于复用的操作,它还调用了recycleByLayoutState(recycler, layoutState);,这个方法里面就有关于回收操作,它最终会调用:

 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);
            }
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            recycleViewHolderInternal(holder);
        }

最后把回收工作交给了recycleViewHolderInternal(holder);

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 (DEBUG && mCachedViews.contains(holder)) {
                throw new IllegalArgumentException("cached view received recycle internal? "
                        + holder + exceptionLabel());
            }
            if (forceRecycle || holder.isRecyclable()) {
                //mViewCacheMax默认大小为2
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                         //如果mCachedScrap满了,把0的ViewHolder放入ViewPool
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    //放入mCachedViews
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    //放入ViewPool中
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {

               ... ... 
            // 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;
            }
        }

然后我们回到刚开始的问题,我第一次向下滑动了6个,调用了3次onCreateViewHolder,调用了6次onBindViewHolder;
滑动的时候肯定是先服用再回收。
第一次向上滑动的时候,当我滑动一个的时候,mCachedViews没有东西,mViewPool也没有东西,所以会调用onCreateViewHolder,在回收的时候,只有满足:


final int limit = dt;
if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, 0, i);
                    return;
                }

才会回收,所以现在还不会回收,当滑动了三个后,才开始回收,回收不是一个个的回收,是从0到 i 回收。

以这个例子来说,当我滑动3三个后,只有0回收了,所以当我是滑动6个时候,会先创建3个,后面的由于缓存中有东西了,就直接复用了,所以只需要调用onBindViewHolder方法。

当我滑动6个后,再回去到0,只有0,1,2,3调用了onBindViewHolder方法,因为mCachedView里面刚好有4,5位置的ViewHolder,就可以直接拿来用,所以就不用重新绑定,而3,2,1,0需要从ViewPool中拿出来重新绑定数据。mCachedView的默认大小为2,所以当它的大小到达2时,会将0位置的移到ViewPool,把最新的加到mCachedView。

在复用时候,会mCachedView中取的时候,是通过位置取的,不是当前的位置,就会向ViewPool中取,ViewPool是通过类型取的。

再向后面继续滑动的时候,因为mCachedView和ViewPool里面都有东西了,就不会再创建新的了。

本文参考:https://www.jianshu.com/p/9306b365da57

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值