前言:阅读RecyclerView的源码,是想解决哪些疑问?
- 设置数据集合后,是如何显示出来的?- UI 绘制
- ViewHolder 是如何实现复用的?
- datas 和 UI 为什么会有不一致的情况?出现越界异常?
- notifyDataChanged 和 notifyItemChanged 性能差异在哪里?
首先介绍 RecyclerView 的几员大将,了解它们都提供了什么能力;做哪些操作要去找哪个负责人。
- LayoutManager mLayout:测量和定位子视图,以及决定什么时候回收对用户不可见的子视图。
- Recycler mRecycler:管理报废或分离的子视图,以供重用。
- Adapter mAdapter:将具体的数据集合转换为 RecyclerView 展示的视图。
- AdapterHelper mAdapterHelper:处理 mAdapter 的更新操作。当子视图更新时,将发生排队操作。
UI 绘制
详细资料:RecyclerView源码解析(一)——绘制流程
RecyclerView 说到底,也是一个 ViewGroup 而已。绘制也是正常的流程
onMeasure -> onLayout -> onDraw
if(宽高不固定) {
onMeasure():
dispatchLayoutStep1();
dispatchLayoutStep2();
onLayout():
dispatchLayoutStep3();
onDraw();
} else {
onMeasure();
onLayout():
dispatchLayoutStep1();
dispatchLayoutStep2();
dispatchLayoutStep3();
onDraw();
}
dispatchLayoutStep1()
* 1.处理Adapter的更新
* 2.决定哪些动画需要执行
* 3.保存当前视图的信息
* 4.如果必要的话,运行预测性布局并保存其信息
dispatchLayoutStep2()
* 布局的第二步,我们为最终状态的视图进行实际布局。如有必要,此步骤可以多次运行。
* 该方法中会设置自视图数量 mState.mItemCount = mAdapter.getItemCount();
* 该方法中会调用 mLayout.onLayoutChildren(mRecycler, mState);
dispatchLayoutStep3()
* 布局的最后一步,我们保存有关动画视图的信息,触发动画并进行必要的清理
mLayout.onLayoutChildren(mRecycler, mState)方法重点操作:
* 确定锚点,根据页面布局方式向上/下填充
* 填充的时候,layoutState.next(recycler) 获取我们应该布局的下一个元素的视图
* recycler.getViewForPosition(mCurrentPosition),通过下面的缓存顺序获取可用的 ViewHolder,
如果缓存中没有,则会调用 mAdapter.createViewHolder(RecyclerView.this, type) 创建新的ViewHolder;
* ViewHolder 未绑定 or 需要更新 or 无效时,尝试绑定视图
# 我们 adapter 中重写的
getItemCount()、getItemViewType(int position)、
onCreateViewHolder(ViewGroup parent, int viewType)、
onBindViewHolder(RecyclerViewHolderAdapter.ViewHolderProxy holder, int position)
都会在这个方法中用到。RecyclerView的四级缓存:
详细资料:
真正带你搞懂 RecyclerView 的缓存机制,再也不怕面试被虐了
补充:
mChangedScrap:这个变量和上边的 mAttachedScrap 是一样的,唯一不同的是,它存放的是发生了变化的 ViewHolder ,这里缓存的 ViewHolder 是要重新走 Adapter 的绑定方法的,mAttachedScrap 中缓存的 ViewHolder 不需要。
layout 期间,会将所有的 ViewHolder 都放到 scrap 中,其中,发生变化的放入mChangedScrap 中,其余的放入 mAttachedScrap 中。
mHiddenViews:它不在 Recycler 这个类中,它在 ChildHelper 类中,是个缓存被隐藏的View 的 ArrayList。使用的时候,获取到 View 后,会将它从该列表删除,得到它对应的 ViewHolder ,并判断 ViewHolder 的各种状态将其加到 mAttachedScrap 或 mChangedScrap 中。
查找顺序:mChangedScrap、mAttachedScrap、mHiddenViews、mCachedViews、mViewCacheExtension、mRecyclerPool
需要重新绑定视图的:mChangedScrap、mRecyclerPool
不需要重新绑定的:mAttachedScrap、mCachedView
回收复用
回收:当一个 itemView 从可见到不可见时,RecyclerView 利用回收机制,将它存放到了内存中,以便其他 item 出现时,不用每次都去生成一个新的 itemView ,而是只去绑定数据就行了。
越界异常
通过阅读源码,发现造成 IndexOutOfBoundsException 的两个比较数值是:
mAdapterHelper.findPositionOffset(position):开篇提到过 mAdapterHelper 主要是处理 mAdapter 的更新操作,
notifyItemChanged 等一系列相关操作,AdapterHelper 为每次适配器数据更改创建一个 UpdateOp ,UpdateOp 中会包含操作类型、修改项位置及个数。
在这个方法中,通过比较 position 和 mPostponedList 中每个 UpdateOp 的操作类型、修改项位置及个数,得出最后的有效位置。
AdapterHelper介绍:
可以排队和处理适配器更新操作的 Helper 类。
为了支持动画,RecyclerView 提供了一个旧版本的适配器来最好地表示布局的先前状态。
有时,当删除未布置的项目时,这并不是一件容易的事,在这种情况下,RecyclerView 无法为动画提供该项目的视图。
AdapterHelper 为每次适配器数据更改创建一个 UpdateOp ,然后对其进行预处理。
在预处理期间,AdapterHelper 找出哪些 UpdateOps 可以推迟到第二次布局通过,哪些不能。
对于无法推迟的 UpdateOps ,AdapterHelper 将根据先前推迟的操作更改它们并在第一次布局通过之前将它们分派。
还负责更新延迟的 UpdateOps ,因为此过程更改了操作顺序。
尽管可以按不同顺序将操作转发到 LayoutManager ,但是可以保证所得数据集保持一致。
mAdapter.getItemCount():这个容易理解,就是我们设置的 Adapter 重写的 getItemCount() 方法,
一般都是返回数据集合的大小。mAdapterHelper.findPositionOffset(position) >= mAdapter.getItemCount() 会引起 IndexOutOfBoundsException ,说明 AdapterHelper 中适配器数据更改的条数 和 数据集合的不一致,所以我们在使用 notifyItemChanged 类方法时,务必要和数据集合的改动量保持一致。
解决方案:
- 在使用 notifyItemChanged 类方法时,务必要和数据集合的改动量保持一致。
- 自定义 LayoutManager 类,继承 LinearLayoutManager ,重写 onLayoutChildren 方法。
- 牺牲性能,使用 notifyDataSetChanged 方法。
notifyDataSetChanged 和 notifyItemChanged 性能差异
在调用 notifyDataSetChanged 方法后,所有的子 View 会被标记,都被缓存到 RecyclerPool 中,然后重新绑定数据。并且由于 RecyclerPool 有容量限制,如果不够最后就要重新创建新的视图了。
但是使用 notifyItemChanged 等方法会将视图缓存到 mChangedScrap 和 mAttachedScrap 中,这两个缓存是没有容量限制的,所以基本不会重新创建新的视图,只是 mChangedScrap 中的视图需要重新绑定一下。
参考链接
真正带你搞懂 RecyclerView 的缓存机制,再也不怕面试被虐了
RecyclerView源码解析(四)——RecyclerView进阶优化使用