Recycleview的自问自答
RecycledView四级缓存的生命周期
mAttachedScrap
,他的数据随着一次layout
从无到有再到无.
从无到有 ->
执行layoutChildren
时候,会执行detachAndScrapAttachViews()
方法,当viewholder.flag
!=INVALID
&& !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()
的时候,当前的ViewHodler
就会被添加进入mAttachedScrap
从有到无 ->
在layoutChunk
阶段,从mAttachedScrap
拿到对应的ViewHolder
后,执行addView
操作完成后,当前ViewHodler
就会从mAttachedScrao
里面进行移除.
所以mAttachedScrap
的值随着一次布局从存在到消失.
mCachedView
生命周期.在滑动的是动态添加和移除.然后在调用notifyDataSetChanged
时候会被清空.RecycledViewPool
生命周期.添加进去就会一直存在.
Recycleview
获取ViewHolder
顺序:
- 如果当前
Recycleview.State.isPreLayout()==true
则先从mChangedScrap
数组里面获取,然后会将获取到的ViewHolder
的Flag
赋值为FLAG_RETURNED_FROM_SCRAP
.1
2
3
4
5
6ViewHolder tryGetViewHolderForPositionByDeadline(int position){
if(mState.isPreLayout()){
holder = mChangedScrap.get(position);
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
}
} - 从
mAttachedScrap
数组获取,还会将获取到的ViewHolder
的flag
赋值为FLAG_RETURNED_FROM_SCRAP
1
2
3
4
5
6ViewHolder tryGetViewHolderForPositionByDeadline(int position){
----
holder = mAttachedScrap.get(position)
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
----
} - 从
ChildHelper.mHiddenViews
数组获取 - 从
mCachedViews
获取,获取完后会从mCachedViews
里面移除当前对他的缓存1
2
3
4
5ViewHolder tryGetViewHolderForPositionByDeadline(int position){
----
holder = mCachedViews.get(position);
----
} - 从
mViewCacheExtenstion
获取,这个需要开发者自行实现 - 从
recycledViewPool
获取,需要注意,从这里获取的viewHolder
会进行一个重置操作,重置内容如下:所有的缓存里面,只有从1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class ViewHolder{
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
}RecycledViewPool
里面获取的ViewHolder
需要重新执行bind
操作.1
2
3
4
5
6ViewHolder tryGetViewHolderForPositionByDeadline(int position){
----
holder = getRecycledViewPool.getRecycledView(type);
holder.resetInternal();
----
} - 缓存里面还没拿到
ViewHolder
的话就去执行createViewHolder
操作.
Recycleview
什么时候会执行bindViewHolder
操作:
在执行bindViewHolder
方法前有个判断:
1 | if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){ |
holder.isBound() = true
是在执行过一次bindViewHodler
后进行的赋值.在第一次布局的时候,所有的ViewHolder
都会执行bindViewHolder
操作.然后执行完bindViewHodler
操作后,当前的viewHodler
的flag
会被赋值FLAG_BOUND
.
1 | void bindViewHolder(ViewHodler holder,int position){ |
所以:
- 第一次布局的时候会执行
bindViewHolder
- 从
RecycledViewPool
里面获取到的ViewHolder
会重新执行bind
,因为RecycledViewPool
厘面的viewHodler
的flag
会被重置掉. - 调用
notify
方法标记了需要更新的viewHodler
他们的flag
会带有FLAG_UPDATE
.所以会重新执行bind
操作.
为什么执行notifyDataSetChaged()
所有的ViewHodler
会重新执行bind
操作,而执行notifyItemChanged()
不会导致全部的viewHolder
执行bind操作
:
首先需要搞清楚哪些ViewHolder
是需要执行bind
操作的:
flag
值没有FLAG_BOUND
flag
值有UPDATE
flag
值有INVALID
那哪些情况下flag
会符合上面三种情况?
- 从
RecycledViewPool
里获取到的ViewHodler
,他们的Flag
值都为0 - 调用了
notifyDataSetChanged
方法,导致在布局的时候,获取到的ViewHodler
是从RecycledViewPool
里面获取到的. - 调用了
nofifyItemChanged
,他会将flag
标记为UPDATE
所以上面这个问题就好回答了,因为调用notifyDataSetChanged
会导致获取viewHolder
的操作都从RecycledViewPool
里面获取,所以所有的viewHolder
都会重新执行bind
操作.而调用notifyItemChanged
方法,所有的ViewHodler
都是从mAttachedScrap
里面获取的,然后只有需要更新的viewHodler
的flag
等于FLAG_UPDATE
.所以只有需要更新的才会执行bind
操作.
为什么调用notifyDataSetChanged
更新视图的时候,会导致视图全部重新执行bind
操作.
要探究这个问题就需要去看detachAndScrapAttachdViews()
方法.这个方法是在layoutChildren
阶段执行的.这个方法在初学阶段你是看不懂的,更不会用了, 现在我来讲明白.
这个方法做的事情就是分离视图和缓存已经存在的viewHodler
.
分离视图有两种选择:
removeView
detachViewFromParent
缓存viewHodler
有四种方法选择:
- 存进
mCachedViews
- 存进
recycledViewPool
- 存进
mAttachedScrap
- 存进
mChangedScrap
1 | private void detachAndScrapAttachdViews(Recycler recycler, int index, View view) { |
我们在调用notifyDataSetChanged
的时候,他会将我们的所有的viewHodler
的flag
设置为invalid 和 update
并且清空了mCachedViews
缓存,就导致在方法detachedAndScrapAttachViews
时候符合情况1,将当前的全部ViewHodler
缓存进了RecycledViewPool
里面.所以就导致执行更新时,会重新都走一遍bind
操作.
所以我们要少用notifyDataSetChanged
方法,他在回收的时候执行的是removeView
操作,是一个很重的操作,然后所有的ViewHodler也会被保存进RecycledViewPool
里面,而不是mRecycledScrap
里面,就导致还会重新走一遍bind
操作,增加了页面更新的时间.
Recycleview里面分割线的实现原理
Recycleview
的分割线是单独画上去的,在draw
阶段画的,所以他的层次要高于ViewHolder
,因为他是晚于ViewHolder
绘制.
看下测量阶段:
1 | class Recycleview{ |
在标记1的地方,可以看到去获取了我们分割线的大小.
1 | Rect getItemDecorInsetsForChild(View child) { |
在标记2的位置可以看到调用了ItemDirection
的getItemOffsets
方法,去获取每个viewHolder
对应的边距.所以自定义ItemDirection
的时候,需要你重写getItemOffsets
方法.设置完的值会被保存在layoutParams.mDecorInsets
里面.
大小获取完成了.现在就是布局了.再来看下布局:
1 | class LinearLayoutManager{ |
可以看到通过layoutParams
去获取了mDecorInsets
值.这是我们在measure
设置进去的.
综上所述,分析了Recycleview
的分割线实现原理.
recycleview获取ViewHolder的方法:
Recycleview
的成员方法findViewHolderForPosition(postion)
Recycleview
的成员方法findViewHolderForLayoutPosition(position)
Recycleview
的成员方法findViewHolderforAdapterPosition(position)
Recycleview
的成员方法findViewHolderForItemId(id)
区别:1 和 2 是一样子的. 2 和 3是不一样的.findViewHolderForLayoutPosition
获取的是holder.layoutPosition
值.findViewHolderforAdapterPosition
获取的是holder.position
值.
那现在问题来了,ViewHolder
里的position和mPreLayoutPosition
他们都表示位置信息,那有什么区别呢?
区别在,mPreLayoutPosition
获取的是当前此刻viewHolder
在的位置.而position
代表这次onLayout
结束后这个viewholder
的位置.这个值会更准确.
我们可以来看下源码:
首先看findViewHolderForLayoutPosition
:
1 | public ViewHolder findViewHolderForLayoutPosition(int position) { |
可以看到默认checkNewPostion
传的就是false
,表示不检查新位置,直接返回当前mPreLayoutPosition
等于目标值的viewHolder
再来看看findViewHolderForAdapterPosition
:
1 | public ViewHolder findViewHolderForAdapterPosition(int position) { |
可以看到当mDataSetHasChangedAfterLayout
为true
的时候会返回null
,那什么时候这个值会为true
呢,在adapter
调用方法notifyDataSetHasChanged
的时候,会赋值他为true
.然后在看方法getAdapterPositionfor(viewHolder)
1 | int getAdapterPositionFor(ViewHolder viewHolder) { |
可以看到内部调用了方法applyPendingUpdatesToPosition翻译下:执行内部位置更新操作.里面涉及到了数组
mPendingUpdates
,这个数组在我们执行notifyItemInserted notifyItemMoved notifyItemRangeChanged
方法的时候会生成操作对象然后存入到mPendingUpdates数组里面….然后在我们获取
viewHolder.position
的时候,这时候会考虑到在执行更新操作的时候是否影响到了当前我们的viewHolder
如果影响到了,就根具具体的操作去获取这次操作结束后的位置. 这就印证了我上面讲的,findViewHodlerforAdapterPostion
获取的位置是这次findViewHolderForLayoutPosition
获取的是当前viewHolder
的位置.
分析下Recycleview的dispatchLayout1
关于Recycleview
的布局分为三个阶段:dispatchLayout1() dispatchLayout2 dispatchLayout3()
.先分析下dispatchLayout1()
:
- 看下
processAdapterUpdatesAndSetAnimationFlags
方法:
这个方法会遍历数组mPendingUpdates
数组,(这个数组存的是我们在执行nofityItemInserted
等更新方法时候的操作),比如我们执行了一个插入更新操作,那么在dispatchLayout1()
阶段遍历这个数组的目的就是更新所有因为这个插入操作而导致的位置更新的viewHolder
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
39private void postponeAndUpdateViewHolders(UpdateOp op) {
mPostponedList.add(op);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.MOVE:
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("Unknown update op type for " + op);
}
}
public void offsetPositionsForAdd(int positionStart, int itemCount) {
offsetPositionRecordsForInsert(positionStart, itemCount);
mItemsAddedOrRemoved = true;
}
void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
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.mPosition >= positionStart) {
//去更新viewHolder的position值
holder.offsetPosition(itemCount, false);
mState.mStructureChanged = true;
}
}
mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
requestLayout();
}
看下方法
findMinMaxChildLayoutPositons(int info)
:
在Recycleview
里有一个成员属性int[] mMinMaxLayoutPosition
这个属性保存了Recycleview
在屏幕上可以显示的最小和最大的item
值.帮助Recycleview
在滑动的时候确定要显示哪些item
,以提高滑动的流畅性.这个值在dispatchLayout1
阶段会去修改:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private void findMinMaxChildLayoutPositions(int[] into) {
final int count = mChildHelper.getChildCount();
if (count == 0) {
into[0] = NO_POSITION;
into[1] = NO_POSITION;
return;
}
int minPositionPreLayout = Integer.MAX_VALUE;
int maxPositionPreLayout = Integer.MIN_VALUE;
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
final int pos = holder.getLayoutPosition();
if (pos < minPositionPreLayout) {
minPositionPreLayout = pos;
}
if (pos > maxPositionPreLayout) {
maxPositionPreLayout = pos;
}
}
into[0] = minPositionPreLayout;
into[1] = maxPositionPreLayout;
}可以看到遍历了当前屏幕上的所有孩子,找到他们的最小值和最大值.
继续看
dispatchLayout1
方法,现在看的是Recycleview
如何标记哪些是需要执行动画操作的view
.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
33class Recycleview{
void dispatchLayout1(){
***
if(mState.mRunSimpleAnimatioms){
//情况1
int count = mChildHelper.getChildCount();
for(int i = 0; i < count; ++i){
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
***
ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState,holder,flag,holder.getImodifiedPayloads());
mViewInfoStore.addToPreLayout(holder,animationInfo);
***
}
}
if(mState.mRunPredictiveAnimations){
//情况2
saveOldPositions();
mLayout.onLayoutChildren(mRecycler,mState);
for(int i = 0; i < mChildHelper.getChildCount(); ++i){
View child = mChildHelper.getChildAt(i);
ViewHolder viewHolder.getChildViewHolderInt(child);
//在刚才的预布局里面没找到这个viewholder说明这是新建的viewholder,是需要执行动画的viewholder
if(!mViewInfoStore.isInPreLayout(viewHolder)){
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder,animationInfo);
}
}
}
***
}
}首先先看情况1:,他遍历了当前页面上的所有孩子,然后给每个
viewHolder
创建了ItemHolderInfo
对象,并且保存在了viewInfoStore
的mLayoutHolderMap
里面,键名是holder
键值是InfoRecord
,这个时候每个InfoRecord
的flag
等于FLAG_PRE(从名字也可以看出是预布局的意思)
再看情况2,他会去执行layout.onLayoutChildren
进行一次布局,然后对比情况1看看多了哪些ViewHolder
,这些多出来的viewHolder
会被添加到ViewInfoStore
里面然后flag
会被赋值为FLAG_APPEAR
到这里dispatchLayout1
就分析完成了,他干了五件大事.
第一件大事:
执行processAdapterUpdatesAndSetAnimationFlags()
方法,遍历mPendingUpdates
数组,修改页面上所有viewHolder
的position
数据,是增是减.然后给是否执行动画标志位进行赋值.第二件大事:
给成员属性mMinMaxLayoutPosition
赋值,找到目前显示的最大和最小position
值第三件大事:
将当前页面的所有viewHolder
添加进了viewInfoStore
里面然后设置flag
等于FLAG_PRE第四件大事调用
layoutManager.onLayoutChildren()
,进去一次布局第五件大事,遍历页面所有孩子,对比第三件大事里面的
mViewInfoStore
没有的viewHolder
,然后将没有的viewHodler
添加到viewInfoStore
里面.并且设置flag
等于FLAG_APPEAR
分析下Recycleview的dispatchLayoutSetp2()
Recycleview中AnchorInfo的作用
AnchorInfo
用于记录Recycleview
的滚动状态,