Recycleview的自问自答

RecycledView四级缓存的生命周期

  1. mAttachedScrap,他的数据随着一次layout从无到有再到无.

从无到有 ->

执行layoutChildren时候,会执行detachAndScrapAttachViews()方法,当viewholder.flag!=INVALID && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()的时候,当前的ViewHodler就会被添加进入mAttachedScrap

从有到无 ->

layoutChunk阶段,从mAttachedScrap拿到对应的ViewHolder后,执行addView操作完成后,当前ViewHodler就会从mAttachedScrao里面进行移除.

所以mAttachedScrap的值随着一次布局从存在到消失.

  1. mCachedView生命周期.在滑动的是动态添加和移除.然后在调用notifyDataSetChanged时候会被清空.

  2. RecycledViewPool生命周期.添加进去就会一直存在.


Recycleview获取ViewHolder顺序:

  1. 如果当前Recycleview.State.isPreLayout()==true则先从mChangedScrap数组里面获取,然后会将获取到的ViewHolderFlag赋值为FLAG_RETURNED_FROM_SCRAP.
    1
    2
    3
    4
    5
    6
    ViewHolder tryGetViewHolderForPositionByDeadline(int position){
    if(mState.isPreLayout()){
    holder = mChangedScrap.get(position);
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    }
    }
  2. mAttachedScrap数组获取,还会将获取到的ViewHolderflag赋值为FLAG_RETURNED_FROM_SCRAP
    1
    2
    3
    4
    5
    6
    ViewHolder tryGetViewHolderForPositionByDeadline(int position){
    ----
    holder = mAttachedScrap.get(position)
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    ----
    }
  3. ChildHelper.mHiddenViews数组获取
  4. mCachedViews获取,获取完后会从mCachedViews里面移除当前对他的缓存
    1
    2
    3
    4
    5
    ViewHolder tryGetViewHolderForPositionByDeadline(int position){
    ----
    holder = mCachedViews.get(position);
    ----
    }
  5. mViewCacheExtenstion获取,这个需要开发者自行实现
  6. recycledViewPool获取,需要注意,从这里获取的viewHolder会进行一个重置操作,重置内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class 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
    6
    ViewHolder tryGetViewHolderForPositionByDeadline(int position){
    ----
    holder = getRecycledViewPool.getRecycledView(type);
    holder.resetInternal();
    ----
    }
  7. 缓存里面还没拿到ViewHolder的话就去执行createViewHolder操作.

Recycleview什么时候会执行bindViewHolder操作:

在执行bindViewHolder方法前有个判断:

1
2
3
if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
bindViewHolder();
}

holder.isBound() = true是在执行过一次bindViewHodler后进行的赋值.在第一次布局的时候,所有的ViewHolder都会执行bindViewHolder操作.然后执行完bindViewHodler操作后,当前的viewHodlerflag会被赋值FLAG_BOUND.

1
2
3
4
5
6
void bindViewHolder(ViewHodler holder,int position){
holder.position = positon;
holder.setFlags(ViewHolder.FLAG_BOUND,
ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
}

所以:

  1. 第一次布局的时候会执行bindViewHolder
  2. RecycledViewPool里面获取到的ViewHolder会重新执行bind,因为RecycledViewPool厘面的viewHodlerflag会被重置掉.
  3. 调用notify方法标记了需要更新的viewHodler他们的flag会带有FLAG_UPDATE.所以会重新执行bind操作.

为什么执行notifyDataSetChaged()所有的ViewHodler会重新执行bind操作,而执行notifyItemChanged()不会导致全部的viewHolder执行bind操作:

首先需要搞清楚哪些ViewHolder是需要执行bind操作的:

  1. flag值没有FLAG_BOUND
  2. flag值有UPDATE
  3. flag值有INVALID

那哪些情况下flag会符合上面三种情况?

  1. RecycledViewPool里获取到的ViewHodler,他们的Flag值都为0
  2. 调用了notifyDataSetChanged方法,导致在布局的时候,获取到的ViewHodler是从RecycledViewPool里面获取到的.
  3. 调用了nofifyItemChanged,他会将flag标记为UPDATE

所以上面这个问题就好回答了,因为调用notifyDataSetChanged会导致获取viewHolder的操作都从RecycledViewPool里面获取,所以所有的viewHolder都会重新执行bind操作.而调用notifyItemChanged方法,所有的ViewHodler都是从mAttachedScrap里面获取的,然后只有需要更新的viewHodlerflag等于FLAG_UPDATE.所以只有需要更新的才会执行bind操作.


为什么调用notifyDataSetChanged更新视图的时候,会导致视图全部重新执行bind操作.

要探究这个问题就需要去看detachAndScrapAttachdViews()方法.这个方法是在layoutChildren阶段执行的.这个方法在初学阶段你是看不懂的,更不会用了, 现在我来讲明白.
这个方法做的事情就是分离视图缓存已经存在的viewHodler.

分离视图有两种选择:

  1. removeView
  2. detachViewFromParent

缓存viewHodler有四种方法选择:

  1. 存进mCachedViews
  2. 存进recycledViewPool
  3. 存进mAttachedScrap
  4. 存进mChangedScrap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void detachAndScrapAttachdViews(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
//情况1
removeViewAt(index);
addViewHolderToRecycledViewPool(holder)
} else {
// 情况2
detachViewAt(index);
if(flag == removed || falg == Invalid || !flag==update){
mAttachedScrap.add(hodler)
}else{
mChangedScrap.add(hodler)
}
}
}

我们在调用notifyDataSetChanged的时候,他会将我们的所有的viewHodlerflag设置为invalid 和 update并且清空了mCachedViews缓存,就导致在方法detachedAndScrapAttachViews时候符合情况1,将当前的全部ViewHodler缓存进了RecycledViewPool里面.所以就导致执行更新时,会重新都走一遍bind操作.
所以我们要少用notifyDataSetChanged方法,他在回收的时候执行的是removeView操作,是一个很重的操作,然后所有的ViewHodler也会被保存进RecycledViewPool里面,而不是mRecycledScrap里面,就导致还会重新走一遍bind操作,增加了页面更新的时间.


Recycleview里面分割线的实现原理

Recycleview的分割线是单独画上去的,在draw阶段画的,所以他的层次要高于ViewHolder,因为他是晚于ViewHolder绘制.

看下测量阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Recycleview{
class LayoutManager{
void measureChildWithMargins(View child,int widthUsed,int heightUsed){
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//标记1
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;

final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
}
}

在标记1的地方,可以看到去获取了我们分割线的大小.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}

if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
//标记2
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}

在标记2的位置可以看到调用了ItemDirectiongetItemOffsets方法,去获取每个viewHolder对应的边距.所以自定义ItemDirection的时候,需要你重写getItemOffsets方法.设置完的值会被保存在layoutParams.mDecorInsets里面.

大小获取完成了.现在就是布局了.再来看下布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
class LinearLayoutManager{
void layoutChunk(){
layoutDecoratedWidthMargins(view,l,t,r,b);
}
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
}

可以看到通过layoutParams去获取了mDecorInsets值.这是我们在measure设置进去的.

综上所述,分析了Recycleview的分割线实现原理.


recycleview获取ViewHolder的方法:

  1. Recycleview的成员方法findViewHolderForPosition(postion)
  2. Recycleview的成员方法findViewHolderForLayoutPosition(position)
  3. Recycleview的成员方法findViewHolderforAdapterPosition(position)
  4. Recycleview的成员方法findViewHolderForItemId(id)

区别:1 和 2 是一样子的. 2 和 3是不一样的.
findViewHolderForLayoutPosition获取的是holder.layoutPosition值.findViewHolderforAdapterPosition获取的是holder.position值.
那现在问题来了,ViewHolder里的position和mPreLayoutPosition他们都表示位置信息,那有什么区别呢?
区别在,mPreLayoutPosition获取的是当前此刻viewHolder在的位置.而position代表这次onLayout结束后这个viewholder的位置.这个值会更准确.

我们可以来看下源码:
首先看findViewHolderForLayoutPosition:

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
public ViewHolder findViewHolderForLayoutPosition(int position) {
return findViewHolderForPosition(position, false);
}
ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) {
final int childCount = mChildHelper.getUnfilteredChildCount();
ViewHolder hidden = null;
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.isRemoved()) {
if (checkNewPosition) {
if (holder.mPosition != position) {
continue;
}
} else if (holder.getLayoutPosition() != position) {
continue;
}
if (mChildHelper.isHidden(holder.itemView)) {
hidden = holder;
} else {
return holder;
}
}
}
return hidden;
}

可以看到默认checkNewPostion传的就是false,表示不检查新位置,直接返回当前mPreLayoutPosition等于目标值的viewHolder

再来看看findViewHolderForAdapterPosition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public ViewHolder findViewHolderForAdapterPosition(int position) {
if (mDataSetHasChangedAfterLayout) {
return null;
}
final int childCount = mChildHelper.getUnfilteredChildCount();
ViewHolder hidden = null;
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.isRemoved()
&& getAdapterPositionFor(holder) == position) {
if (mChildHelper.isHidden(holder.itemView)) {
hidden = holder;
} else {
return holder;
}
}
}
return hidden;
}

可以看到当mDataSetHasChangedAfterLayouttrue的时候会返回null,那什么时候这个值会为true呢,在adapter调用方法notifyDataSetHasChanged的时候,会赋值他为true.然后在看方法getAdapterPositionfor(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
39
int getAdapterPositionFor(ViewHolder viewHolder) {
mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition);
}

public int applyPendingUpdatesToPosition(int position) {
final int size = mPendingUpdates.size();
for (int i = 0; i < size; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
if (op.positionStart <= position) {
position += op.itemCount;
}
break;
case UpdateOp.REMOVE:
if (op.positionStart <= position) {
final int end = op.positionStart + op.itemCount;
if (end > position) {
return RecyclerView.NO_POSITION;
}
position -= op.itemCount;
}
break;
case UpdateOp.MOVE:
if (op.positionStart == position) {
position = op.itemCount; //position end
} else {
if (op.positionStart < position) {
position -= 1;
}
if (op.itemCount <= position) {
position += 1;
}
}
break;
}
}
return position;
}

可以看到内部调用了方法applyPendingUpdatesToPosition翻译下:执行内部位置更新操作.里面涉及到了数组mPendingUpdates,这个数组在我们执行notifyItemInserted notifyItemMoved notifyItemRangeChanged方法的时候会生成操作对象然后存入到mPendingUpdates数组里面….然后在我们获取viewHolder.position的时候,这时候会考虑到在执行更新操作的时候是否影响到了当前我们的viewHolder如果影响到了,就根具具体的操作去获取这次操作结束后的位置. 这就印证了我上面讲的,findViewHodlerforAdapterPostion获取的位置是这次onLayout结束后的位置.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
    39
    private 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
    25
    private 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
    33
    class 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对象,并且保存在了viewInfoStoremLayoutHolderMap里面,键名是holder键值是InfoRecord,这个时候每个InfoRecordflag等于FLAG_PRE(从名字也可以看出是预布局的意思)

再看情况2,他会去执行layout.onLayoutChildren进行一次布局,然后对比情况1看看多了哪些ViewHolder,这些多出来的viewHolder会被添加到ViewInfoStore里面然后flag会被赋值为FLAG_APPEAR

到这里dispatchLayout1就分析完成了,他干了五件大事.

  • 第一件大事:
    执行processAdapterUpdatesAndSetAnimationFlags()方法,遍历mPendingUpdates数组,修改页面上所有viewHolderposition数据,是增是减.然后给是否执行动画标志位进行赋值.

  • 第二件大事:
    给成员属性mMinMaxLayoutPosition赋值,找到目前显示的最大和最小position

  • 第三件大事:
    将当前页面的所有viewHolder添加进了viewInfoStore里面然后设置flag等于FLAG_PRE

  • 第四件大事调用layoutManager.onLayoutChildren(),进去一次布局

  • 第五件大事,遍历页面所有孩子,对比第三件大事里面的mViewInfoStore没有的viewHolder,然后将没有的viewHodler添加到viewInfoStore里面.并且设置flag等于FLAG_APPEAR

分析下Recycleview的dispatchLayoutSetp2()

Recycleview中AnchorInfo的作用

AnchorInfo用于记录Recycleview的滚动状态,