设置数据观察者
为
RecyclerView
设置
Adapter
的时候,会给
Adapter
设置一个数据观察者
RecyclerViewDataObserver mObserver
。在
Adapter
通知数据更新的时候,这个观察者会根据情况完成界面刷新工作。
首先来分析最简单粗暴,而且也是最常用的数据刷新方法,
Adapter#notifyDataSetChanged()
,它表示数据完全改变,界面需要全局刷新。
Adapter#notifyDataSetChanged()
会调用
RecyclerViewDataObserver#onChanged()
方法
private class RecyclerViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
复制代码
RecyclerViewDataObserver
接收到数据完全改变的消息后,它首先做一些预处理工作,以响应数据集改变,然后请求重新布局来刷新界面。
首先来看看数据集改变的预处理工作到底做了啥
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
mDispatchItemsChangedEvent |= dispatchItemsChanged;
mDataSetHasChangedAfterLayout = true;
markKnownViewsInvalid();
复制代码
processDataSetCompletelyChanged()
首先做了一些状态标记,然后调用
markKnownViewsInvalid()
来标记已知的View为无效的。
void markKnownViewsInvalid() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
复制代码
markKnownViewsInvalid()
处理的目标不仅仅只有
RecyclerView
的子View,而且还包括
mCachedViews
缓存中的View。
界面由于滑动,导致某些不可见的子View被移除,并且优先使用
mCachedViews
缓存它。当再次由于滑动需要显示这个子View时,就会从
mCachedViews
中获取。
markKnownViewsInvalid()
做了两件事
标记View为
FLAG_UPDATE
和
FLAG_INVALID
。
标记View的
ItemDecoration
区域为
dirty
,也就是设置View的布局参数的
mInsetsDirty
值为
true
,表示需要刷新View的
ItemDecoration
区域。
预处理工作做完了,就会调用
requestLayout()
来重新布局,我们把主要精力放在
layout
过程。
layout
过程分为了三步,
dispatchLayoutStep1()
处理更新操作以及保存动画信息,
dispatchLayoutStep3()
执行动画并做一些清理工作,而
dispatchLayoutStep2()
是完成了数据刷新的工作。
dispatchLayoutStep2()
把子View的布局工作交给了
LayoutManager#onLayoutChildren()
完成,这里以
LinearLayoutManager#onLayoutChidren()
为例进行分析。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
fill(recycler, mLayoutState, state, false);
复制代码
LinearLayoutManager
对子View的布局工作大致分为两步。首先是分离/移除子View,并缓存它。然后是获取子View并填充给
RecyclerView
。
首先我们来分析分离/移除和缓存这一步,调用的是
LayoutManager#detachAndScrapAttachedViews()
方法,它会遍历
RecyclerView
所有子View,然后调用
LayoutManager#scrapOrRecycleView()
方法来执行分离/移除和缓存子View
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
return;
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
复制代码
在预处理工作环节,把View标记为
FLAG_INVALID
,因此就会先把这个View从
RecyclerView
中移除,使用的是
ViewGroup#removeViewAt()
方法。然后使用
Recycler
来回收这些被移除的子View。
我们现在来看下回收的过程
void recycleViewHolderInternal(ViewHolder holder) {
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
} else {
复制代码
由于子View被标记为
FLAG_INVALID
,因此子View只能交给
RecyclerPool
进行回收。
现在所有子View都被
RecyclerPool
回收了,那么接下来分析如何给
RecyclerView
填充子View。这一步是由
LinearLayoutManager#layoutChunk()
实现的
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
addView(view);
measureChildWithMargins(view, 0, 0);
layoutDecoratedWithMargins(view, left, top, right, bottom);
复制代码
填充子View经历了这四步,但是我们把目光放在如何从
Recycler
中获取View。它是由
Recycler#tryGetViewHolderForPositionByDeadline()
实现的
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false
ViewHolder holder = null
// 1. 首先从mChangedScrap中获取
// LinearLayoutManager在大部分情况下是支持predictive item animations
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position)
fromScrapOrHiddenOrCache = holder != null
// 2. 从mAttachedScrap, hidden view, mCachedViews中获取
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun)
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID)
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false)
holder.unScrap()
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag()
recycleViewHolderInternal(holder)
holder = null
} else {
fromScrapOrHiddenOrCache = true
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position)
final int type = mAdapter.getItemViewType(offsetPosition)
// 3. 通过stable id从mAttachedScrap, mCachedViews中获取
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun)
if (holder != null) {
// update position
holder.mPosition = offsetPosition
fromScrapOrHiddenOrCache = true
if (holder == null && mViewCacheExtension != null) {
// 4. 从自定义缓存中获取
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type)
if (view != null) {
holder = getChildViewHolder(view)
if (holder == null) { // fallback to pool
// 5. 从RecyclerPool中获取
holder = getRecycledViewPool().getRecycledView(type)
if (holder != null) {
// 重置所有的标志位
holder.resetInternal()
// ...
if (holder == null) {
// 6. 利用Adapter创建
holder = mAdapter.createViewHolder(RecyclerView.this, type)
// ...
// 7. 根据条件决定是否执行绑定操作
boolean bound = false
if (mState.isPreLayout() && holder.isBound()) {
// 如果已经绑定,就只更新predictive item animations的位置信息
holder.mPreLayoutPosition = position
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 如果没有绑定,就需要执行绑定操作
final int offsetPosition = mAdapterHelper.findPositionOffset(position)
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs)
// 8. 校正布局参数,并更新它
// ...
return holder
复制代码
这里我把所有的获取路径都标注了,但是由于View被标记为
FLAG_INVALID
,所以只能从
RecyclerPool
进行获取(第6步)。而从
RecylerPool
获取
ViewHolder
后,它的所有标志位都被重置了,因此还需要进行绑定(第7步)。
获取到了View,就把它添加到
RecyclerView
中,然后测量、布局、绘制,因此完成了界面刷新过程。
现在我们来总结下
Adapter#notifyDataSetChanged()
方法的优缺点。
优点就是简单无脑。缺点就是影响绘制性能,因为它要把所有子View移除、回收、获取、再绑定。
而在实际中,数据往往只改变一小部分,例如某几项数据更新了,某几项数据删除了等等。这个时候我们希望只刷新受影响的子View即可,而不是期望所有子View都刷新。
Adapter
提供了很多局部刷新的方法,例如
notifyItemChanged()
用来处理数据更新,
notifyItemInserted()
处理数据增加,
notifyItemRemoved()
处理数据移除,
notifyItemMove()
处理数据移动,并且还提供了相应的范围操作的方法
notifyRangXXX()
。这样我们就不必无脑使用
notifyDataSetChanged()
方法,但是需要我们自己比较数据,然后决定调用那种布局刷新的方法。
全局刷新影响绘制性能,那么我们来看看局部刷新是如何优化绘制性能的。
我们挑选
Adapter#notifyItemChanged()
方法来分析,它会调用观察者的
onItemRangeChanged()
方法
private class RecyclerViewDataObserver extends AdapterDataObserver {
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
复制代码
首先会通知
AdapterHelper
,某个范围的数据有改变
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
if (itemCount < 1) {
return false;
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
mExistingUpdateTypes |= UpdateOp.UPDATE;
return
mPendingUpdates.size() == 1;
复制代码
AdapterHelper
会创建一个相应操作类型的
UpdateOp
对象保存,然后也会保存此次操作的类型。
从返回值可以看出,如果等待更新的操作只有一个,就代表需要理解处理。我们假设现在只有一个更新操作,那么会调用
triggerUpdateProcessor()
来处理
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
复制代码
触发更新操作受条件限制,这里的限制条件基本上只有
mHasFixedSize
,它是通过
RecyclerView#setHasFixedSize()
设置的。如果
RecyclerView
的宽高设置为固定尺寸,例如
100dp
,或者
match_parent
,那么可以调用
setHasFixedSize(true)
设置
RecyclerView
有固定宽高。这样可以在某些时候避免
RecyclerView
自我测量这一步。
但是无论使用哪种方式执行更新操作,都会经历layout过程。在
dispatchLayoutStep1()
中会调用
processAdapterUpdatesAndSetAnimationFlags()
处理这些更新操作,并且决定是否执行动画。
本文不分析动画部分的源码。
private void processAdapterUpdatesAndSetAnimationFlags() {
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
复制代码
这里又根据
LayoutManager
是否支持
Predictive item animations
,分内了两种处理方式,但是它们殊途同归,最终它们都会处理受影响的子View。
Predictive item animations
: 对于添加,移除,移动操作(不包括改变操作),会自动创建一个动画,这个动画会显示
View
从哪里来,到哪里去。
对于数据改变操作,
processAdapterUpdatesAndSetAnimationFlags()
最终会通过
AdapterHelper
的如下代码来处理受影响的子View
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload)
复制代码
这个
mCallback
是在
RecyclerView
中实现的
void initAdapterManager() {
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
viewRangeUpdate(positionStart, itemCount, payload);
mItemsChanged = true;
复制代码
viewRangeUpdate()
处理了范围数据改变的操作,比较简单,总结如下
RecyclerView
处于数据改变范围内的子View,被标记为
FLAG_UPDATE
,并且标记它的
ItemDecoration
区域为
dirty
。
mCachedViews
中缓存的,且处于数据改变范围内View,被标记为
FLAG_UPDATE
,并且被
RecyclerPool
回收。
现在,
dispatchLayoutStep1()
已经把受数据改变影响的View(包括
mCachedView
缓存的)全部标记为
FLAG_UPDATE
,然后在
dispatchLayoutStep2()
中为子View进行重新布局,它是由
LayoutManager#onLayoutChildren()
实现的,我们这里仍然以
LinearLayoutManager#onLayoutChildren()
为例进行分析。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
fill(recycler, mLayoutState, state, false);
复制代码
这段代码是不是似曾相识,没错,我们刚在前面分析过,只不过这次分析的情况是局部数据改变,而非全局刷新。
首先我们来看下如何分离/废弃并缓存子View的。
RecyclerView#detachAndScrapAttachedViews()
会遍历所有子View,然后通过
ReyclerView#scrapOrRecycleView()
来处理
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
return;
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
复制代码
现在的情况是View只被标记为
FLAG_UPDATE
,这与全局刷新的情况不一样了,这里第一步是把子View从
RecyclerView
中分离(
detach
)而不是移除(
remove
)。它通过
RecyclerView
中的如下回调实现
private void initChildrenHelper() {
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
public void detachViewFromParent(int offset) {
final View view = getChildAt(offset);
if (view != null) {
final ViewHolder vh = getChildViewHolderInt(view);
if (vh != null) {
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
RecyclerView.this.detachViewFromParent(offset);
复制代码
首先把子View标记为
FLAG_TMP_DETACHED
,然后分离子View。
移除(
remove
)和分离(
detach
)有何区别?
移除会导致重新布局,也就是
requestLayout()
。
分离只是从
ViewGroup#mChildren
数组中移除引用,但是必须在同一个绘制周期内,把分离的View重新附着上去或者删除。因此并不会引发重新布局。
现在所有的子View都已经从
RecyclerView
中分离了,接下来就会使用
Recycler
来缓存它们,调用的是
RecyclerView#scrapView()
方法
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
复制代码
在
RecyclerView
的所有子View中,对于数据改变范围内的子View,会被标记为
FLAG_UPDATE
,它会被
mChangedScrap
缓存,而其他子View会被
mAttachedScrap
缓存。
现在我们已经了解了局部刷新的缓存是如何使用的,那么现在我们来看看
LinearLayoutManager
是如何实现子View填充的,它是由
LinearLayoutManager#layoutChunk()
实现的
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
addView(view);
measureChildWithMargins(view, 0, 0);
layoutDecoratedWithMargins(view, left, top, right, bottom);
复制代码
又是一段熟悉的代码,我们还是把目光聚焦到如何从
Recycler
获取子View的过程,它是通过
Recycler#tryGetViewHolderForPositionByDeadline()
实现的
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false
ViewHolder holder = null
// 1. 首先从mChangedScrap中获取
// LinearLayoutManager在大部分情况下是支持predictive item animations
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position)
fromScrapOrHiddenOrCache = holder != null
// 2. 从mAttachedScrap, hidden view, mCachedViews中获取
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun)
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID)
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false)
holder.unScrap()
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag()
recycleViewHolderInternal(holder)
holder = null
} else {
fromScrapOrHiddenOrCache = true
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position)
final int type = mAdapter.getItemViewType(offsetPosition)
// 3. 通过stable id从mAttachedScrap, mCachedViews中获取
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun)
if (holder != null) {
// update position
holder.mPosition = offsetPosition
fromScrapOrHiddenOrCache = true
if (holder == null && mViewCacheExtension != null) {
// 4. 从自定义缓存中获取
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type)
if (view != null) {
holder = getChildViewHolder(view)
if (holder == null) { // fallback to pool
// 5. 从RecyclerPool中获取
holder = getRecycledViewPool().getRecycledView(type)
if (holder != null) {
// 重置所有的标志位
holder.resetInternal()
// ...
if (holder == null) {
// 6. 利用Adapter创建
holder = mAdapter.createViewHolder(RecyclerView.this, type)
// ...
// 7. 根据条件决定是否执行绑定操作
boolean bound = false
if (mState.isPreLayout() && holder.isBound()) {
// 如果已经绑定,就只更新predictive item animations的位置信息
holder.mPreLayoutPosition = position
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 如果没有绑定,就需要执行绑定操作
final int offsetPosition = mAdapterHelper.findPositionOffset(position)
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs)
// 8. 校正布局参数,并更新它
// ...
return holder
复制代码
对于部分数据改变操作,被分离的子View,只被标记为
FLAG_UPDATE
和
FLAG_TMP_DETACHED
,因此能用到的获取View的途径只有第1步和第2步,也就是从
mChangedScrap
和
mAttachedScrap
中获取。然而,对于其他的操作,例如添加,移除,移动,可能就会从不同路径获取View。
对于数据改变这种情况,从
mChangedScrap
和
mAttachedScrap
中获取
ViewHolder
的条件基本上只要满足一个条件即可,这个条件是从
ViewHolder
获取的位置要与填充的位置相等。
从
mChangedScrap
和
mAttachedScrap
中获取
ViewHolder
后,它还是已经绑定的状态。但是对于数据改变受影响的子View,由于被标记为
FALG_UPDATE
,因此还需要再绑定一次数据,这样就可以达到数据更新的效果。而对于那些没有受数据改变影响的子View,就不需要再绑定。
现在我们已经从
Recycler
中获取了View,并且数据改变的View已经重新绑定数据,现在需要把这些分离的子View重新附着(
attach
)到
RecyclerView
上,它是通过
LayoutManager#addViewInt()
实现的
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
} else if (child.getParent() == mRecyclerView) {
} else {
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
复制代码
对于因为数据改变而分离的子View,会重新附着(
attach
)到
RecyclerView
上。然而对于移动,添加操作,还会有不同的操作,这里就不依依分析了。
现在被分离的子View已经重新附着到
RecyclerView
上,并且数据改变的部分也相应的更新了,剩下的就是绘制工作了。
局部刷新,是以分离和再附着的方式处理那些不受影响的子View,而只处理受影响的子View,或重新绑定后再附着,或直接创建在添加到
RecyclerView
。总之,相对于全局刷新,提升了绘制性能。
DiffUtil
要使用局部刷新,就必须比较前后的数据差异,然后决定使用哪种数据刷新方式。比较这个过程往往是复杂的,所以后来Google又推出了一个工具类
DiffUtil
,它把这个比较过程抽象出来,通过它可以计算前后数据差异,然后精准的调用局部刷新的方法。
如果觉得我的文章写的还不错,点个赞,关注我。
-
6778
-
SillyMonkey5504
Android
-
2276
-
wzgiceman
Android
RxJava
-
1257
-
Android面试官
Android Things