美文网首页
Recyclerview源码深入探索:Adapter的增删改再也

Recyclerview源码深入探索:Adapter的增删改再也

作者: 艾瑞败类 | 来源:发表于2023-05-04 13:31 被阅读0次

    作者:maxcion

    看到标题说的是三级缓存,有的地方说是四级缓存,请你不要迷惑,到底是三还是四,这就像图片加载这个场景有人说是三级缓存有人说是二级缓存,说三级缓存是把通过网络请求图片这个环节也认为是一层缓存,你认为这个环节应该不应该属于缓存呢?所以到底是三还是四不重要,因为逻辑是固定的.

    其实如果要比较RecyclerviewScrollView这两个哪个控件使用起来更简单,那必然是ScrollView,而且上手难度完全不是一个量级的.那为什么ScrollView的出镜率完全比不上Recyclerview呢?这就要归功于Recyclerview缓存设计和他的可扩展性了.

    Recyclerview源码分析:二、滑动时如何布局 中有提到即使你有再多的childView需要展示,但是Recyclerview只会创造一定数量的childView,那我们有以下几个问题需要探索一下:

    前提:垂直布局的Recyclerview高度为100dp,所有的child高度为10dp,初始化时填充了10个child,这时候手指向上滑动了30dp,然后再向下滑30dp回到默认的位置

    1. 在向上滑动30dp的这个环节中,划出屏幕中的child0child1child2这三个child是否进入到同一个缓存中了?
    2. 在向上滑动的30dp的这个环境中,从屏幕底部滑进来的child10child11child12这三个child都是全新创建的ViewHolder吗?如果不是哪些是用的缓存,哪些是全新创建的?
    3. 在向下滑动30dp回到默认位置的这个环节中,滑进来的的child2child1child0这三个child中哪几个不需要走数据绑定逻辑,哪些需要走数据绑定逻辑?

    从上面的三个问题可以看出来都是和缓存相关的,那现在要谈缓存,应该先讨论把数据存到缓存里面还是先讨论从缓存中取数据呢?如果标题所说的三级缓存,如果只有一层缓存,先讨论存还是先讨论取都不会有太大的区别,但是这里有三层,如果我们不能先明白三层的数据怎么来的,直接讨论怎么从三层缓存中取数据,然后再接触的都是自己从未接触过的API会非常打击继续阅读源码的信心,所以这里选择先看如果存数据.

    缓存从哪来

    缓存从哪来?先不从代码分析,正常情况下,我们有哪些场景ViewHolder会从屏幕中"消失"呢?

    • ViewHolder被划出屏幕
    • 调用Adapter.notifyItemRemove()

    我们先以划出屏幕这个场景来讲解,在 Recyclerview源码分析:二、滑动时如何布局 中刚好讲了滑动时如何布局的,第二篇中也能看出Recyclerview在处理滑动时主要就是为了处理嵌套滑动和过滤滑动的,和布局相关的逻辑全部都在LayoutManager.fill()

    LLM.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);
        }
        //计算总共有多少空间可以用来摆放child,remainingSpace的值可谓正可为负
        //为负数的场景:
            //在执行fill()之前会执行,会执行updateLayoutState()其中有这段代码
            // mLayoutState.mAvailable = requiredSpace;
            //        if (canUseExistingSpace) {
            //            mLayoutState.mAvailable -= scrollingOffset;
            //        }
            //首先会把手指滑动的距离赋值给mAvailable,然后再减去scrollingOffset,所以
            //当scrollingOffset>mAvailable时就会为负数,也就代表当前没有空白的位置需要填充child
            //当scrollingOffset代表当前需要滑动多少距离可以把最后一个child完全展示,一个高度为100dp的rv
            //child高度都是15dp,那么屏幕上会有7个child,并且第七个child没有完全展示,需要滑动5dp才能让
            //第七个child完全展示,那么此时如果手指滑动的距离是2dp,那么第七个child还有3dp的部分在屏幕外面
            //这时候remainingSpace就是负数,代表不需要填充新的child
        //为正数的场景:也就是最后一个child未展示的部分高度<手指滑动的距离这时候肯定就需要填充新的child
    
        //这样代码很好理解的吧,如果当前最后一个child没完全展示,但是滑动距离又小于未展示部分的高度
        //这时候肯定不需要填充新的child
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        //一个用来保存每次布局一个child的结果类,比如一个child消费了多少空间
        //是否应该真实的计算这个child消费的空间(预布局的时候有些child虽然消费了空间,
        // 但是不应该不参与真正的空间剩余空间的计算)
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //只要还有空间和item就进行布局layoutchunk
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            //重置上一次布局child的结果
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            //这里是真正layout child的逻辑
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            //layoutState.mLayoutDirection的值是 1或者-1 所以这里是 乘法
            //如果是从顶部往底部填充,当前填充的是第三个child 且每个高度是10dp,那么layoutState.mOffset的值
            //就是上次填充时的偏移量 + 这次填充child的高度
            //如果是从底部往顶部填充,那就是次填充时的偏移量 - 这次填充child的高度
            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
             */
            //判断是否要真正的消费当前child参与布局所消费的高度
            //从判断条件中可以看到预布局和这个有关,不过预布局等后面几章会详细说的
            //这里就是同步目前还剩多少空间可以用来布局
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                //这里是重点,下面的if判断里面会用到mAvailable的值进行计算
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }
    
            //在这个判断内执行滑出去的child进行回收
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                //我先看下layoutState.mAvailable 什么时候会小于0
                //在执行fill()之前会执行,会执行updateLayoutState()其中有这段代码
                // mLayoutState.mAvailable = requiredSpace;
                //        if (canUseExistingSpace) {
                //            mLayoutState.mAvailable -= scrollingOffset;
                //        }
                //requiredSpace是手指滑动的距离,所以上面代码的执行后
                //layoutstate.mAvailable = 手指滑动距离 - scrollingOffset;
                //在这个函数的上面又对mAvailable进行了赋值layoutState.mAvailable -= layoutChunkResult.mConsumed
                //所以现在layoutstate.mAvailable = 手指滑动距离 - scrollingOffset - layoutChunkResult.mConsumed
                //手指滑动距离 - scrollingOffset 这个计算是什么呢?它属于计算在布局时的有效手指滑动,比如说
                //最后一个child有5dp的内容在屏幕外没显示出来,这时候向上滑动了6dp,那实际上布局需要关注的填充高度为6dp-1dp(有效滑动)
                //所以也就能看出来来layoutstate.mAvailable<0 就是指有效滑动距离,小于填充child使用的高度
                if (layoutState.mAvailable < 0) {
                    //上面有计算 layoutstate.mAvailable = 手指滑动距离 - scrollingOffset - layoutChunkResult.mConsumed
                    //那经过这个计算之后layoutState.mScrollingOffset  = 手指滑动距离 - scrollingOffset - layoutChunkResult.mConsumed + layoutChunkResult.mConsumed + scrollingOffset
                    //也就是   手指滑动距离
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                //执行回收相关逻辑
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }
    

    这里总结一下就是通过计算手指滑动距离 - 最后一个child没完全展示的高度 > 0,就代表需要填充一个新的child,有新的child进入屏幕,就有旧的child移除屏幕,那么被移出去的child就需要被回收.回收相关的逻辑控制在recycleByLayoutState()

    LLM.recycleByLayoutState()

    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        int scrollingOffset = layoutState.mScrollingOffset;
        int noRecycleSpace = layoutState.mNoRecycleSpace;
        //这里我们还是以垂直布局手指向上滑动场景为例
        //因为手指向上滑动,就需要在底部填充child,所以layoutState.mLayoutDirection != LayoutState.LAYOUT_START
        //就会走到else逻辑中
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
        } else {
            //①
            recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
        }
    }
    
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
            int noRecycleSpace) {
        if (scrollingOffset < 0) {
            if (DEBUG) {
                Log.d(TAG, "Called recycle from start with a negative value. This might happen"
                        + " during layout changes but may be sign of a bug");
            }
            return;
        }
        // ignore padding, ViewGroup may not clip children.
        //在前面计算的结果中scrollingOffset==手指滑动的距离
        //所以知道第一个child的bottom小于这个值的都会在这次滑动中划出屏幕
        //那他们自然就要被回收
        final int limit = scrollingOffset - noRecycleSpace;
        final int childCount = getChildCount();
        if (mShouldReverseLayout) {
            for (int i = childCount - 1; i >= 0; i--) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, childCount - 1, i);
                    return;
                }
            }
        } else {
            //从顶部第一个child开始找,找到第一个child的bottom>scrollingOffset(5dp)的child
            //那么这个child之前的所有child在这次滑动中都会划出屏幕
            //所以要把他们都回收掉
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
    }
    

    这里的回收逻辑就是判断所有child只要是botton < 有效滑动 都会被回收,这里是进行判断要回收哪些child,回收逻辑在recycleChildren()

    recycleChildren()

    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
        if (startIndex == endIndex) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
        }
        if (endIndex > startIndex) {
            for (int i = endIndex - 1; i >= startIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        } else {
            for (int i = startIndex; i > endIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        }
    }
    

    这里的逻辑就是通过遍历child的起始index到给定的end index,然后分别执行回收逻辑,也就是removeAndRecycleViewAt()

    Recycerlview.LayoutManager.removeAndRecycleViewAt()

    public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
        final View view = getChildAt(index);
        removeViewAt(index);
        recycler.recycleView(view);
    }
    

    这里是先把要回收的View先从RV中移除,然后再走recycler.recycleView()逻辑进行回收

    Recycler.recyclerView()

    public void recycleView(@NonNull View view) {
        // This public recycle method tries to make view recycle-able since layout manager
        // intended to recycle this view (e.g. even if it is in scrap or change cache)
        ViewHolder holder = getChildViewHolderInt(view);
        if (holder.isTmpDetached()) {
            removeDetachedView(view, false);
        }
        if (holder.isScrap()) {
            holder.unScrap();
        } else if (holder.wasReturnedFromScrap()) {
            holder.clearReturnedFromScrapFlag();
        }
        //①这里将回收逻辑交给了recycleViewHolderInternal()
        recycleViewHolderInternal(holder);
        if (mItemAnimator != null && !holder.isRecyclable()) {
            mItemAnimator.endAnimation(holder);
        }
    }
    
    void recycleViewHolderInternal(ViewHolder holder) {
    
        ...
    
        final boolean transientStatePreventsRecycling = holder
                .doesTransientStatePreventRecycling();
        @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null
                && transientStatePreventsRecycling
                && mAdapter.onFailedToRecycleView(holder);
        boolean cached = false;
        boolean recycled = false;
        if (sDebugAssertionsEnabled && mCachedViews.contains(holder)) {
            throw new IllegalArgumentException("cached view received recycle internal? "
                    + holder + exceptionLabel());
        }
        if (forceRecycle || holder.isRecyclable()) {
            //这是第一层缓存mViewCacheMax,代表这第一层缓存最多可以缓存多少View
            //这就是第一层缓存叫做mCachedViews,我们是从滑动场景跟踪代码进来的
            //在这里介绍一下这个缓存CachedViews:是用来缓存被划出屏幕的View
            //这层缓存大小是通过mViewCacheMax这个参数来控制的,默认值是2
            //可以通过RV.setItemViewCacheSize()来修改这层缓存的大小
            //如果我们把这层缓存设置层无限大,那我们就相当于实现了一个懒加载的Scrollview
    
            //再说一下这层缓存的特性:
            //1\. 从屏幕中滑出去的View会被缓存在这层缓存中
            //2\. 从这层缓存中复用的child,是不需要经过任何处理直接使用的
                //也就代表着有些缓存层,从缓存中取出view后还需要做一些处理,比如数据的重新绑定
            //这层缓存是非常有必要的,而且他是一级缓存,再说一下他存在的必要性,现在RV高度为100dp,每个child高20dp,
            //这是屏幕有5个child,这时候手指向上滑动40dp,那么child0,child1都被移除了屏幕,并且进入了这层缓存中
            //这时候用户手指又向下滑动了40dp,child1,child0分别又进入了屏幕,但是这次child1和child0不是新创建的
            //而是从缓存中直接取的,取出来直接add到recyclerview中,这层缓存主要是针对快速上下滑动场景设计的
            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最大缓存数量时,就回收mCachedViews中的第一个viewholder
                //因为我们这层一级缓存的大小是有限的,如果设置成无限大,那就变成一个懒加载的ScrollView
                //虽然懒了,但是并不能节省内存,当数量多了之后内存直接爆炸,所以当有新的child进入当前缓存层时,
                //如果缓存满了,就会把最老的view移入其他缓存层,和LRU缓存一样
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    //①这里就是把过期的一级缓存移入其他缓存层的逻辑
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
    
                ...
                //如果超出了缓存数量就把过期的view移入其他缓存层,
                //然后把新的view添加到当前缓存层
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        } else {
            ...
    }
    
    void recycleCachedViewAt(int cachedViewIndex) {
        if (sVerboseLoggingEnabled) {
            Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
        }
        ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
        if (sVerboseLoggingEnabled) {
            Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
        }
        //这里就是把view存到其他缓存层的逻辑
        addViewHolderToRecycledViewPool(viewHolder, true);
        //把当前这个过期的child从一级缓存层中删除
        mCachedViews.remove(cachedViewIndex);
    }
    
    void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
        clearNestedRecyclerViewIfNotNested(holder);
        View itemView = holder.itemView;
        //这个判断应该和辅助功能相关,不用看
        if (mAccessibilityDelegate != null) {
            AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate();
            AccessibilityDelegateCompat originalDelegate = null;
            if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) {
                originalDelegate =
                        ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate)
                                .getAndRemoveOriginalDelegateForItem(itemView);
            }
            // Set the a11y delegate back to whatever the original delegate was.
            ViewCompat.setAccessibilityDelegate(itemView, originalDelegate);
        }
        //这就是回调设置的回收监听
        if (dispatchRecycled) {
            dispatchViewRecycled(holder);
        }
        //这里可以看到把holder对象的adapter和RV进行了解绑
        //既然解绑了,那下次从缓存中取出这些缓存的时候肯定需要重新绑定
        holder.mBindingAdapter = null;
        holder.mOwnerRecyclerView = null;
        //①
        getRecycledViewPool().putRecycledView(holder);
    }
    

    从上面的代码可以看出,一级缓存中的过期View会被移入其他缓存层,真正的移入其他缓存层逻辑被交给了RecycledViewPoolputRecycledView().

    RecycledViewPool.putRecycledView()

    public void putRecycledView(ViewHolder scrap) {
        //首先获取当前要被回收View的 ViewType,因为RV有多布局这个概念
        //这一层缓存是以ViewType为单元进行缓存的.这里就是我们的第三级缓存的\
        //是的是第三级缓存不是第二级缓存,第二级的缓存只有在取的时候才会被涉及,
        //所以在存的时候只涉及一级缓存和三级缓存,我没有胡扯,后面讲取缓存的时候
        //会讨论第二级缓存的.
    
        //先总体概括一下第三级缓存,不需要完全理解,只需要有这个概念就好了,后面会通过代码证实的
        //因为第三季缓存是以ViewType为单元的缓存,所以会针对各种不同的ViewType进行缓存
        //我们要缓存同一种类型的View,我们首先想到的数据结构肯定是List,所以每个ViewType对应一个List
        //现在我们有很多种ViewType,那就对应很多List,那我们怎么映射一个ViewType和一个List的关系呢?
        //最简单的方案肯定是用Map.对应的结构就是Map<ViewType,List<View>>
        final int viewType = scrap.getItemViewType();
        //①这一步就是 要取对应类型缓存的那个List
        final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
        if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
            PoolingContainer.callPoolingContainerOnRelease(scrap.itemView);
            return;
        }
        if (sDebugAssertionsEnabled && scrapHeap.contains(scrap)) {
            throw new IllegalArgumentException("this scrap item already exists");
        }
        //这里会把当前ViewHolder的所有信息进行充值
        scrap.resetInternal();
        scrapHeap.add(scrap);
    }
    
    //这里的逻辑是从map中通过ViewType找到对应的缓存List
    //这里的ScrapData是对List<View>一种包装
    private ScrapData getScrapDataForType(int viewType) {
        ScrapData scrapData = mScrap.get(viewType);
        if (scrapData == null) {
            scrapData = new ScrapData();
            mScrap.put(viewType, scrapData);
        }
        return scrapData;
    }
    
    //这里可以看出ScrapData 只是对List<BiewHolder>进行了包装,
    //并且添加了一个mMaxScrap属性,进行控制缓存个数
    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    

    总结一下就是,滑动的时候,被划出屏幕的child首先会进入一级缓存CachedViews中,因为一级缓存的设计和LRU一致,所以一级缓存中过期的child会被移入第二级和第三级缓存中,但是第二级缓存只在取的时候被用到,所以第一级缓存中过期的child会被移入第三层缓存RecycledViewPool中,而第三层缓存是针对各种ViewType来缓存的,所以他的缓存结构是Map<ViewTye,List<ViewHolder>>.

    缓存要去哪

    Recyclerview源码分析:一、静态时如何布局的

    Recyclerview源码分析:二、滑动时布局是如何填充的

    两篇讨论如何填充和布局的时候,在layoutChunk()执行layoutState.next(recycler)当时我说暂时不管直接认为他是new了一个View,从他的入参也能看出来肯定是从缓存中取的,因为我们把View存在recycler中.

    LLM.LayoutState.next()

    View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextViewFromScrapList();
        }
        //这里直接从Recycler中取了
        final View view = recycler.getViewForPosition(mCurrentPosition);
        //mItemDirection有两个值 1/-1,如果是向填充child,取值就是1,mCurrentPosition就会+1
        //反之就是-1
        mCurrentPosition += mItemDirection;
        return view;
    }
    
    //Recyclerview.Recycler.java
    
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    
    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
            //这里是通过id在各个缓存中找child,为什么要有这个逻辑>能不能去掉?
            //我们以无限循环的Banner场景来谈,一个无限循环的banner,一共有3个轮播图
            //当我们要展示child3的时候我们期望展示child0,但是上面的寻找逻辑
            //都是通过position验证的,所以在展会position3的时候child0是不符合标准的
            //那我们就可以在这里通过id在position3的位置展示child0了
            if (mAdapter.hasStableIds()) {
                //这里面的逻辑我就不带着看了,里面的逻辑和上面几乎一致,只是将上面判断position的位置
                //变成id判断
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                        type, dryRun);
                if (holder != null) {
                    // update position
                    holder.mPosition = offsetPosition;
                    fromScrapOrHiddenOrCache = true;
                }
            }
            //这里就是我们的第二级缓存了,第二级缓存是交给开发者实现的
            //通过Recyclerview.setViewCacheExtension(ViewCacheExtension extension) 
            //ViewCacheExtension是一个接口 View getViewForPositionAndType(@NonNull Recycler recycler, int position,int type)
            //开发者在这里面返回一个View就代表取到缓存了
            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 (sVerboseLoggingEnabled) {
                    Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                            + position + ") fetching from shared pool");
                }
                //上面没取到数据,就从第三级缓存RecycledViewPool中取数据
                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;
                }
                //就调用Adapter.createViewHolder()创建一个
                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 (sVerboseLoggingEnabled) {
                    Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                }
            }
        }
    
        ...
    
        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 (sDebugAssertionsEnabled && 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);
        }
    
        ...
        return holder;
    }
    

    从上面的逻辑可以看到,从第一和第二级缓存都没找到,就会到第三缓存中找,如果仍然没有找到,就会调用Adapter来新建一个Viewholder,但是第三级缓存的逻辑都在Recycler中,我们现在看看第三级缓存是如何取的.

    Recyclerview.Recycler.getRecycledView

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

    其实这样一看也很简单,就是通过ViewType从Map中找到对应的缓存List<ViewHolder>,然后遍历List

    相关文章

      网友评论

          本文标题:Recyclerview源码深入探索:Adapter的增删改再也

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