RecyclerView知识点记录
获取缓存
1 | mLayout.onLayoutChildren() |
Recycler有4个层次用于缓存 ViewHolder 对象,优先级从高到底依次为:
- ArrayList
mAttachedScrap - ArrayList
mCachedViews //只有“列表回滚”这一种场景 - ViewCacheExtension mViewCacheExtension
- RecycledViewPool mRecyclerPool
补充:
第一次:
尝试从 mChangedScrap 中获取(条件:mState.isPreLayout() 为 true 时,即预布局)
第二次:
尝试从 1. mAttachedScrap 2.mHiddenViews 3.mCachedViews 中查找 ViewHolder
第三次:
第三次,如果给 Adapter 设置了 stableId,调用 getScrapOrCachedViewForId 尝试获取 ViewHolder
跟第二次的区别在于,之前是根据 position 查找,现在是根据 id 查找
第四次:
ViewCacheExtension,暂时忽略
第五次:
尝试从 RecyclerViewPool 中获取
如果四层缓存都未命中,则重新创建并绑定 ViewHolder 对象。
RecycledViewPool
1 | SparseArray<ScrapData> mScrap = new SparseArray<>(); |
填充->回收->滚动
1 | //RecyclerView.java |
接着看fill
1 | fill() |
其中scrollingOffset定义如下:
1 | updateLayoutState(){ |
逻辑是关键:mOrientationHelper.getDecoratedEnd(child) > limit,mOrientationHelper.getDecoratedEnd(child)计算出该表项底部到列表顶部的距离,在纵向列表中,“表项底部纵坐标 > 某个值”意味着表项位于某条线的下方,即 limit 是列表中隐形的线,所有在这条线上方的表项都应该被回收。limit = 在不往列表里填充新表项的情况下,列表最多可以滚动多少像素,比如:列表底部露出半个表现6.则limit = 表现6bottom - 列表height = 一半的表现6height
回到代码可知,如果第一个表项的getDecoratedEnd值 大于 limit,则回收 recycleChildren(recycler, 0, 0),即没有任何回收
再回到fill()
1 | fill(){ |
可见每一次while,mScrollingOffset是变大的
【这里逻辑有待研究……】
小结:
- RecyclerView 在滚动发生之前,会根据滚动位移大小来决定需要向列表中填充多少新的表项。
- RecyclerView 在滚动发生之前,会计算出一条limit 隐形线,它是决定哪些表项该被回收的重要依据。触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于limit 隐形线下方,则该表项上方的所有表项都会被回收。
fling
RecyclerView 的脱手滚动不是立刻执行的,触发脱手滚动时,一个叫ViewFlinger的Runnable会被抛到Choreographer中,它被包装成一个动画任务。等待下一个垂直同步信号到来时,这个任务就在主线程被执行。
OverScroller来计算每一段的滑动距离
布局
dispatchLayoutStep1 预布局(真正开启预布局,必须有ItemAnimator)
dispatchLayoutStep2 真正布局
dispatchLayoutStep3 post布局(对 mAttachedScrap 和 mChangedScrap 进行了清空)
mAttachedScrap和mChangedScrap
在将表项一个个填充到列表之前会先将其先回收到mAttachedScrap中
mAttachedScrap生命周期起始于RecyclerView布局开始(dispatchLayoutStep2),终止于RecyclerView布局结束(dispatchLayoutStep3)。
mAttachedScrap用于屏幕中可见表项的回收和复用
举个简单的例子,列表有1-4个表项,现删除了第3个表项,这会触发layout过程,RecyclerView的layout有好多个阶段,第一阶段列表的4个表项对应的ViewHolder都会被存入scrap列表,layout的最后阶段会去取用于用于展示的表项(1,2,4,5),此时1,2,4表项就可以从scap中获取,而5从recycler pool中获取
mAttachedScrap:是从屏幕上分离出来的ViewHolder,但是又即将添加到屏幕上去的ViewHolder(涉及到onLayoutChildren,先清空再填充)
mChangedScrap:mChangedScrap主要是为列表项数据发生变化时的动画效果服务的。
mChangedScrap保存的holder信息只有预布局时才会被复用
使用场景:
1.开启了列表项动画(itemAnimator),并且列表项动画的canReuseUpdatedViewHolder(ViewHolder viewHolder)方法返回false的前提下;
2.调用了notifyItemChanged、notifyItemRangeChanged这一类方法,通知列表项数据发生变化;
mChangedScrap 和 mAttachedScrap 只在布局阶段使用。其他时候它们是空的。布局完成之后,这两个缓存中的 viewHolder,会移到 mCacheView 或者 RecyclerViewPool 中。
RV预加载
监听列表滚动状态
1 | if(layoutManager.findLastVisibleItemPosition() > 某个值){ |
容易出现bug,不准确,而且和LayoutManager类型耦合
类型无关预加载
可以把刚才的判断逻辑移到
1 | override fun onBindViewHolder(holder: ViewHolder, position: Int){ |
- 不需要关心列表滑动的快慢,因为所有表项都会经历onBindViewHolder(),索引值和预加载阈值就可以用==做判断。
- 不要担心用户在列表底部多次上拉导致回调多次预加载,因为这种情况下onBindViewHolder()不会执行多次。
(如果滑动过长,触发了onBindViewHolder,可以用标识位) - 当RecyclerView更换LayoutManager时,也不需要修改代码。
性能优化
- onCreateViewHolder中的加载xml布局,可以换成动态代码,弃用xml
- 也可以使用异步加载布局 AsyncLayoutInflater
- ConstraintLayout没有性能优势
- Glide初始化比较耗时,可以提前初始化
notifyDataSetChanged
RecyclerView 在真正刷新列表之前,将一切都无效化了。包括当前所有被填充表项及离屏缓存中的 ViewHolder 实例。无效化体现在代码上即是为 ViewHolder 添加 FLAG_UPDATE 和 FLAG_INVALID 标志位。
因为在上一节的“无效化”阶段,ViewHolder 被添加了 FLAG_UPDATE 和 FLAG_INVALID 标志位,所以就满足了!holder.isBound() || holder.needsUpdate() || holder.isInvalid()这个条件,从缓存池命中的 ViewHolder 就得重新绑定数据。
参考文章:
https://juejin.cn/column/6962854213718130701?share_token=6f2049ad-b27c-44bc-87c1-476b0894fd13