RecycleView
描述
在 RecycleView 的 onMeasure 阶段,默认采用自动测量模式,如果自身宽高不确定,会尝试通过 LayoutManager 测量子 Item 来确定自己的宽高,所以尽可能的使用 match_parent 或固定宽高来布局 RecycleView,减少性能的损耗
在 RecycleView 的 onLayout 阶段,才会真正调用调用 LayoutManager.onLayoutChildren() 来对 Item 进行测量和布局
在 LayoutManager.onLayoutChildren() 方法中,会先通过 detachAndScrapAttachedViews() 函数回收布局,然后再通过 file() 函数进行布局,具体流程如下:
file() -> layoutChunk() -> LayoutState.next() -> addView() -> measureChildWithMargins() -> layoutDecoratedWithMargins()
可以看到 LayoutState.next() 函数里最终调用 tryGetViewHolderForPositionByDeadline() 函数,然后通过 Recyler 四级缓存或创建来获取 ViewHolder
从上面可以得出,会先回收布局,然后再从缓存中取出来,这里是不是没事找事做了?这里应该是一种职责分离吧,layout 的事归 LayoutManager 管,缓存的事归 Recycler 管。LayoutManager 不应该知道哪个 viewHolder 是否有效
可以看到 RecycleView为了提高效率,部分交互是通过 attachViewToParent 和 detachViewFromParent 来对 View 进行轻量级的添加和移除,其中 attachViewToParent 可以调整 View 的布局顺序,注意与 getChildDrawingOrder 的区别
【备注一】detach 在 ViewGroup 中的实现很简单,只是将当前 View 从 ParentView 的 ChildView 数组中移除,将当前 View 的 mParent 设置为null, 可以理解为轻量级的临时 remove。
【备注二】remove 代表真正的移除,不光从 ChildView 数组中移除,其他和View树各项联系也会被彻底斩断。
核心组件
LayoutManager:负责item的measure、layoutRecycler:四级回收、复用机制SmoothScroller:滑动速度控制LinearSmoothScroller
SnapHelper:惯性滑动控制LinearSnapHelperPagerSnapHelper
ItemAnimator:Item动画SimpleItemAnimatorDefaultlteAnimator
ItemDecoration:Item样式装饰DividerItemDecorationItemTouchHelper:可以实现侧滑删除,拖动排序- 可以扩展吸点悬浮的效果
OnItemTouchListener:手势拦截器DiffUtil:差分刷新
Recyler 回收池 - RecycleView 四级缓存
1 | public final class Recycler { |
优化点
当我们调用
notifyDataSetChanged()或notifyItemRangeChanged(p, c)(c这个范围非常大的时候),会将所有的ViewHolder全部放入到pool中,即第四级缓存中,由于第四级缓存默认大小为5,所以当界面显示的数量大于5时,会出现丢弃的情况,造成需要重新craete和bind来补充缺失的item,对性能影响比较大(notifyItemRangeChanged哪怕超出屏幕的item都会尝试create和bind,同样性能损耗较大)。如果列表显示很多行,且notifyDataSetChanged()函数调用较为频繁,可以考虑设置pool的大小。1
recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);
或者给条目设置全局ID:
1
2
3
4
5
6// adapter 中复写
override fun getItemId(position: Int): Long {
return position.toLong()
}
mAdapter.setHasStableIds(true);
当知道
Item的改变不会影响到RecycleView的宽高时,可以通过setHasFixedSize(true)函数增加提高增删改插时的性能,原因如下:1
2
3
4
5
6
7
8
9
10
11
12
13// onItemRangeChanged
// onItemRangeRemoved
// onItemRangeMoved
// onItemRangeInserted
// 当 mHasFixedSize = true 时,上述四种方法都不会触发 requestLayout 重新布局
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}【注意】
notifyDataSetChanged()最终会触发onChanged()并调用requestLayout()重新测量布局数据预取,
Android 5.0起,显示分为两个阶段:- UI线程先处理布局信息,即创建和绑定View
- 渲染(Render)线程把指令发送给GPU
在不卡顿的前提下,帧与帧之间UI线程会有闲置的时机,
RecycleView在新条目即将进入视野前,会花大量的时间来创建和绑定View,造成下一帧出现卡顿,数据预取的思想就是利用闲置的线程,提前将下一帧创建和绑定View的工作移至前一帧的空闲时间来完成。如果使用
RecycleView内部的LayoutManager,已经自动优化了。需要手动优化有以下两种场景:
- 嵌套
RecyclerView:内部的LayoutManager需要调用LinearLayoutManager#setInitialPrefetchItemCount来设置预存取的数量,如:至少展示三个条目,则需要传入4 - 自定义
LayoutManager:需要重写LayoutManager#collectInitialPrefetchPositions方法实现预取逻辑,参考:https://juejin.cn/post/6844903661382959118
在
TabLayout+ViewPager+RecyclerView的场景中,多个界面的条目布局是一致的,可以考虑多个RecyclerView共用一个RecycledViewPool来减少创建ViewHolder的开销在条目中需要加载图片等场景,滚动时加载或只加载内存中的图片
尽可能的使用局部刷新函数,善用
DiffUtil避免在
onCreate和onBind中创建过多对象RecycleView和Item尽可能的不要使用wrap_content不要在
ScrollView中添加RecycleView