RecyclerView缓存策略

RecyclerView缓存策略

首先,我们还是看一下RecycerView典型的Adapter的实现:

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerviewAdapter.MyViewHolder> {

    private Context context;
    private List<String> data;

    public MyRecyclerViewAdapter(Context context,List<String> data){
        this.context = context;
        this.data = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.recycler_item_my, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
        holder.name.setText(data.get(position));

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("这里是点击每一行item的响应事件",""+position+item);
            }
        });

    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    public class MyViewHolder extends RecyclerView.ViewHolder{
        TextView name;

        public MyViewHolder(View itemView) {
            super(itemView);
            name = itemView.findViewById(R.id.name);
        }
    }
}
复制代码

可以看到Adapter主要是两个方法在缓存中起作用:用来创建新的ViewHolder的onCreateViewHolder,以及用来显示数据的onBindViewHolder方法。

2.1 Recycler

同ListView中的RecycleBin一样,RecyclerView中的缓存也由一个内部类Recycler进行管理。Recycler里面有四个不同层次的缓存,比ListView层次要丰富一点,当然,这与RecyclerView拓展性更好有一定的关系。

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;
复制代码

解释如下:

  • mAttachedScrap、mChangedScrap

    一级缓存,同ListView中ActionViews,在layout发生前将屏幕上面的ViewHolder保存起来,供layout中进行复用

  • mCachedViews

    二级缓存,默认大小保持在DEFAULT_CACHE_SIZE = 2,可以通过RecyclerView.setItemViewCacheSize(int)方法进行设置 mCachedViews数量如果超出限制,会根据索引将里面旧的移动到RecyclerViewPool中

  • ViewCacheExtension

    三级缓存,开发者可以自定义的缓存

  • RecyclerViewPool

    四级缓存,可以在多个RecyclerView中共享缓存

    根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为DEFAULT_MAX_SCRAP = 5,超过部分会丢弃,可以通过其setMaxRecycledViews(int viewType, int max)方法来控制对应type的缓存池大小。

Recycler的方法本质上就是对上面数据结构的一些操作。主要的方法有:

  • recycleView(View)

    将view对应的ViewHolder移动到mCachedViews中;如果View是scrapped状态,会先unscrap

  • recycleViewHolderInternal(ViewHolder)

    将ViewHolder保存到mCachedViews中

  • addViewHolderToRecycledViewPool(ViewHolder, boolean)

    将ViewHolder保存到RecycledViewPool中

  • scrapView(View)

    将一个attached状态的View保存到mAttachedScrap或mChangedScrap中

  • getChangedScrapViewForPosition(int)

    从mChangedScrap中寻找匹配的ViewHolder

  • getScrapOrHiddenOrCachedHolderForPosition(int, boolean)

    依次从mAttachedScrap、mCachedViews中寻找匹配的ViewHolder

  • getScrapOrCachedViewForId(long, int, boolean)

    依次从mAttachedScrap、mCachedViews中寻找匹配的ViewHolder

  • tryGetViewHolderForPositionByDeadline(int, boolean, long)

    从mChangedScrap、mAttachedScrap、mCachedViews、ViewCacheExtension、RecycledViewPool中进行匹配;若匹配不了,最后会直接调用Adapter.createViewHolder方法进行创建

  • tryBindViewHolderByDeadline(ViewHolder, int, int, long)

    调用Adapter.bindViewHolder方法绑定View

2.2 缓存流程

RecyclerView的缓存流程同ListView一样,也是体现在了layout过程中。由于RecyclerView layout过程中步骤比较多,而这些内容不是本章的重点,所以这里只给出大致流程,重点放到缓存流程中。

RecyclerView的layout流程分为三个方法,对应layout step的三个步骤。这部分表格如下:

image.png

本章中我们关注的重点显然是在dispatchLayoutStep2方法中:

/**
  * The second layout step where we do the actual layout of the views for the final state.
  * This step might be run multiple times if necessary (e.g. measure).
  */
private void dispatchLayoutStep2() {
    eatRequestLayout();
    onEnterLayoutOrScroll();
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;

    // onLayoutChildren may have caused client code to disable item animations; re-check
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    resumeRequestLayout(false);
}
复制代码

该方法比较简单,重点在第15行的mLayout.onLayoutChildren(mRecycler, mState)方法中。这里mLayout我们选择最常用的LinearLayoutManager进行分析。LinearLayoutManager.onLayoutChildren方法如下:

/**
  * {@inheritDoc}
  */
@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
    if (DEBUG) {
        Log.d(TAG, "is pre layout:" + state.isPreLayout());
    }
    if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
        if (state.getItemCount() == 0) {
            removeAndRecycleAllViews(recycler);
            return;
        }
    }
    if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
        mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
    }

    ensureLayoutState();
    mLayoutState.mRecycle = false;
    // resolve layout direction
    resolveShouldLayoutReverse();

    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        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())) {
        // This case relates to when the anchor child is the focused view and due to layout
        // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
        // up after tapping an EditText which shrinks RV causing the focused view (The tapped
        // EditText which is the anchor child) to get kicked out of the screen. Will update the
        // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
        // the available space in layoutState will be calculated as negative preventing the
        // focused view from being laid out in fill.
        // Note that we won't update the anchor position between layout passes (refer to
        // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
        // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
        // child which can change between layout passes).
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused);
    }
    if (DEBUG) {
        Log.d(TAG, "Anchor info:" + mAnchorInfo);
    }

    // LLM may decide to layout items for "extra" pixels to account for scrolling target,
    // caching or predictive animations.
    int extraForStart;
    int extraForEnd;
    final int extra = getExtraLayoutSpace(state);
    // If the previous scroll delta was less than zero, the extra space should be laid out
    // at the start. Otherwise, it should be at the end.
    if (mLayoutState.mLastScrollDelta >= 0) {
        extraForEnd = extra;
        extraForStart = 0;
    } else {
        extraForStart = extra;
        extraForEnd = 0;
    }
    extraForStart += mOrientationHelper.getStartAfterPadding();
    extraForEnd += mOrientationHelper.getEndPadding();
    if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION
            && mPendingScrollPositionOffset != INVALID_OFFSET) {
        // if the child is visible and we are going to move it around, we should layout
        // extra items in the opposite direction to make sure new items animate nicely
        // instead of just fading in
        final View existing = findViewByPosition(mPendingScrollPosition);
        if (existing != null) {
            final int current;
            final int upcomingOffset;
            if (mShouldReverseLayout) {
                current = mOrientationHelper.getEndAfterPadding()
                        - mOrientationHelper.getDecoratedEnd(existing);
                upcomingOffset = current - mPendingScrollPositionOffset;
            } else {
                current = mOrientationHelper.getDecoratedStart(existing)
                        - mOrientationHelper.getStartAfterPadding();
                upcomingOffset = mPendingScrollPositionOffset - current;
            }
            if (upcomingOffset > 0) {
                extraForStart += upcomingOffset;
            } else {
                extraForEnd -= upcomingOffset;
            }
        }
    }
    int startOffset;
    int endOffset;
    final int firstLayoutDirection;
    if (mAnchorInfo.mLayoutFromEnd) {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                : LayoutState.ITEM_DIRECTION_HEAD;
    } else {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
    }

    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mInfinite = resolveIsInfinite();
    mLayoutState.mIsPreLayout = state.isPreLayout();
    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 {
        // 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;
        }
    }

    // changes may cause gaps on the UI, try to fix them.
    // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
    // changed
    if (getChildCount() > 0) {
        // because layout from end may be changed by scroll to position
        // we re-calculate it.
        // find which side we should check for gaps.
        if (mShouldReverseLayout ^ mStackFromEnd) {
            int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
            startOffset += fixOffset;
            endOffset += fixOffset;
            fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
            startOffset += fixOffset;
            endOffset += fixOffset;
        } else {
            int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
            startOffset += fixOffset;
            endOffset += fixOffset;
            fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
            startOffset += fixOffset;
            endOffset += fixOffset;
        }
    }
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    if (!state.isPreLayout()) {
        mOrientationHelper.onLayoutComplete();
    } else {
        mAnchorInfo.reset();
    }
    mLastStackFromEnd = mStackFromEnd;
    if (DEBUG) {
        validateChildOrder();
    }
}
复制代码

方法很长,但还好有一些注释。

  1. 首先,从开头到第112行都是第一步的内容:计算锚点坐标以及锚点item的position。谁让112行是onAnchorReady方法呢,太明显了。

  2. 注意第113行的detachAndScrapAttachedViews方法,该方法会对所有的子View调用scrapOrRecycleView方法。这样所有的子View都会暂时detach掉,并保存到mAttachedScrap或mChangedScrap或mCachedViews中,等待后续复用。

     /**
     * 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(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);
     } else {
         detachViewAt(index);
         recycler.scrapView(view);
         mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
     }
 }
复制代码

前面提到过mCachedViews如果空间不足,会根据索引将里面旧的移动到RecyclerViewPool中,这样此方法的就将除了ViewCacheExtension之外的缓存全部囊括了。

  1. 根据计算的值,多次调用fill方法填充子View。

显然,fill方法是新重点。该方法和ListView中的fillDown等类似,也是循环计算-填充-计算,我们直接看填充部分。填充部分调用了layoutChunk方法:该方法会首先调用LayoutState.next方法获取一个view;然后会addView,add过程中如果是detach过的,将会view重新attach到RecyclerView上,否则就是remove过了的,直接addView;最后调用measureChildWithMargins、layoutDecoratedWithMargins方法对子View进行测量、布局。layoutChunk方法代码如下:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    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();
    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);
        }
    }
    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.
    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();
}
复制代码

很显然,缓存部分的关键就是LayoutState.next方法了:

/**
  * 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;
}
复制代码

我们先略过mScrapList,暂时认为其为null,后面遇到再分析。所以这里调用了RecyclerView.getViewForPosition方法:

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

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

离真相又近了一步,tryGetViewHolderForPositionByDeadline方法里面会对各级缓存进行匹配,这里分段进行解释。

a. 如果有mChangedScrap,尝试进行匹配

 // 0) If there is a changed scrap, try to find from there
 if (mState.isPreLayout()) {
     holder = getChangedScrapViewForPosition(position);
     fromScrapOrHiddenOrCache = holder != null;
 }
复制代码

这里的isPreLayout()与mState.mRunPredictiveAnimations有直接关系,可以看成前者的值取决与后者,该值在dispatchLayoutStep1过程中被更新;当Item发生了更新时,scrapView方法会将ViewHolder保存到mChangedScrap中去。

b. 尝试从mAttachedScrap、mCachedViews中寻找匹配的ViewHolder。找到之后会对ViewHolder做一些检查,如果不满足条件,且dryRun为false(实际上就是false),会将ViewHolder清除掉并保存到mCachedViews中。在向mCachedViews中添加缓存时,如果超过了允许的上限(即mViewCacheMax),将会把旧的缓存移动到RecycledViewPool中。

 // 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;
         }
     }
 }
复制代码

c. 如果Adapter.hasStableIds()为true,会根据ItemId和ViewType在mAttachedScrap、mCachedViews中寻找ViewHolder。Adapter中该属性默认为false。

 // 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;
     }
 }
复制代码

d. 如果存在ViewCacheExtension,调用ViewCacheExtension.getViewForPositionAndType寻找ViewHolder

 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());
         }
     }
 }
复制代码

e. fallback到RecycledViewPool,看是否有可用的ViewHolder

 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);
         }
     }
 }
复制代码

f. 以上都不满足,最后调用Adapter.createViewHolder创建ViewHolder

 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");
     }
 }
复制代码

在获取到ViewHolder之后,如果需要bind,会调用tryBindViewHolderByDeadline方法,该方法中接着调用Adapter.bindViewHolder方法交给开发者完成绑定工作。

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);
}
复制代码

tryGetViewHolderForPositionByDeadline方法完成之后会一直返回到LinearLayoutManager.layoutChunk方法中,接着会根据ViewHolder的来源,该attach的attach,该addView的addView,最后measure并layout,一个子View的layout过程就完成了。

最后以一张流程图结束本节:

image.png

ListView与RecyclerView缓存机制对比

ListView与RecyclerView缓存机制原理大致相似:滑动过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异.(这只是缓存使用的其中一个场景,还有如刷新等)。原理图如下所示:

image.png

两者缓存机制的对比有以下几点不同:

  1. 层级不同

    RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。

image.png

image.png

ListView和RecyclerView缓存机制基本一致:

a. mActiveViews和mAttachedScrap功能相似,用来快速重用屏幕上可见的列表项,而不需要重新创建和绑定

b. mScrapViews和mCacheViews + mRecyclerPool功能相似,用来缓存离开屏幕的itemView,让即将进入屏幕的itemView复用

c. RecyclerView的优势在于:

- mCacheViews的使用,可以做到屏幕外的列表项在进入屏幕时也无须bindView快速重用

- mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下(如ViewPager+多个列表页,或者竖向列表的item中嵌套有横向列表等)有优势。
复制代码

客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

  1. 缓存不同
  • RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:View + ViewHolder
  • ListView缓存View,实际使用的时候需要手动将自定义的ViewHolder添加到View的tag中

缓存不同,二者在缓存的使用上也略有差异,具体来说:

  • RecyclerView中mCacheViews获取时,是通过匹配pos获取目标位置的换缓存的,这样做的好处是,当数据源不变的情况下,无须重新bindView;而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新调用了Adapter的getView方法,这就必定会导致我们的bind代码执行。

  • ListView中通过pos获取的是View;RecyclerView通过pos获取的是ViewHolder。

另外,RecyclerView更大的亮点在于提供了局部刷新的接口,这样可以避免调用许多无用的bindView。

ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享