美文网首页三千码友在身旁
view系列源码分析之三大常用控件之recycleview

view系列源码分析之三大常用控件之recycleview

作者: 暴走的小青春 | 来源:发表于2019-12-22 22:01 被阅读0次

    作为三大常用控件的recycleview和scrollview有着同样广泛的用途,但是在源码分析难度上却不是一个等量级的,可以说,recyclview的控件是我可能过最复杂的控件了,所以务必要带着问题去分析,不然完全没有分析下去的勇气,先说几个问题吧:
    1.recycleview有了layoutmanager为何setadapter后就把数据显示在屏幕上
    2.recycleview的缓存是如何进行的
    3.recycleview的更新流程是怎么样的
    4.recycleview的局部更新和传统更新的差别
    暂时先列这4点吧,因为recycleview实在是有太多东西要分析的了
    1.首先列举最常规的情况:我们在onCreate中初始化,LinearLayoutManager然后在setAdapter了,假设在setadapter时已经有数据了
    首先进入到LinearLayoutManager中

     public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
                boolean reverseLayout) {
            setOrientation(orientation);
            setReverseLayout(reverseLayout);
            setAutoMeasureEnabled(true);
        }
    

    很简单这里设置了自动measure为true
    然后在setLayoutManager中有个这个方法

    void updateViewCacheSize() {
                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--) {
                    recycleCachedViewAt(i);
                }
            }
    

    也就是最大缓存的view,默认是2,记住,等会又会调用到它,其他的方法无非就是重置的操作
    而在setAdapter方法里会间接调用

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

    这里由于没有重复调用adapter,所以也没有老数据,简单来说就是注册了观察者以及告诉recycleview,adapter被onAttached了
    其中这两个地方都调用了requestLayout,也就是说会调用onMeasure和onLayout方法,在回到onMeasure中

     @Override
        protected void onMeasure(int widthSpec, int heightSpec) {
            if (mLayout == null) {
                defaultOnMeasure(widthSpec, heightSpec);
                return;
            }
            if (mLayout.mAutoMeasure) {
                final int widthMode = MeasureSpec.getMode(widthSpec);
                final int heightMode = MeasureSpec.getMode(heightSpec);
                final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                        && heightMode == MeasureSpec.EXACTLY;
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                if (skipMeasure || mAdapter == null) {
                    return;
                }
    ....
    

    由于在xml中给recyclview的宽高是EXACTLY的,所以这里就return了
    并执行了onMeasure方法

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

    可以看到一开始确定了recyclview的宽高了,不了解onMeaure和requestLayout人可以看下关于onmeasure,onLayout, requestLayout ,invalidate你可能忽视的细节
    既然在onMeasure中没做啥,来分析下onLayout方法

     @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
            dispatchLayout();
            TraceCompat.endSection();
            mFirstLayoutComplete = true;
        }
    

    其中dispatchLayout方法是重点要分析的

    void dispatchLayout() {
            if (mAdapter == null) {
                Log.e(TAG, "No adapter attached; skipping layout");
                // leave the state in START
                return;
            }
            if (mLayout == null) {
                Log.e(TAG, "No layout manager attached; skipping layout");
                // leave the state in START
                return;
            }
            mState.mIsMeasuring = false;
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                    || mLayout.getHeight() != getHeight()) {
                // First 2 steps are done in onMeasure but looks like we have to run again due to
                // changed size.
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else {
                // always make sure we sync them (to ensure mode is exact)
                mLayout.setExactMeasureSpecsFrom(this);
            }
            dispatchLayoutStep3();
        }
    

    可以看到这里有三个重要的方法,先来看下dispatchLayoutStep1方法

    private void dispatchLayoutStep1() {
            mState.assertLayoutStep(State.STEP_START);
            fillRemainingScrollValues(mState);
            mState.mIsMeasuring = false;
            eatRequestLayout();
            mViewInfoStore.clear();
            onEnterLayoutOrScroll();
            processAdapterUpdatesAndSetAnimationFlags();
            saveFocusInfo();
            mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
            mItemsAddedOrRemoved = mItemsChanged = false;
            mState.mInPreLayout = mState.mRunPredictiveAnimations;
            mState.mItemCount = mAdapter.getItemCount();
            findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
    ...
    

    这里有一个关键点是processAdapterUpdatesAndSetAnimationFlags方法和
    第一个从名字上来看就是当adapter被局部更新时候所调用的方法,这个等会分析
    紧接着分析dispatchLayoutStep2方法

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

    重点在mLayout.onLayoutChildren方法
    首先找到他的

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

    这里会走到if条件里面去,目的就是给开始放置位置赋值,由于没有设置reverse属性,所以 mAnchorInfo.mLayoutFromEnd的boolean是false
    并且mAnchorInfo.mPosition的值是0

      anchorInfo.assignCoordinateFromPadding();
            anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    

    而后面会走到mAnchorInfo.mLayoutFromEnd为false的判断里,接调用fill方法,在此期间,会拿到recyclview的高度,然后赋值给mLayoutState.mAvailable属性

      private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
            mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
                    LayoutState.ITEM_DIRECTION_TAIL;
            mLayoutState.mCurrentPosition = itemPosition;
            mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
            mLayoutState.mOffset = offset;
            mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
        }
    

    来看下fill方法

    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;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
            LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
            while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
                layoutChunkResult.resetInternal();
                if (VERBOSE_TRACING) {
                    TraceCompat.beginSection("LLM LayoutChunk");
                }
                layoutChunk(recycler, state, layoutState, layoutChunkResult);
                if (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;
        }
    

    其中会走到while循环里去,调用layoutChunk(recycler, state, layoutState, layoutChunkResult);方法,此时在recycle里只有一个mViewCacheMax,其他的recycleviewpool和cach都没有创建呢

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

    在这方法里有两个关键点,第一个是获取到view第一个view,这里关键代码在缓存这块细说,主要记住在这方法里没有任何缓存的话会调用我们熟悉的onCreateViewHolder和onBindViewHolder方法, 关键点二就是被加到了recycleview里面了,然后会获取每个item的高度,从而把remainingSpace和layoutState.mAvailable重新赋值,当不满足条件的情况下,就退出循环了,而在fill结束以后 startOffset和endOffset就是所加view实际的高度了

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

    也就是说最基本的dispatchLayoutStep2被分析完了,在没有动画和任何缓存的情况下还是很简单的,至此第一个问题也就有答案了,主要就是添加显示在屏幕view,然后add进去,而dispatchLayoutStep3里面主要对动画的处理和恢复初始值,这里就不分析了,接下来分析比较绕的缓存这一块

    缓存

    缓存的view的获取主要在tryGetViewHolderForPositionByDeadline里

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

    首先我们来大概分析一下,这里在源码中也有注释,第一步是获取改变的废弃的view,前提是是在预布局阶段,那何时会有预布局呢,其实你如果单个view notify的话就会有预布局了,具体的后文会说,正常的显然不走此流程,第二种情况是按照位置获取废弃的view,这其实是当你滑动的时候,会将部分view放进去,具体的看后文,现在都是获取不到的,第三种是按照id获取废弃的view,这基本用不到可以跳过,第四种是根据ViewCacheExtension获取缓存的,当然源码中并不会给我们实现,显然意义不大,最后是根据getRecycledViewPool也就是相同的type类型来获取holder,当都获取不到的时候就调用createViewHolder方法来获取,当获取完以后会把此type放进scrapdata中,会间接调用getScrapDataForType方法

     private ScrapData getScrapDataForType(int viewType) {
                ScrapData scrapData = mScrap.get(viewType);
                if (scrapData == null) {
                    scrapData = new ScrapData();
                    mScrap.put(viewType, scrapData);
                }
                return scrapData;
            }
    

    而后会调用bindViewHolder从而调用onBindViewHolder方法

    public final void bindViewHolder(VH holder, int position) {
                holder.mPosition = position;
                if (hasStableIds()) {
                    holder.mItemId = getItemId(position);
                }
                holder.setFlags(ViewHolder.FLAG_BOUND,
                        ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
                TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
                onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
                holder.clearPayload();
                final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
                if (layoutParams instanceof RecyclerView.LayoutParams) {
                    ((LayoutParams) layoutParams).mInsetsDirty = true;
                }
                TraceCompat.endSection();
            }
    

    在分析出了缓存以后,我们来看下这些主要的缓存是如何触发的,我们轻轻滑动recycleview,很明显在其onTouchEvent中,然后会间接调用到
    scrollVerticallyBy方法

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

    从而调用scrollBy方法,间接的会调用fill方法,在fill方法中

    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                // TODO ugly bug fix. should not happen
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
    

    有这样一个分之,触发条件是比如在屏幕上的view是0,1,2,在把第三个view完全滑出来的时候,第0个view就会被回收了,从而调用了removeAndRecycleViewAt方法

     public void removeAndRecycleViewAt(int index, Recycler recycler) {
                final View view = getChildAt(index);
                // 注释一
                removeViewAt(index);
                // 注释二
                recycler.recycleView(view);
            }
    

    注释一主要是给adapter回掉的,回调的adapter的onViewDetachedFromWindow方法,注释二是添加此view到recycler里去的,着重看下注释二,会间接调用recycleViewHolderInternal方法

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

    可以看到由于第一次的cachedViewSize是0,而且mViewCacheMax的个数由于预加载的原因(recycleview在25以后做了优化,会在同一帧提前加载布局)这里本来是二的cache,变成了三,最后会走到里

      mCachedViews.add(targetCacheIndex, holder);
                        cached = true;
    

    可以说第一个view被划出去的时候放到了mCachedViews里面,现在展示的列表的view是1,2,3,当我们重新滑到第0个时,此时view就会从
    mCachedViews里取出来,并且从列表中移除掉此view

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

    这里主要走的是cacheSize的方法,也就是说从mCachedViews里恢复的view是可以直接add的,不用调用任何的回掉,也就是最高效的
    也就是recycleview的view都是先add在remove的
    此时我们把视图初始化,回到0,1,2的位置,我这里手动关闭了预加载,也就是说mViewCacheMax的是二,来分析下,回收池的回收情况
    这里滑到了2,3,4的位置,显然cache的size是二了,我们在往上滑
    这里走到了如下代码中

     if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                            recycleCachedViewAt(0);
                            cachedViewSize--;
                        }
    

    来看下recycleCachedViewAt的方法

     void recycleCachedViewAt(int cachedViewIndex) {
                if (DEBUG) {
                    Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
                }
                ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
                if (DEBUG) {
                    Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
                }
                addViewHolderToRecycledViewPool(viewHolder, true);
                mCachedViews.remove(cachedViewIndex);
            }
    

    这里我们看到会把第一个的cached从列表里移出去,同时把第一个的view放进了recycledViewPool中

       void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
                clearNestedRecyclerViewIfNotNested(holder);
                if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
                    holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
                    ViewCompat.setAccessibilityDelegate(holder.itemView, null);
                }
                if (dispatchRecycled) {
                    dispatchViewRecycled(holder);
                }
                holder.mOwnerRecyclerView = null;
                getRecycledViewPool().putRecycledView(holder);
            }
    

    这个dispatchViewRecycled是放到缓存池的标志

       public void putRecycledView(ViewHolder scrap) {
                final int viewType = scrap.getItemViewType();
                final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
                if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                    return;
                }
                if (DEBUG && scrapHeap.contains(scrap)) {
                    throw new IllegalArgumentException("this scrap item already exists");
                }
                scrap.resetInternal();
                scrapHeap.add(scrap);
            }
    

    这里我们看到其实是每一个type最大有5个scrap,因为recycleview的缓存池是和type相关的,并不是和位置相关的,继续看下recycleViewHolderInternal方法会发现,此时它又把第二个位置给放到缓存队列里了,所以现在的结果是
    第0个在缓存池里,第一和第二个在cache里,第三,第四,第五个在列表的显示,继续滑动,会发现从第六个开始,就不再调用onCreateViewHolder方法了,但是由于是缓存池,缓存的是type,所以
    onBindViewHolder方法还是会调用的原因如下

    tryGetViewHolderForPositionByDeadline方法
    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);
                }
    

    由于是缓存池会让此holder需要更新,所以会走到tryBindViewHolderByDeadline从而调用onBindViewHolder方法,而从cache恢复的却不会
    现在出一个小问题:
    recycleview的数量有20个都是同一个类型的type,界面上有3个itme,一共会调用几次onCreateViewHolder呢?(这里不考虑25+的预加载的情况)
    当然第二个的答案也就出来了,第三和第四个的情况下篇分析把,不然篇幅太长了

    相关文章

      网友评论

        本文标题:view系列源码分析之三大常用控件之recycleview

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