RecyclerView复用与回收源码分析

我们知道RecyclerView 在滑动到一定数量之后回进行视图复用,那RecyclerView 是如何做到复用和回收的呢?首先我们在分析源码的时候得清楚我们要以什么目的去分析源码,像RecyclerView 复用的话,既然它是在滑动的时候复用的呢,那我们猜想它是不是就是在走滑动事件的时候去做的复用呢?带着这个疑问我们找到RecyclerView 源码里面

从生面的描述中,首先我们需要找到滑动事件然后继续找复用与回收的方法

寻找回收与复用的方法

首先从RecyclerView源码中找到滑动事件的地方:

@Override
    public boolean onTouchEvent(MotionEvent e) {
      //....省略代码....
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
              //....省略代码.... 
            break;

            case MotionEvent.ACTION_MOVE: {
 				//....省略代码.... 
 				
                    if (scrollByInternal( //这里是回收复用的关键
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {
              //....省略代码.... 
           
            break;

            case MotionEvent.ACTION_CANCEL: {
                cancelTouch();
            } 
            break;
        }
        //....省略代码.... 
        return true;
    }

从代码中我们找到scrollByInternal这个方法,然后找到scrollStep:

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        //代码省略.....
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

    	//代码省略.....
    }

我们看到它调用了两个方法一个是垂直方向滑动的方法,一个是水平方向滑动的方法,而这两个方法是LayoutManager里面的,我们随便进去一个scrollVerticallyBy

    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
            return 0;
        }

发现它什么都没实现,因为我们知道LayoutManager它是一个抽象类,我们进到它的实现类LinearLayoutManager里面看下:

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

发现它调用了scrollBy

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
            //省略代码...
           
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
     
     //省略代码...
        return scrolled;
    }

我们知道了一个叫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 (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            //复用方法
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
           //省略代码....
        return start - layoutState.mAvailable;
    }

从源码中可以看到两个方法:recycleByLayoutStatelayoutChunk;这两个就是回收与复用视图的两个方法;至此,回收与复用的方法就找到了,接下来,就分析他们是如何复用与回收的,我们先来看复用;

复用源码分析

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
   //代码省略...

我们看到它调用了layoutState.next

  View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

可以看到它调用了getViewForPosition,我们点进去之后,它最终会调用tryGetViewHolderForPositionByDeadline方法:

 ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
     
            if (holder == null) {
            //从mAttachedScrap与mCachedViews 缓存中拿
                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.
                    //从mViewCacheExtension缓存中拿
                    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");
                    }
                    //从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;
                    }
                    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");
                    }
                }
            }

            return holder;
        }

从源码中,我们可以看到RecyclerView 在复用的做了多级缓存,然后在获取缓存中的ViewHolder视图的时候,它会一级一级的去找,从源码中我们姑且将缓存分为4级别,分别是:

  1. mChangedScrap与mAttachedScrap
  2. mCachedViews 默认最大缓存数量为2
  3. mViewCacheExtension
  4. RecycledViewPool

首先它会一级一级的去找,需要注意的是在RecycledViewPool里面拿的时候是这样拿的:

  @Nullable
        public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                //先进先出的方式拿的
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }

看到没,它在拿RecycledViewPool里面的视图的时候,采用的是先进先出的顺序拿的,也就是说它拿的是这个list集合里面的最后一个元素,而在后面回收mCachedViews的时候,采用的又是先进后出的方式, 接下来我们看下回收的时候是怎样的;

回收源码分析

回到回收的代码,如下所示:

  private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }

发现它会根据布局的方向调用recycleViewsFromEndrecycleViewsFromStart方法,我们看下recycleViewsFromStart方法:

  private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
       
        final int limit = dt;
        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 {
            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;
                }
            }
        }
    }

发现有个recycleChildren的方法:

   private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
       //代码省略...
        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);
            }
        }
    }

然后又调了removeAndRecycleViewAt:

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

发现它调用了recycler.recycleView(view)

  public void recycleView(@NonNull View view) {
			//代码省略...
            recycleViewHolderInternal(holder);
        }

recycleViewHolderInternal这个方法才是真正回收的方法:

void recycleViewHolderInternal(ViewHolder holder) {
			
			//代码省略
			
			//判断是否需要回收
            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();
					//判断缓存的大小是否是缓存的最大值,其实最大值为2, 如果大于等于最大值的话,就将第一个元素回收,保证这个缓存里面的视图是最新的
                    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;
                }
            } 
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }

从上面的源码中可以看到如果mCachedViews这个缓存的数据慢了的话,它就会调用recycleCachedViewAt方法将mCachedViews里面的一个元素移除掉,我们来看下recycleCachedViewAt:

   void recycleCachedViewAt(int cachedViewIndex) {
            addViewHolderToRecycledViewPool(viewHolder, true);
            mCachedViews.remove(cachedViewIndex);
        }

我们可以看到在这个方法里面,它显示将视图viewHolder添加到了RecycledViewPool里面,而这个RecycledViewPool相当于一个视图缓存池,然后将mCachedViews里面的一个元素移除了,需要注意的是其实mCachedViews缓存的最大值其实就是2:
图1
这里值的注意的是在回收mCachedViews的时候,采用的是先进后出的方式来回收的,而在上面说的复用的时候,RecycledViewPool采用的是先进先出的方式,这是为啥呢,因为在复用的时候我们通过viewType获取到的ViewHolder是一样的,而且在通过scrapHeap.remove(scrapHeap.size() - 1)的方式,每次取的时候也方便一些,而在回收mCachedViews的时候由于要回收的每个ViewHolder可能是不一样的,所以要保证mCachedViews里面的视图是最新的,所以采用了先进后出的方式;接着我们来看下,这个RecycledViewPool是怎么将视图缓存起来的:

   void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            clearNestedRecyclerViewIfNotNested(holder);
    		//代码省略....
            holder.mOwnerRecyclerView = null;
            getRecycledViewPool().putRecycledView(holder);
        }

可以看到,它调用了getRecycledViewPool().putRecycledView(holder)我们来看下putRecycledView这个方法,看它是怎么缓存的:

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

可以看到,它调用了getScrapDataForType这个方法,然后得到了一个数据,然后将视图ViewHolder添加到了这个数据里面, 至此寻找滑动事件来分析RecyclerView回收与复用的源码就完了,其实我们还可以根据onLayout来分析,这里我就不一一赘述了,我就将onLayout分析的方法调用流程贴出来吧:
在这里插入图片描述

总结

Recycler复用与回收,其实就是根据多级缓存,回收的时候将ViewHolder放到了这些缓存里面,复用的时候一级一级的从这些缓存里面去拿!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RecyclerView复用错乱问题是因为它的使用方式不正确所导致的。复用错乱是指在滑动RecyclerView时,列表项的数据与视图不匹配,在屏幕上显示的数据混乱或重复。 造成这个问题的常见原因有以下几点: 1. 数据源更新不及时:当数据源发生变化时,需要及时更新适配器的数据集。如果没有正确地处理数据源的变化,就会导致RecyclerView中的视图与数据不匹配。 2. 没有正确实现ViewHolder模式:RecyclerView通过重用ViewHolder来提高性能。如果没有正确实现ViewHolder模式,就会导致视图复用时数据混乱。正确的做法是在ViewHolder中设置视图的数据,而不是在绑定ViewHolder时修改视图的数据。 3. 使用了错误的布局管理器:布局管理器决定了RecyclerView中列表项的排列方式。如果使用了错误或不合适的布局管理器,就会导致列表项的位置混乱或错乱。 解决RecyclerView复用错乱问题的方法有以下几点: 1. 在数据源发生变化时,及时更新适配器的数据集,并调用适配器的notifyDataSetChanged方法,通知RecyclerView进行视图更新。 2. 确保正确实现ViewHolder模式,在ViewHolder中正确地设置视图的数据。 3. 注意选择合适的布局管理器,确保列表项的位置正确排列。 4. 可以使用一些开源库或框架,如DiffUtil或DataBinding等,来简化数据源更新和视图更新的过程。 总结起来,解决RecyclerView复用错乱问题的关键是正确处理数据源的更新、正确实现ViewHolder模式、选择合适的布局管理器,并注意使用相关的开源库或框架来简化开发过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值