Android高级UI之RecyclerView回收复用机制以及自定义LayoutManager

搞清楚的几个问题

1.RecyclerView回收复用的谁?
2.RecyclerView有几级缓存?
3.如何实现自定义LayoutManager?

四级缓存

	/**
     * A Recycler is responsible for managing scrapped or detached item views for reuse.
     *
     * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
     * that has been marked for removal or reuse.</p>
     *
     * <p>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.
     * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
     * If not, the view can be quickly reused by the LayoutManager with no further work.
     * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
     * may be repositioned by a LayoutManager without remeasurement.</p>
     */
    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);

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;

...

一级缓存(scrap):mChangedScrap 与 mAttachedScrap
mChangedScrap 与 mAttachedScrap称为scrap,用来缓存屏幕内的ViewHolder。

这两个scrap就是第一级缓存,是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这两个缓存来找。其中mAttachedScrap存储的是当前还在屏幕中的ViewHolder,mChangedScrap存储的是数据被更新的ViewHolder,比如说调用了Adapter的notifyItemChanged方法。

RecyclerView之所以要将缓存分成这么多块肯定在功能上是有一定的区分的,它们分别对应不同的使用场景,scrap是用来保存被RecyclerView移除掉但最近又马上要使用的缓存,比如说RecyclerView中自带item的动画效果,本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画。如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中。

二级缓存(cache):mCachedViews
用来缓存移除屏幕之外的ViewHolder ,默认大小为2。

也是RecyclerView中非常重要的一个缓存,就linearlayoutmanager来说cached缓存默认大小为2,它的容量非常小,所起到的作用就是RecyclerView滑动时刚被移出屏幕的viewholer的收容所,因为RecyclerView会认为刚被移出屏幕的viewholder可能接下来马上就会使用到,所以不会立即设置为无效viewholer,会将它们保存到cached中,但又不能将所有移除屏幕的viewholder都视为有效viewholer,所以它的默认容量只有2个。

三级缓存(cacheExtension):mViewCacheExtension
开发给用户的自定义扩展缓存,需要用户自己管理View的创建和缓存 ,通常用不到。
如果使用,需要调用Recycler的setViewCacheExtension(ViewCacheExtension extension)方法进行设定。

四级缓存(pool):RecycledViewPool
ViewHolder 缓存池。
根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为5。

保存的对象就是那些无效的ViewHolder ,虽说无效的ViewHolder 上的数据是无效的,但是它的rootview还是可以拿来使用的,RecycledViewPool一般会和mCachedViews配合使用,mCachedViews存不下的会被保存到RecycledViewPool中,毕竟mCachedViews默认容量大小只有2,但是RecycledViewPool容量也是有限的,当保存满之后再有ViewHolder添加的话会直接丢弃。

使用多级缓存的目的:为了提高性能。

ViewHolder

对于传统的AdapterView,需要在实现的Adapter类中手动加ViewHolder,RecyclerView直接将ViewHolder内置,并在原来基础上功能上更强大。ViewHolder描述RecylerView中某个位置的itemView和元数据信息,属于Adapter的一部分。其实现类通常用于保存findViewById的结果。 主要元素组成有:

public static abstract class ViewHolder {
    
    

        public final View itemView;//itemView
        WeakReference<RecyclerView> mNestedRecyclerView;
        int mPosition = NO_POSITION; //位置
        int mOldPosition = NO_POSITION;//上一次的位置
        long mItemId = NO_ID;
        int mItemViewType = INVALID_TYPE;
        int mPreLayoutPosition = NO_POSITION;

        // The item that this holder is shadowing during an item change event/animation
        ViewHolder mShadowedHolder = null;
        // The item that is shadowing this holder during an item change event/animation
        ViewHolder mShadowingHolder = null;

        /**
         * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
         * are all valid.
         */
        static final int FLAG_BOUND = 1 << 0;

        /**
         * The data this ViewHolder's view reflects is stale and needs to be rebound
         * by the adapter. mPosition and mItemId are consistent.
         */
        static final int FLAG_UPDATE = 1 << 1;

        /**
         * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
         * are not to be trusted and may no longer match the item view type.
         * This ViewHolder must be fully rebound to different data.
         */
        static final int FLAG_INVALID = 1 << 2;

        /**
         * This ViewHolder points at data that represents an item previously removed from the
         * data set. Its view may still be used for things like outgoing animations.
         */
        static final int FLAG_REMOVED = 1 << 3;

        /**
         * This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
         * and is intended to keep views around during animations.
         */
        static final int FLAG_NOT_RECYCLABLE = 1 << 4;

        /**
         * This ViewHolder is returned from scrap which means we are expecting an addView call
         * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until
         * the end of the layout pass and then recycled by RecyclerView if it is not added back to
         * the RecyclerView.
         */
        static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5;

        /**
         * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove
         * it unless LayoutManager is replaced.
         * It is still fully visible to the LayoutManager.
         */
        static final int FLAG_IGNORE = 1 << 7;

        /**
         * When the View is detached form the parent, we set this flag so that we can take correct
         * action when we need to remove it or add it back.
         */
        static final int FLAG_TMP_DETACHED = 1 << 8;

        /**
         * Set when we can no longer determine the adapter position of this ViewHolder until it is
         * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is
         * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon
         * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is
         * re-calculated.
         */
        static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9;

        /**
         * Set when a addChangePayload(null) is called
         */
        static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10;

        /**
         * Used by ItemAnimator when a ViewHolder's position changes
         */
        static final int FLAG_MOVED = 1 << 11;

        /**
         * Used by ItemAnimator when a ViewHolder appears in pre-layout
         */
        static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;

        static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1;

        /**
         * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
         * hidden list (as if it was scrap) without being recycled in between.
         *
         * When a ViewHolder is hidden, there are 2 paths it can be re-used:
         *   a) Animation ends, view is recycled and used from the recycle pool.
         *   b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
         *
         * This flag is used to represent "case b" where the ViewHolder is reused without being
         * recycled (thus "bounced" from the hidden list). This state requires special handling
         * because the ViewHolder must be added to pre layout maps for animations as if it was
         * already there.
         */
        static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;

        /**
         * Flags that RecyclerView assigned {@link RecyclerViewAccessibilityDelegate
         * #getItemDelegate()} in onBindView when app does not provide a delegate.
         */
        static final int FLAG_SET_A11Y_ITEM_DELEGATE = 1 << 14;

        int mFlags;


		...

关于ViewHolder,重点介绍的是mFlags。
FLAG_BOUND——ViewHolder已经绑定到某个位置,mPosition、mItemId、mItemViewType都有效
FLAG_UPDATE——ViewHolder绑定的View对应的数据过时,需要重新绑定,mPosition、mItemId还是一致的。
FLAG_INVALID——ViewHolder绑定的View对应的数据无效,需要完全重新绑定不同的数据 ,mPosition、mItemId已经和ViewHolder不对应。
FLAG_REMOVED——ViewHolder对应的数据已经从数据集移除
FLAG_NOT_RECYCLABLE——ViewHolder不能复用
FLAG_RETURNED_FROM_SCRAP——这个状态的ViewHolder会加到scrap list被复用。
FLAG_CHANGED——ViewHolder内容发生变化,通常用于表明有ItemAnimator动画
FLAG_IGNORE——ViewHolder完全由LayoutManager管理,不能复用
FLAG_TMP_DETACHED——ViewHolder从父RecyclerView临时分离的标志,便于后续移除或添加回来
FLAG_ADAPTER_POSITION_UNKNOWN——ViewHolder不知道对应的Adapter的位置,直到绑定到一个新位置
FLAG_ADAPTER_FULLUPDATE——方法addChangePayload(null)调用时设置

回收复用

回收什么?复用什么?
回收复用的是ViewHolder

回收到哪里去?从哪里获得复用?
四级缓存,分别对应四种集合:

  1. mAttachedScrap 和 mChangedScrap
  2. mCachedViews
  3. mViewCacheExtension
    这个的创建和缓存完全由开发者自己控制,系统未往这里添加数据
  4. RecycledViewPool

什么时候回收?什么时候复用?
在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。

复用(即读取缓存)

在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。

注意布局发生的场景有很多,比如:
1.第一次显示RecyclerView时
2.页面横竖屏切换改变RecyclerView布局时
3.RecyclerView重新排序屏幕中item时(可以做这种功能)
等等

1.页面进行布局( RecyclerView#onLayout() )的复用(即读取缓存)时序图:
在这里插入图片描述

2.滑动( RecyclerView#onTouchEvent() )时的复用(即读取缓存)的调用过程:
onTouchEvent() --> scrollByInternal() --> scrollStep() --> mLayout.scrollVerticallyBy()
–> scrollBy() --> fill() --> layoutChunk() --> View view = layoutState.next(recycler); addView(view);

源码入口:onTouchEvent()的ACTION_MOVE事件

//RecyclerView.java

	@Override
    public boolean onTouchEvent(MotionEvent e) {
    
    

		...

	   case MotionEvent.ACTION_MOVE: {
    
    
		
		...

	       if (scrollByInternal(
	                    canScrollHorizontally ? dx : 0,
	                    canScrollVertically ? dy : 0,
	                    vtev)) {
    
    
	                getParent().requestDisallowInterceptTouchEvent(true);
	            }
	    ...
	    } break;


		...


}

fill()方法:获取itemview填充RecyclerView

//LinearLayoutManager.java

    /**
     * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
     * independent from the rest of the {@link LinearLayoutManager}
     * and with little change, can be made publicly available as a helper class.
     *
     * @param recycler        Current recycler that is attached to RecyclerView
     * @param layoutState     Configuration on how we should fill out the available space.
     * @param state           Context passed by the RecyclerView to control scroll steps.
     * @param stopOnFocusable If true, filling stops in the first focusable new child
     * @return Number of pixels that it added. Useful for scroll functions.
     */
    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;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
    
    
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
    
    
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            //滑动时回收ViewHolder
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //循环判断RecyclerView是否还有空间,如果有,执行layoutChunk方法填充RecyclerView,直至填充满。
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
    
    
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
    
    
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            //复用
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
    
    
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
    
    
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            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.SCROLLING_OFFSET_NaN) {
    
    
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
    
    
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
    
    
                break;
            }
        }
        if (DEBUG) {
    
    
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

从缓存中获取View就是layoutState.next(recycler)方法:

//LinearLayoutManager.java

        /**
         * Gets the view for the next element that we should layout.
         * Also updates current item index to the next item, based on {@link #mItemDirection}
         *
         * @return The next element that we should layout.
         */
        View next(RecyclerView.Recycler recycler) {
    
    
            if (mScrapList != null) {
    
    
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

layoutState.next() --> getViewForPosition() --> tryGetViewHolderForPositionByDeadline()

tryGetViewHolderForPositionByDeadline()方法主要功能就是从缓存中获取ViewHolder,分析下该方法:

//RecyclerView.java


		/**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * <p>
         * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return
         * rather than constructing or binding a ViewHolder if it doesn't think it has time.
         * If a ViewHolder must be constructed and not enough time remains, null is returned. If a
         * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is
         * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this.
         *
         * @param position Position of ViewHolder to be returned.
         * @param dryRun True if the ViewHolder should not be removed from scrap/cache/
         * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
         *                   complete. If FOREVER_NS is passed, this method will not fail to
         *                   create/bind the holder if needed.
         *
         * @return ViewHolder for requested position
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    
    
            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
            if (mState.isPreLayout()) {
    
    
	            /**
	            	判断mInPreLayout变量(默认为false),当有动画时此变量才为true,即只有有动画时,mChangedScrap才起作用。
	            */
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
    
    
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
    
    
                    if (!validateViewHolderForOffsetPosition(holder)) {
    
    
                        // 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;
                    }
                }
            }
            if (holder == null) {
    
    
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
    
    
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount() + exceptionLabel());
                }

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 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;
                    }
                }
                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());
                        }
                    }
                }
                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);
                        }
                    }
                }
                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");
                    }
                }
            }

            // This is very ugly but the only place we can grab this information
            // before the View is rebound and returned to the LayoutManager for post layout ops.
            // We don't need this in pre-layout since the VH is not updated by the LM.
            if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
    
    
                holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                if (mState.mRunSimpleAnimations) {
    
    
                    int changeFlags = ItemAnimator
                            .buildAdapterChangeFlagsForAnimations(holder);
                    changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                    final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                            holder, changeFlags, holder.getUnmodifiedPayloads());
                    recordAnimationInfoIfBouncedHiddenView(holder, info);
                }
            }

            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()) {
    
    
                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);
                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;
        }

分几种情况去各种缓存集合中获取缓存的ViewHolder:

  1. getChangedScrapViewForPosition()
    mChangeScrap, 与动画相关

  2. getScrapOrHiddenOrCachedHolderForPosition()
    mAttachedScrap 、mCachedViews

  3. getScrapOrCachedViewForId()
    mAttachedScrap 、mCachedViews

  4. mViewCacheExtension.getViewForPositionAndType()
    自定义缓存

  5. getRecycledViewPool().getRecycledView()
    从缓冲池里面获取

  6. 当各级缓存都没有读取到的时候调用 mAdapter.createViewHolder() --> onCreateViewHolder() 创建ViewHolder

  7. 获取ViewHolder后根据ViewHolder的mFlags值判断是否需要调用tryBindViewHolderByDeadline() 方法(当ViewHolder还没有绑定过或者需要更新数据或者数据已经无效时会调用): tryBindViewHolderByDeadline() --> mAdapter.bindViewHolder() --> onBindViewHolder()

在这里插入图片描述

回收(即写入缓存):

所谓回收,就是看RecyclerView是怎么往四级缓存中添加ViewHolder的。

在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。

1.在页面进行滑动( RecyclerView#onTouchEvent() )时写入缓存的调用过程:
onTouchEvent() --> scrollByInternal() --> scrollStep() --> mLayout.scrollVerticallyBy()
–> scrollBy() --> fill -->recycleByLayoutState --> recycleViewsFromStart --> recycleChildren
–> removeAndRecycleViewAt --> recycler.recycleView
–> recycler.recycleViewHolderInternal(viewHolder);

滑动时调用fill方法时,fill方法会判断是否调用recycleByLayoutState 方法进行回收ViewHolder
在这里插入图片描述

可以看到滑动时回收View最终是直接调用recycleViewHolderInternal()将ViewHolder直接缓存到mCachedViews和RecyclerViewPool 中,而不会像布局时那样调用scrapOrRecycleView()方法判断是否将ViewHolder放入scrap中。

2.在页面进行布局( RecyclerView#onLayout() )时写入缓存的时序图:
在这里插入图片描述

LinearLayoutManager.onLayoutChildren() --> detachAndScrapAttachedViews() --> scrapOrRecycleView()

//LinearLayoutManager.java

    @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.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
		
		...
		//在布局之前,将所有子View先detach掉,并放入缓存(包括四级缓存)中
		detachAndScrapAttachedViews(recycler);

		...
		//然后调用fill()方法循环不断从缓存中取出View加入到RecyclerView中,直到占满RecyclerView
		fill(recycler, mLayoutState, state, false);
		
		...

	}

为什么LayoutManager需要先执行detach,然后再重新attach这些view呢?
是为了隔离LayoutManager和RecyclerView.Recycler之间的关注点/职责。LayoutManager不需要知道哪些子view需要保留或者被回收到RecyclerViewPool或者其他地方,这是Recycler的职责。

//RecyclerView.java


        /**
         * Temporarily detach and scrap all currently attached child views. Views will be scrapped
         * into the given Recycler. The Recycler may prefer to reuse scrap views before
         * other views that were previously recycled.
         *
         * @param recycler Recycler to scrap views into
         */
        public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    
    
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
    
    
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }

        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    
    
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
    
    
                if (DEBUG) {
    
    
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
    
    
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);//缓存到mCachedViews和RecyclerViewPool
            } else {
    
    
                detachViewAt(index);
                recycler.scrapView(view);//将view缓存到scrap(即mAttachedScrap和mChangedScrap)
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

RecyclerView回收View有两种方式:Detach和Remove。Detach的View放在Scrap缓存中,Remove掉的View放在mCachedViews和RecyclerViewPool缓存中;那我们应该如何去选择呢?

在什么样的场景中使用Detach呢?主要是在我们的代码执行结束之前,我们需要反复去将View移除并且马上又要添加进去时,选择Detach方式,比如:当我们对View进行重新排序的时候,可以选择Detach,因为屏幕上显示的就是这些position对应的View,我们并不需要重新去绑定数据,这明显可以提高效率。使用Detach的方式可以通过函数detachAndScrapView()实现。

在什么样的场景中使用Remove呢?使用Remove的方式,是当View不在屏幕中有任何显示的时候,你需要将它Remove掉,以备后面循环利用,比如滑动RecyclerView时,滑出屏幕的itemview就是使用Remove的方式进行回收。使用Remove的方式可以通过函数removeAndRecycleView()实现。

1.recycler.recycleViewHolderInternal(viewHolder);

缓存到mCachedViews和RecyclerViewPool 。

        /**
         * internal implementation checks if view is scrapped or attached and throws an exception
         * if so.
         * Public version un-scraps before calling recycle.
         */
        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());
            }
            //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()) {
    
    
            	//如果ViewHodler没有改变
                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();
                    /**
                    	如果mCachedViews.size大于mViewCacheMax(默认是DEFAULT_CACHE_SIZE = 2;),
                    	则调用recycleCachedViewAt(0)将mCachedViews中最旧的ViewHolder加入RecycledViewPool缓存池中,
                    	并在mCachedViews中将最旧的ViewHolder删除。所以​缓存池里面的数据都是从mCachedViews里面出来的。
                    */
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    
    
                        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.add(targetCacheIndex, holder);
                    cached = true;
                }
                //如果没有缓存到mCachedViews中,则直接加入到RecycledViewPool缓存池中
                if (!cached) {
    
    
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
    
    
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.

                // TODO: consider cancelling an animation when an item is removed scrollBy,
                // to return it to the pool faster
                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"
                            + exceptionLabel());
                }
            }
            // 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;
            }
        }

在这里插入图片描述

mCachedViews和RecyclerViewPool:
在这里插入图片描述
添加ViewHolder时,如果mCachedViews中缓存已满,则先将mCachedViews中的第0个元素移除并放入RecycledViewPool缓存池中,然后将待添加的ViewHolder加入到mCachedViews中。

RecycledViewPool缓存池

	 /**
     * RecycledViewPool lets you share Views between multiple RecyclerViews.
     * <p>
     * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
     * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
     * <p>
     * RecyclerView automatically creates a pool for itself if you don't provide one.
     */
    public static class RecycledViewPool {
    
    
        private static final int DEFAULT_MAX_SCRAP = 5;

        /**
         * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
         *
         * Note that this tracks running averages of create/bind time across all RecyclerViews
         * (and, indirectly, Adapters) that use this pool.
         *
         * 1) This enables us to track average create and bind times across multiple adapters. Even
         * though create (and especially bind) may behave differently for different Adapter
         * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
         *
         * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
         * false for all other views of its type for the same deadline. This prevents items
         * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
         */
        static class ScrapData {
    
    
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

        private int mAttachCount = 0;


		...
		

		 /**
         * Add a scrap ViewHolder to the pool.
         * <p>
         * If the pool is already full for that ViewHolder's type, it will be immediately discarded.
         *
         * @param scrap ViewHolder to be added to the pool.
         */
        public void putRecycledView(ViewHolder scrap) {
    
    
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            //如果RecycledViewPool中对应viewType的ViewHolder已经存满了(mMaxScrap默认值是5,即每一种ViewType的ViewHolder默认是缓存5个),
            //则本次要缓存的ViewHolder直接丢掉,因为RecycledViewPool缓存的只是ViewHolder类型,ViewHolder里面没有数据。
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
    
    
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
    
    
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();//清空ViewHolder的数据
            scrapHeap.add(scrap);//然后将ViewHolder加入缓存池中
        }

	...

}

RecycledViewPool 的putRecycledView()方法先清空ViewHolder的数据,然后才将ViewHolder加入缓存池中,所以RecycledViewPool缓存池中缓存的是ViewHolder类型,ViewHolder里面没有数据。这与mCachedViews是不同的,mCachedViews缓存的是带有数据的ViewHolder。

2.recycler.scrapView(view);

缓存到scrap(即mAttachedScrap和mChangedScrap)。

//RecyclerView.java

        /**
         * Mark an attached view as scrap.
         *
         * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
         * for rebinding and reuse. Requests for a view for a given position may return a
         * reused or rebound scrap view instance.</p>
         *
         * @param view View to scrap
         */
        void scrapView(View view) {
    
    
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
    
    
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
    
    
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
    
    
                if (mChangedScrap == null) {
    
    
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

这个方法根本的目的就是,判断ViewHolder的flag状态,从而来决定是放入mAttachedScrap还是mChangedScrap。从上面的代码,我们得出:

  1. mAttachedScrap里面放的是两种状态的ViewHolder:
    1). 被同时标记为remove和invalid的ViewHolder;
    2). 完全没有改变的ViewHolder,即不需要更新的ViewHolder。
    3). 这里还有第三个判断,这个跟RecyclerView的ItemAnimator有关,如果ItemAnimator为空或者ItemAnimator的canReuseUpdatedViewHolder方法为true,也会放入到mAttachedScrap。

  2. 那么mChangedScrap里面放什么类型flag的ViewHolder呢?当然是ViewHolder的isUpdated方法返回为true时,会放入到mChangedScrap里面去。所以,调用Adapter的notifyItemChanged方法时,并且RecyclerView的ItemAnimator不为空,会放入到mChangedScrap里面。即mChangedScrap用来保存RecyclerView做动画时,被detach的ViewHolder。

notifyDataSetChanged

notifyDataSetChanged–>mObservable.notifyChanged
–> (RecyclerViewDataObserver)mObservers.get(i).onChanged --> requestLayout

自定义LayoutManager

开发中常见的RecyclerView问题

常见面试题

RecyclerView的复用机制是怎么样的?

RecyclerView支持多个不同类型的布局,他们是怎么缓存并且查找的呢?

RecyclerView的item能不能直接调用setTag(),而不传递键的方式,如setTag(“字符串”)

为什么RecyclerView要用到适配器呢,你对适配器的理解是什么?

RecyclerView一屏加载的个数是怎么确定的,他是怎么做到不显示的Item缓存到内存中的?

RecyclerView在填充item的过程中,每填充一个item,RecyclerView的bottom都会加上这个item的高度,直到RecyclerView的bottom大于屏幕的高度,填充完毕。

有看过RecyclerView的边界判断的源码吗?简单聊下他的判断机制,什么时候该回收?

https://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView?hl=en

RecyclerView一些你可能需要知道的优化技术
RecyclerView 源码分析(三) - RecyclerView的缓存机制
RecyclerView缓存机制(scrap view)
打造属于你的LayoutManager

基于场景解析RecyclerView的回收复用机制原理
RecyclerView源码分析
Android源码分析之RecyclerView源码分析(二)——缓存机制
Android-RecyclerView布局显示和回收复用流程

阿里3轮面试都问了RecyclerView
阿里3轮面试都问了RecyclerView
地狱难度!字节跳动Android高级岗:说说RecyclerView的回收复用机制

猜你喜欢

转载自blog.csdn.net/yzpbright/article/details/108953435