RecyclerView知识点

RecyclerView知识点记录


获取缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mLayout.onLayoutChildren()
fill()
//循环条件 = 剩余空间大于0
while(){ layoutChunk() }
//layoutChunk获取表项
View view = layoutState.next(recycler);
View view = recycler.getViewForPosition(mCurrentPosition)
tryGetViewHolderForPositionByDeadline()

//取到之后,使view成为rv子视图
addView(view);
//测量
measureChildWithMargins(view, 0, 0);
//布局
layoutDecoratedWithMargins(view, left, top, right, bottom);

//其中addView()的最终落脚点是ViewGroup.attachViewToParent()

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
2
3
4
5
6
7
SparseArray<ScrapData> mScrap = new SparseArray<>();
static class ScrapData {
//同类ViewHolder存储在ArrayList中
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
//每种类型的ViewHolder最多存5个
int mMaxScrap = DEFAULT_MAX_SCRAP;
}

填充->回收->滚动

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
//RecyclerView.java
case MotionEvent.ACTION_MOVE:
//父控件优先
dispatchNestedPreScroll()
scrollByInternal()


//自身滚动
scrollStep()
//将列表未消耗的滚动距离继续留给其父控件消耗
dispatchNestedScroll()


//下面scrollStep()调用链
if (y != 0) {
// 垂直滚动
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}

//LayoutManager.java
scrollVerticallyBy()
scrollBy()
fill()//先填充再滚动
mOrientationHelper.offsetChildren(-scrolled);

//OrientationHelper.java
mLayoutManager.offsetChildrenVertical(amount);

//RecyclerView.java
mRecyclerView.offsetChildrenVertical(dy);
for (int i = 0; i < childCount; i++) {
mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
}

//View.java
//该方法会修改 View 的 mTop 和 mBottom 值,并触发轻量级的重绘
offsetTopAndBottom()

接着看fill

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
fill()

//不断获取新表项用于填充的同时也在回收表项,就好比滚动着的列表,有表项插入的同时也有表项被移出
//循环条件 = 剩余空间大于0
while(){
//填充新的表项
layoutChunk()
//回收表项
recycleByLayoutState(recycler, layoutState);
}

private void recycleByLayoutState() {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd();
} else {
recycleViewsFromStart();
}
}

recycleViewsFromStart(){

//其中noRecycleSpace的值为 0
final int limit = scrollingOffset - noRecycleSpace;

for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit || ......) {
//回收索引为 0 到 i-1 的表项
recycleChildren(recycler, 0, i);
return;
}
}

}

//回收索引为 0 到 i-1 的表项
recycleChildren(){
for(){
removeAndRecycleViewAt(i, recycler);
}
}

其中scrollingOffset定义如下:

1
2
3
4
5
6
7
8
updateLayoutState(){
//计算出该表项底部到列表顶部的距离,然后在减去列表长度。这个差值可以理解为在不往列表里填充新表项的情况下,列表最多可以滚动多少像素
scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
...
// 将列表因滚动而需要的额外空间存储在 mLayoutState.mAvailable
mLayoutState.mAvailable = requiredSpace;
mLayoutState.mScrollingOffset = scrollingOffset;
}

逻辑是关键:mOrientationHelper.getDecoratedEnd(child) > limit,mOrientationHelper.getDecoratedEnd(child)计算出该表项底部到列表顶部的距离,在纵向列表中,“表项底部纵坐标 > 某个值”意味着表项位于某条线的下方,即 limit 是列表中隐形的线,所有在这条线上方的表项都应该被回收。limit = 在不往列表里填充新表项的情况下,列表最多可以滚动多少像素,比如:列表底部露出半个表现6.则limit = 表现6bottom - 列表height = 一半的表现6height

回到代码可知,如果第一个表项的getDecoratedEnd值 大于 limit,则回收 recycleChildren(recycler, 0, 0),即没有任何回收

再回到fill()
1
2
3
4
5
6
7
8
9
fill(){
while(){
......
layoutChunk();
......
// 在 limit 上追加新表项所占像素值
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
}
}

可见每一次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
2
3
if(layoutManager.findLastVisibleItemPosition() > 某个值){
loaddata();
}

容易出现bug,不准确,而且和LayoutManager类型耦合

类型无关预加载

可以把刚才的判断逻辑移到

1
2
3
override fun onBindViewHolder(holder: ViewHolder, position: Int){
//根据position来决定是否加载数据
}
  1. 不需要关心列表滑动的快慢,因为所有表项都会经历onBindViewHolder(),索引值和预加载阈值就可以用==做判断。
  2. 不要担心用户在列表底部多次上拉导致回调多次预加载,因为这种情况下onBindViewHolder()不会执行多次。
    (如果滑动过长,触发了onBindViewHolder,可以用标识位)
  3. 当RecyclerView更换LayoutManager时,也不需要修改代码。

性能优化

  1. onCreateViewHolder中的加载xml布局,可以换成动态代码,弃用xml
  2. 也可以使用异步加载布局 AsyncLayoutInflater
  3. ConstraintLayout没有性能优势
  4. 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