当RecyclerView滚动时它干了什么

简介: 当RecyclerView滚动时它干了什么

当谈到RecyclerView的时候,复用机制是我们能脱口而出的优点之一。系统内置的ViewHolder避免了我们使用ListView时手动去创建ViewHolder的麻烦。但是关于何时回收View,何时复用View,我们真的能做到胸有成竹吗?当我们滑动一个RecyclerView时,是先回收View,再复用View?还是先复用View,再回收View呢?答案是都有可能。具体分析且看下面分析。


本文大纲

  1. 滑动RV的两个场景
  2. DEMO验证答案
  3. 滑动原理讲解
  4. 源码分析


01

滑动RV的两个场景


场景一  RV中每个Item高度都为100px,最后一个Item超出屏幕50px。RV初始状态如下图

640.png

Q1    假设向上滑动40px

请问是否有View发生回收和复用?如果有,先复用还是先回收?答    回收和复用都没有发生

Q2    假设向上滑动60px请问是否有View发生回收和复用?如果有,先复用还是先回收?答    没有发生回收,发生了复用

Q3    假设向上滑动120px请问是否有View发生回收和复用?如果有,先复用还是先回收?答    发生了回收和复用。先复用后回收


场景二 RV中第一个Item高度为50px,其它都为100px,最后一个Item超出屏幕95px。RV初始状态如下


640.png

Q1    假设向上滑动40px请问是否有View发生回收和复用?如果有,先复用还是先回收?答    回收和复用都没有发生
Q2    假设向上滑动60px请问是否有View发生回收和复用?如果有,先复用还是先回收?答    发生了回收,没有发生复用
Q3    假设向上滑动120px请问是否有View发生回收和复用?如果有,先复用还是先回收?答    发生了回收和复用。先回收后复用

从答案可以看出。回收和复用并没有固定的答案。它因场景而异。此时你会提出质疑了,上面的答案只是你片面之词。我凭什么相信你的答案就是正确的。那我们进入第二个环节,验证答案真伪。


02

DEMO验证答案


我们来验证场景一


程序运行图

640.png


程序代码

class RecyclerViewActivity1 : AppCompatActivity() {
    private lateinit var mRecyclerView: RecyclerView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view1)
        mRecyclerView = findViewById(R.id.recyclerview)
        mRecyclerView.setHasFixedSize(true)
        mRecyclerView.setItemViewCacheSize(0)
        mRecyclerView.layoutManager =
            LinearLayoutManager(this).apply {
                orientation = LinearLayoutManager.VERTICAL
                isItemPrefetchEnabled = false
            }
        val list: MutableList<String> =
            ArrayList()
        repeat(100) {
            list.add("item $it")
        }
        mRecyclerView.adapter = MyAdapter(list)
    }
    inner class MyAdapter(val mStrings: MutableList<String>) :
        RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            println("RecyclerView 场景一 onCreateViewHolder ")
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.view_item, parent, false)
            return object : RecyclerView.ViewHolder(view) {}
        }
        override fun getItemCount(): Int {
            return mStrings.size
        }
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            println("RecyclerView 场景一 onBindViewHolder $position ")
            val textView = holder.itemView as TextView
            textView.layoutParams.height = (resources.displayMetrics.density * 100).toInt()
            textView.text = mStrings[position]
        }
        override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
            println("RecyclerView 场景一 发生回收 " + (holder.itemView as TextView).text)
            super.onViewRecycled(holder)
        }
    }
    fun scroll120(view: View) {
        mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 120).toInt())
    }
    fun scroll60(view: View) {
        mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 60).toInt())
    }
    fun scroll40(view: View) {
        mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 40).toInt())
    }
}


日志输出


首先进入初始状态


image.png

点击上滑40px。打印日志不变。证明 回收和复用都没有发生


点击上滑60px。打印日志如下。证明 没有发生回收,发生了复用


image.png


点击上滑动120px。打印日志如下。证明 发生了回收和复用。先复用后回收


image.png


我们来验证场景二


程序运行图

640.png


程序代码

class RecyclerViewActivity2 : AppCompatActivity() {
    private lateinit var mRecyclerView: RecyclerView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view2)
        mRecyclerView = findViewById(R.id.recyclerview)
        mRecyclerView.setHasFixedSize(true)
        mRecyclerView.setItemViewCacheSize(0)
        mRecyclerView.layoutManager =
            LinearLayoutManager(this).apply {
                orientation = LinearLayoutManager.VERTICAL
                isItemPrefetchEnabled = false
            }
        val list: MutableList<String> =
            ArrayList()
        repeat(100) {
            list.add("item $it")
        }
        mRecyclerView.adapter = MyAdapter(list)
    }
    inner class MyAdapter(val mStrings: MutableList<String>) :
        RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            println("RecyclerView 场景二 onCreateViewHolder ")
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.view_item, parent, false)
            return object : RecyclerView.ViewHolder(view) {}
        }
        override fun getItemCount(): Int {
            return mStrings.size
        }
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            println("RecyclerView 场景二 onBindViewHolder $position ")
            val textView = holder.itemView as TextView
            textView.layoutParams.height =
                if (position == 0) (resources.displayMetrics.density * 50).toInt() else (resources.displayMetrics.density * 100).toInt()
            textView.text = mStrings[position]
        }
        override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
            println("RecyclerView 场景二 发生回收 " + (holder.itemView as TextView).text)
            super.onViewRecycled(holder)
        }
    }
    fun scroll120(view: View) {
        mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 120).toInt())
    }
    fun scroll60(view: View) {
        mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 60).toInt())
    }
    fun scroll40(view: View) {
        mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 40).toInt())
    }
}

日志输出

首先进入初始状态


image.png


点击上滑40px。打印日志不变。证明 回收和复用都没有发生


点击上滑60px。打印日志如下。证明 发生回收,没有发生复用



image.png

点击上滑动120px。打印日志如下。证明 发生了回收和复用。先回收后复用


image.png

03

滑动原理分析

640.png


如图所示,介绍几个关于坐标的参数

  1. delta:手指滑动的距离120px。
  2. mOffsetRV最后一个子View的Bottom在屏幕坐标系的Y坐标600px。RV的下一个View(Item7)mOffset处布局。
  3. mScrollingOffset:RV最后一个子view的Bottom距离RV Bottom的距离50px。向上滑动不超过该距离。如需创建新的View。
  4. mVailable:delta-mScrollingOffset。可以填充View的空间。如果大于0表示有空间填充新的View

如果delta<mScrollingOffset,mScrollingOffset=delta,mVailable<0


滑动逻辑如下

  1. 从RecyclerView的第0个View开始遍历,直到View的Bottom>mScrollingOffset,并记录该View的下标index,回收[0,index)区间的View,index为开区间,如果index=0则不发生回收,会调用RV的removeView方法。具体回收算法先按下不表。
  2. 如果mVailable>0,则从mOffset处,用新的View填充。mOffset+=新View的高度,mVailable-=新View的高度,mScrollingOffset+=新View的高度,如果mVailable<0,mScrollingOffset+=mVailable。布局完成后用步骤1的算法按需回收上面的View。
  3. 重复步骤2
  4. 将RV整体,向上移动delta或者consumed距离


根据此滑动逻辑,我们分析场景一中的向上滑动120px

640.png

mOffset = 600px

mScrollingOffset = 50px

mAvailable = 70px

item1高度100px


  1. 首先从第0个View遍历Bottom>50px。找到item1.bottom=100px,记录index=0。因为0<1。所以不发生回收
  2. mAvailable>0,从Item6的底部,增加View Item7(此处发生复用逻辑)高度为100px,mOffset=700px,mOffset=-30,mAvailable=-30,mScrollingOffset=mScrollingOffset+100-30=120px。然后检查回收。首先从第0个View遍历Bottom>120px。找到item2.bottom=200px,记录index=1。回收[0,1)区间的View。即回收Item1
  3. mAvailable=-30<0,推出填充逻辑
  4. 整体向上移动120px

我们看到先创建Item7 然后回收Item1。跟日志相符合


image.png

同样的逻辑我们也可以分析场景二中的向上滑动120px的情况。场景二会先发生回收,再发生复用。读者可以自己去求证。


03

源码分析


RV的滑动,最终会调用LayoutManager的scrollBy方法。我们使用的是LinearLayoutManager。


//LinearLayoutManager.java
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
                + 
                (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;
    }


第10行updateLayoutState方法,主要是计算mOffset等参数。

第11行fill方法,根据剩余空间,填充View

第21行offsetChildren,整体移动RV的子View

//主要是计算
private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        // If parent provides a hint, don't measure unlimited.
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mLayoutDirection = layoutDirection;
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        calculateExtraLayoutSpace(state, mReusableIntPair);
        int extraForStart = Math.max(0, mReusableIntPair[0]);
        int extraForEnd = Math.max(0, mReusableIntPair[1]);
        boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
        mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;
        mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;
        int scrollingOffset;
        if (layoutToEnd) {
            mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
            // get the first child in the direction we are going
            final View child = getChildClosestToEnd();
            // the direction in which we are traversing children
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // calculate how much we can scroll without adding new children (independent of layout)
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();
        } else {
            final View child = getChildClosestToStart();
            mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding();
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
            scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
                    + mOrientationHelper.getStartAfterPadding();
        }
        mLayoutState.mAvailable = requiredSpace;
        if (canUseExistingSpace) {
            mLayoutState.mAvailable -= scrollingOffset;
        }
        mLayoutState.mScrollingOffset = scrollingOffset;
    }
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.mExtraFillSpace;
        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();
            }
            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 || layoutState.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;
    }



第10行,首先判断是否需要回收View。

第14行,根据剩余空间,判断是否需要,第19行是具体的layout方法,45行是layout完成后判断是否需要回收View。


本文主要讲解了滑动时的回收和复用的逻辑。具体如何如何回收,如何复用。RecyclerView的三级缓存是如何实现的。且听下回分解。

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
Android开发 数据格式 XML
Android FrameLayout子view居中(左居中,右居中)等
Android的布局FrameLayout默认是把布局内的子view堆砌在左上角,但是,可以通过设置子view的: android:layout_gravity 此参数控制子view的布局位置,实现FrameLayou...
2797 0
|
存储 缓存 容灾
AIGC 商业化道路探索 - Stable Diffusion 商业化应用(下)
Stable Diffusion 应用到商业领域的案例越来越多,商用场景下的技术架构应当如何构建?本文基于阿里云近期的一个 Stable Diffusion 商业案例,对大规模底模切换、大量 LoRA 调优的场景提出一个商业场景适用的技术架构,并已实现部署交付,稳定运行。
|
7月前
|
人工智能 UED
AI教育热潮,如何衡量教育App渠道投放效果?
AI教育是高潜力场景,但如何衡量AI热潮下教育App的真实拉新与转化效果呢?
266 6
|
自然语言处理
高效团队的秘密:7大团队效能模型解析
3分钟了解7大团队效能模型,有效提升团队绩效。
1317 7
高效团队的秘密:7大团队效能模型解析
|
计算机视觉
[笔记]OpenCV+FFmpeg+Qt实现视频编辑器之OpenCV视频lO接口
[笔记]OpenCV+FFmpeg+Qt实现视频编辑器之OpenCV视频lO接口
515 0
|
数据安全/隐私保护
注册Github账号详细过程
注册GitHub账号前,请备妥有效电子邮件地址以接收验证信,设定独特的用户名(仅含字母、数字或单连字符,不以连字符起始或结束),并创建具有一定复杂度的密码。接着,访问GitHub官网(`https://github.com/`),点击右上角的“Sign up”进入注册页面。在此页面填写用户名、电子邮箱与密码,选择是否接收产品更新及公告,通过人机验证后提交。最后,通过注册邮箱内的验证链接完成验证,即可启用您的GitHub账号。
1684 0
|
XML IDE 开发工具
13. 【Android教程】文本框 TextView
13. 【Android教程】文本框 TextView
324 2
|
自然语言处理 算法 测试技术
【C/C++ CommonAPI入门篇】从 Franca IDL 到 C++: 深入解析汽车软件接口开发
【C/C++ CommonAPI入门篇】从 Franca IDL 到 C++: 深入解析汽车软件接口开发
910 1
|
Java
带圆角LOGO的QrCode二维码实时生成
最近工作中经常要用到QrCode二维码,研究了一下,写了个带圆角LOGO的JAVA实现,QrCode之所以能在中间放个LOGO图标,是因为编码时的信息冗余。
2297 0
|
运维 Oracle 关系型数据库
医院检验科LIS系统源码,oracle数据库、报告管理、质控管理
医院检验科LIS系统源码,oracle数据库、报告管理、质控管理
336 0