• RecycleView滑动、缓存、复用源码分析与优化


    一、RecycleView基础介绍

    RecycleView继承自ViewGroup,是一种通过在滑动过程中不断回收复用进而实现流畅滑动的控件,RecycleView回收、缓存、复用的对象都是ViewHolder.itemView,也可以说是ViewHolder(后面回详细说明)

    // 直接继承关系
    public class RecyclerView extends ViewGroup implements ScrollingView,
            NestedScrollingChild2, NestedScrollingChild3 {
    	// some other code
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    二、滑动过程分析

    用户滑动RecycleView,首先时间会被Recycle.onTouchEvent()获取,我们直接看MotionEvent.ACTION_MOVE:

    1. MotionEvent.ACTION_MOVE
    case MotionEvent.ACTION_MOVE: {
                    final int index = e.findPointerIndex(mScrollPointerId);
                    if (index < 0) {
                        Log.e(TAG, "Error processing scroll; pointer index for id "
                                + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                        return false;
                    }
    
                    final int x = (int) (e.getX(index) + 0.5f);
                    final int y = (int) (e.getY(index) + 0.5f);
                    int dx = mLastTouchX - x;
                    int dy = mLastTouchY - y;
    
                    if (mScrollState != SCROLL_STATE_DRAGGING) {
                        boolean startScroll = false;
                        if (canScrollHorizontally) {
                            if (dx > 0) {
                                dx = Math.max(0, dx - mTouchSlop);
                            } else {
                                dx = Math.min(0, dx + mTouchSlop);
                            }
                            if (dx != 0) {
                                startScroll = true;
                            }
                        }
                        if (canScrollVertically) {
                            if (dy > 0) {
                                dy = Math.max(0, dy - mTouchSlop);
                            } else {
                                dy = Math.min(0, dy + mTouchSlop);
                            }
                            if (dy != 0) {
                                startScroll = true;
                            }
                        }
                        if (startScroll) {
                            setScrollState(SCROLL_STATE_DRAGGING);
                        }
                    }
    
                    if (mScrollState == SCROLL_STATE_DRAGGING) {
                        mReusableIntPair[0] = 0;
                        mReusableIntPair[1] = 0;
                        if (dispatchNestedPreScroll(
                                canScrollHorizontally ? dx : 0,
                                canScrollVertically ? dy : 0,
                                mReusableIntPair, mScrollOffset, TYPE_TOUCH
                        )) {
                            dx -= mReusableIntPair[0];
                            dy -= mReusableIntPair[1];
                            // Updated the nested offsets
                            mNestedOffsets[0] += mScrollOffset[0];
                            mNestedOffsets[1] += mScrollOffset[1];
                            // Scroll has initiated, prevent parents from intercepting
                            getParent().requestDisallowInterceptTouchEvent(true);
                        }
    
                        mLastTouchX = x - mScrollOffset[0];
                        mLastTouchY = y - mScrollOffset[1];
    
                        if (scrollByInternal(
                                canScrollHorizontally ? dx : 0,
                                canScrollVertically ? dy : 0,
                                e)) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                        }
                        if (mGapWorker != null && (dx != 0 || dy != 0)) {
                            mGapWorker.postFromTraversal(this, dx, dy);
                        }
                    }
                } break;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    代码前面主要是对滑动参数(水平滑动距离、垂直滑动距离)的合法性进行一些判定,并设定一些状态值(START_SCROLL等),我们直接看最后面的关键函数scrollByInternal(),最终的滑动操作是由此函数完成;

    2. scrollByInternal()
    boolean scrollByInternal(int x, int y, MotionEvent ev) {
            int unconsumedX = 0;
            int unconsumedY = 0;
            int consumedX = 0;
            int consumedY = 0;
    
            consumePendingUpdateOperations();
            if (mAdapter != null) {
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                scrollStep(x, y, mReusableIntPair);
                consumedX = mReusableIntPair[0];
                consumedY = mReusableIntPair[1];
                unconsumedX = x - consumedX;
                unconsumedY = y - consumedY;
            }
            if (!mItemDecorations.isEmpty()) {
                invalidate();
            }
    
            /**
            * some other code
            */
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这里只展示了scrollByInternal()部分源码, 可以看到,首先判定了一下mAdapter不为空,这里的mAdapter就是RecycleView.Adapter的一个实例,之后执行scrollStep(),OK,我们继续来看scrollStep()做了什么:

    3. scrollStep()
    /**
         * Scrolls the RV by 'dx' and 'dy' via calls to
         * {@link LayoutManager#scrollHorizontallyBy(int, Recycler, State)} and
         * {@link LayoutManager#scrollVerticallyBy(int, Recycler, State)}.
         *
         * Also sets how much of the scroll was actually consumed in 'consumed' parameter (indexes 0 and
         * 1 for the x axis and y axis, respectively).
         *
         * This method should only be called in the context of an existing scroll operation such that
         * any other necessary operations (such as a call to {@link #consumePendingUpdateOperations()})
         * is already handled.
         */
        void scrollStep(int dx, int dy, @Nullable int[] consumed) {
            startInterceptRequestLayout();
            onEnterLayoutOrScroll();
    
            TraceCompat.beginSection(TRACE_SCROLL_TAG);
            fillRemainingScrollValues(mState);
    
            int consumedX = 0;
            int consumedY = 0;
            if (dx != 0) {
                consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
            }
            if (dy != 0) {
                consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
            }
    
            TraceCompat.endSection();
            repositionShadowingViews();
    
            onExitLayoutOrScroll();
            stopInterceptRequestLayout(false);
    
            if (consumed != null) {
                consumed[0] = consumedX;
                consumed[1] = consumedY;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这里其实注释已经说的很明白了,scrollStep()会将RecycleView水平滑动dx,垂直滑动dy,并同时会统计实际消耗的滑动事件数。当dx或者dy不为0(即有效滑动)时,会分别通过mLayout.scrollHorizontallyBy()mLayout.scrollVerticallyBy()实现水平和垂直滑动。这里的mLayout,其类型是RecycleView.LayoutManager,我们继续看mLayout.scrollHorzontallyBy():

    4. mLayout.scrollHorzontallyBy()
    /**
         * {@inheritDoc}
         */
        @Override
        public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
                RecyclerView.State state) {
            if (mOrientation == VERTICAL) {
                return 0;
            }
            return scrollBy(dx, recycler, state);
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
                RecyclerView.State state) {
            if (mOrientation == HORIZONTAL) {
                return 0;
            }
            return scrollBy(dy, recycler, state);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可以看到,scrollHorizontallyBy()scrollVerticallyBy()逻辑是一样的,都是通过scrollBy()实现最终的滑动操作

    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
            if (getChildCount() == 0 || delta == 0) {
                return 0;
            }
            ensureLayoutState();
            mLayoutState.mRecycle = true;
            final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
            final int absDelta = Math.abs(delta);
            updateLayoutState(layoutDirection, absDelta, true, state);
            final int consumed = mLayoutState.mScrollingOffset
                    + fill(recycler, mLayoutState, state, false);
            if (consumed < 0) {
                if (DEBUG) {
                    Log.d(TAG, "Don't have any more elements to scroll");
                }
                return 0;
            }
            final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
            mOrientationHelper.offsetChildren(-scrolled);
            if (DEBUG) {
                Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
            }
            mLayoutState.mLastScrollDelta = scrolled;
            return scrolled;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    三、缓存回收过程分析

    我们知道,RecycleView复用的是ViewHolder,用户滑动过程中,旧的(滑到屏幕外的)Item(里面通常包含若干个View,回收的单位是ViewHolder)会被回收,新进入屏幕的会复用之前的View并重新绑定数据,那么我们首先需要关注,滑动过程中,新进入的View到底是如何创建的:

    1. RecycleView的四级缓存
    1.1. mAttachedScrap,mChangeScrap

    scrap是用来保存被rv移除掉但最近又马上要使用的缓存,比如rv自带item的动画效果,本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画,如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中。除此之外,在一些需要RecycleView不断自动刷新屏幕内数据的场景,典型的比如炒股软件:会不断刷新屏幕内股票价格、涨跌等信息,刷新过程中,View会被缓存到此scrap中。

    1.2. mCacheViews

    也是一个非常重要的缓存,就LinearLayoutManager来说,cached缓存默认大小为2,他的容量非常小,所起到的作用就是rv滑动时候刚被移出屏幕的viewholder的收容所,因为rv会认为刚被移出屏幕的viewholder可能马上就会使用到,所以不会立即设置为无效viewholder,会将他们保存到cached中,但又不能将所有移除屏幕的viewholder视为有效viewholder,所以他的默认容量只有两个,可以通过setViewCacheSize(int viewCount)方法修改

    1.3. mViewCacheExtension

    第三级缓存,这是一个自定义缓存,rv可以自定义缓存行为,在这里你可以决定缓存的保存逻辑,但是这么个自定义缓存一般都没有见过具体的使用场景,而且自定义缓存需要你对rv中的源码非常熟悉才行,否则在rv执行item动画,或者执行notify的一系列方法后你的自定义缓存是否还能有效就是一个值得考虑的问题,所以一般不太推荐使用该缓存,更多的我觉得这可能是google自已留着方便扩展来使用的,目前来说这还只是个空实现而已,从这点来看其实rv所说的四级缓存本质上还只是三级缓存。

    1.4. mRecycleViewPool

    又一个重要的缓存,这也是唯一一个我们开发者可以方便设置的一个(虽然extension也能设置,但是难度大),而且设置方式非常简单,new一个pool传进去就可以了,其他的都不用我们来处理,google已经给我们料理好后事了,这个缓存保存的对象就是那些无效的viewholder,虽然说无效的viewholder上的数据是无效的,但是他的rootview还是可以拿来使用的,这也是为什么最早listview有一个convertView的原因,当然这种机制也被rv很好的继承下来了,pool一般会和cached配合使用,这么来说cached存不下的就会被保存到pool中,毕竟cached的默认容量大小只有2,但是pool容量也是有限的当保存满之后再有viewholder到来的话就只能会无情抛弃掉,它也有一个默认的容量大小
    private static final int DEFAULT_MAX_SCRAP = 5;
    int mMaxScrap = DEFAULT_MAX_SCRAP;

    		static class ScrapData {
                final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
                int mMaxScrap = DEFAULT_MAX_SCRAP;
                long mCreateRunningAverageNs = 0;
                long mBindRunningAverageNs = 0;
            }
            SparseArray<ScrapData> mScrap = new SparseArray<>();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ViewPool内部本质上是采用一种稀松数组(SparseArray)存储数据的,这里的mScrap就是缓存池实例,每一个ScrapData对应一种类型(viewType)的缓存数据,针对每一种viewType,默认缓存大小为5,即每一种ViewHolder,缓存至多五个实例;

    2 缓存复用过程
    2.1. layoutChunk()

    在layoutChunk()方法中,我们可以看到如下代码:

    		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;
            }
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
            if (layoutState.mScrapList == null) {
                if (mShouldReverseLayout == (layoutState.mLayoutDirection
                        == LayoutState.LAYOUT_START)) {
                    addView(view);
                } else {
                    addView(view, 0);
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    滑动过程中的新加入的View是通过addView()直接加到当前页面View树的,而这个View是通过layoutState.next()方法获取,这里传入的参数类型时RecycleView.Recycle,也就是RecycleView的回收池

    2.2. layoutState.next()
    		View next(RecyclerView.Recycler recycler) {
                if (mScrapList != null) {
                    return nextViewFromScrapList();
                }
                final View view = recycler.getViewForPosition(mCurrentPosition);
                mCurrentPosition += mItemDirection;
                return view;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.3. recycle.getViewForPosition()
    2.4. tryGetViewHolderForPositionByDeadline()

    四、RecycleView的优化

    五、一些问题

    1. RecycleView适配器及理解:
      实现业务逻辑和UI解耦
    2. RecycleView如何判读一屏有多少个View?
      每加载一个Item进入RecycleView中,就将当前Item高度加到RecycleView中,直到当前bottom大于等于屏幕高度;
    3. RecycleView数据更新
      在这里插入图片描述
      当用户开始向上滑动RecycleView时,底部会出现空缺,此时RecycleView会去回收池中检查是否有被回收的View,如果有符合条件的,会通过适配器的onBindViewHolder(View view)为新的View绑定数据并添加到页面View树中进行显示。
      在这里插入图片描述
      回收池本质上是一个集合,准确的说是若干个栈,每个栈存储一种类型的View(如ImageView,Textview等),在不断滑动刷新过程中,实际上回收池会通过getItemType()返回的item类型去对应的栈中查找是否存在可复用的view;
      回收池的初始化是放在setAdapter()中进行的,并非放在RecycleView的构造或是初始化中,因为只有适配器被设置成功后,才可以确定有多少种View,进而确定回收池需要初始化多少个栈。

    RecycleView默认会使用key, value方式对每一个view打标签

    1. RecycleView数据的刷新:
      ① 局部刷新
      a. notifyItemChanged(2):刷新index = 2的item;
      b. notifyItemRangeChanged(2, 3):刷新index = 2 和 index = 3的item;
      ② 全局刷新
      a. notifyAllData()
  • 相关阅读:
    Redis6(一)——NoSQL数据库简介与Redis的安装
    Ansible密码正确但无法登录目标服务器
    【安装笔记-20240529-Windows-Electerm 终端工具】
    分析报告显示,PHP是编程语言主力军,且在电商领域占据“统治地位”
    Qt 关于QT_BEGIN_NAMESPACE宏的作用
    网络安全(骇客)—技术学习
    接口性能优化
    优先调节阀位,条件调节阀位
    Flink Icerberg 离线输仓-维度建模过程(二)
    4-1网络层-网络层的功能
  • 原文地址:https://blog.csdn.net/qq_41613281/article/details/124040240