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_SCRAP1
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_BOUNDflag值有UPDATEflag值有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.
分离视图有两种选择:
removeViewdetachViewFromParent
缓存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()阶段遍历这个数组的目的就是更新所有因为这个插入操作而导致的位置更新的viewHolder1
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的滚动状态,