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

2023-10-26

作者: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

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Recyclerview源码深入探索:Adapter的增删改再也不迷路 的相关文章

随机推荐

  • 华为OD机试 - 组成最大数(Java)

    题目描述 小组中每位都有一张卡片 卡片上是6位内的正整数 将卡片连起来可以组成多种数字 计算组成的最大数字 输入描述 号分割的多个正整数字符串 不需要考虑非数字异常情况 小组最多25个人 输出描述 最大的数字字符串 用例 输入 22 221
  • Pycharm 交互式Console BUG问题解决 - OSError: [Errno 22] Invalid argument: ‘D:\pyCHram\<input>‘

    引言 Pycharm python console是一个很好用的交互式编程栏 但是在跑一些深度学习模型的时候 偶尔会发生类似于这种错误的报错 导致无法在交互式的Python console模式下运行测试代码 Pycharm 问题解决 OSE
  • 攻防世界NewsCenter思路

    打开环境后发现是这样一个页面 考虑到xss或者sql注入 尝试了alert无果 便试试sql注入 随便输入aaa后使用burp抓包 将这些保存在1 txt里 用sqlmap抓包 命令 sqlmap py r 1 txt dbs 这里看到一个
  • U-Boot相关命令开发板烧写问题及解决方案

    前言 最近在学习u boot命令在开发板的烧写 在进行该实验的过程中 出现了很多问题和错误 在这里我根据自己的开发历程 将我出现的几大问题进行了汇总 并附有相关解决办法 这些解决方案都经过我亲自验证有效 希望能让大家在开发过程中有所启发 问
  • C++实现复化梯形公式求积分算法

    1 算法原理简介 步1 将积分区间2n等分 步2 调用复化梯形公式 2 应用实例 取 n 10 利用复化梯形公式计算积分 3 程序代码 include
  • BLDC无刷直流电机驱动电路-硬石电子

    1 BLDC无刷直流电机驱动电路 因为BLDC是三相完全一样的驱动电路 下图为其中一相电路图 其他两相完全一样 主要元器件 高速光耦 TLP715 MOS管驱动IC IR2110S MOS IRF540NS D7和C13为自举电路 2 霍尔
  • ubuntu18.04安装之后没有网络,不显示网络图标

    新安装的ubuntu18 04 06安装完成后插着网线 但是没有有线网 桌面上不显示网络图标 原因是因为ubuntu系统安装时自带的网卡驱动不兼容导致的 下面来讲解解决方法 首先 先试用手机连接线 将手机连接到电脑usb口 使用手机上的US
  • matlab中hash和map的用法总结

    若要在matlab中使用hash 有两种方式 1 采用matlab官方给出的结构类型map containers Map http cn mathworks com help matlab map containers html 2 调用J
  • 到底要不要孩子学习机器人编程

    人工智能发展迅猛 很多技术已经成熟应用到我们生活场景中 如果再不从小让孩子学习机器人编程教育 掌握更多编程语言 那未来就out啦 格物斯坦小坦克可以告诉你关于机器人编程要不要学的答案 教育部也将启动中小学生信息素养测评 并推动在中小学阶段设
  • uni-app利用chooseImage方法封装一个图片选择组件

    效果如图 可以预览 长按可删除 可以设置最多上传数量 这里封装的组件有个MaxNumber number类型 用的时候在父组件传就行了 这里默认给的8 废话不多说直接上代码 封装好了之后我们用的时候只需要引入直接用就行
  • 从etcd看Raft协议

    首先 什么是etcd 看官方的定义 A highly available key value store for shared configuration and service discovery 翻译过来就是 用于配置共享和服务发现的K
  • 动态规划(五)

    01背包问题 01 Knapsack problem 有10件货物要从甲地运送到乙地 每件货物的重量 单位 吨 和利润 单位 元 如下表所示 由于只有一辆最大载重为30t的货车能用来运送货物 所以只能选择部分货物配送 要求确定运送哪些货物
  • 如何快速画AltiumDesigner封装——用Ultralibrarian生成库文件---官网最新打开方式

    如何用Ultralibrarian生成库文件 官网最新打开方式 步骤1 下载元器件 步骤2 AltiumDesigner生成库文件 Ultralibrarian软件比较常用的生成库文件的软件 网上对于它的介绍大多还停留在软件使用层面上 但官
  • 区块链的工作流程

    工作流程 通过前两篇文章 相信大家对区块链有了基本的认识 区块链系统有很多种 第一个应用区块链的软件就是比特币 事实上区块链就是比特币带出来的 到现在为止 已经出现很多基于区块链的系统了 比如超级账本 以太坊等 每一类系统都有自己的特点 无
  • 雪花算法生成ID

    雪花算法生成ID Snowflake 雪花算法是由Twitter开源的分布式ID生成算法 以划分命名空间的方式将64 bit位分割成多个部分 每个部分代表不同的含义 而Java中64bit的整数是Long类型 所以在Java中 SnowFl
  • [网络安全]sqli-labs Less-2 解题详析

    网络安全 Less 2 GET Error based Intiger based 基于错误的GET整型注入 判断注入类型 判断注入点个数 查库名 查表名 查users表的列名 查字段 注意 总结 往期回顾 网络安全 sqli labs L
  • TensorRT简介

    一 什么是TensorRT 一般的深度学习项目 训练时为了加快速度 会使用多 GPU 分布式训练 但在部署推理时 为了降低成本 往往使用单个 GPU 机器甚至嵌入式平台 比如 NVIDIA Jetson 进行部署 部署端也要有与训练时相同的
  • 【设计模式】工厂方法模式(C#)

    设计模式 工厂方法模式 1 概述 针对简单工厂中的缺点 使用工厂方法模式就可以完美的解决 完全遵循开闭原则 定义一个用于创建对象的接口 让子类决定实例化哪个产品类对象 工厂方法使一个产品类的实例化延迟到其工厂的子类 工厂方法模式的主要角色
  • 解决gensim fasttext官方案例报错TypeError: Either one of corpus_file or corpus_iterable value must be provide

    完整报错为 TypeError Either one of corpus file or corpus iterable value must be provided 解决方法 将官方案例中传递参数时指定的sentences 删除即可 比如
  • Recyclerview源码深入探索:Adapter的增删改再也不迷路

    作者 maxcion 看到标题说的是三级缓存 有的地方说是四级缓存 请你不要迷惑 到底是三还是四 这就像图片加载这个场景有人说是三级缓存有人说是二级缓存 说三级缓存是把通过网络请求图片这个环节也认为是一层缓存 你认为这个环节应该不应该属于缓