RecyclerView 是如何管理视图的(二) ChildHelper

1,413 阅读4分钟

ChildHelper是LayoutManager中用来操作视图的辅助方法。由于动画的原因,LayoutManager中,可能会同时含有两套布局。但是我们知道,Android的视图体系中,RecyclerView是ViewGroup;加入的Item是子View,ViewGroup当中会通过child记录所有ChildView的引用,如果我们直接在child中操作动画产生的多套布局显然是不现实的。

所以,RecyclerView将子视图的管理交给了ChildHelper,任何对子视图的操作都必须通过ChildHelper进行。

1. ChildHelper

RecyclerView用来管理子View的类,它内部包含了一个RecyclerView,并且为其附加了隐藏一些子视图的能力。主要用于动画方面。其中,大致包含两类的方法,常规的一类方法包括一系列的ViewGroup同名方法的重写,比如getChildAt, getChildCount,这些方法将会忽略掉被隐藏的视图。

如果需要将RecyclerView当做一个普通的ViewGroup的情况下,不考虑隐藏的视图,那么则需要使用unfiltered方法簇,例如:getUnfilteredChildCountgetUnfilteredChildAt

ChildHelper的变量只有三个:

final Callback mCallback;//事件发生时,在RecyclerView中的回调,在RecyclerView中实现

final Bucket mBucket;// 链表的结构的二进制Bitset,按位置来标记对应View的状态。

final List<View> mHiddenViews;// 用于存储被Hide的View

1.1 Callback

ChildHelper在RecyclerView构造时被初始化,构造方法中显式地传入了Callback的实现方法:

// RecyclerView.java#initChildrenHelper()
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
    @Override
    public int getChildCount() {
        // 返回过滤后的子视图数目
      	return RecyclerView.this.getChildCount();
    }

    @Override
    public void addView(View child, int index) {
      	// 向RecyclerView中添加视图,并向对应的ViewHolder派事件;
        RecyclerView.this.addView(child, index);
      	// Adapter中,触发onViewAttachedToWindow方法
        dispatchChildAttached(child);
    }

    @Override
    public int indexOfChild(View view) {
      	// 返回当前子视图的下标
        return RecyclerView.this.indexOfChild(view);
    }

    @Override
    public void removeViewAt(int index) {
        final View child = RecyclerView.this.getChildAt(index);
        if (child != null) {
          	// 派发ViewHolder的Detach事件,在Adapter中响应;
            dispatchChildDetached(child);
            child.clearAnimation();
        }
      	// 从RecyclerView中移除视图
        RecyclerView.this.removeViewAt(index);
    }

    @Override
    public View getChildAt(int offset) {
      	// 返回对应位置的视图
        return RecyclerView.this.getChildAt(offset);
    }

    @Override
    public void removeAllViews() {
      	// 删除所有视图
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            dispatchChildDetached(child);
            child.clearAnimation();
        }
        RecyclerView.this.removeAllViews();
    }

    @Override
    public ViewHolder getChildViewHolder(View view) {
      	// 根据View获得ViewHolder
      	// (通常从RecyclerView的视角出发,用其Children数组中的View获取其对应的ViewHolder)
        return getChildViewHolderInt(view);
    }

    @Override
    public void attachViewToParent(View child, int index,
            ViewGroup.LayoutParams layoutParams) {
      	// 根据view找到对应的ViewHolder,对其进行检查,实际的attach和ViewHolder并没有关系。
        final ViewHolder vh = getChildViewHolderInt(child);
        if (vh != null) {
          if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
                throw new IllegalArgumentException("Called attach on a child which is not" + " detached: " + vh + exceptionLabel());
          }
         	//清除
          vh.clearTmpDetachFlag();
        }
      	// 将视图attach到RecyclerView上
        RecyclerView.this.attachViewToParent(child, index, layoutParams);
    }

    @Override
    public void detachViewFromParent(int offset) {
      	// detach
        final View view = getChildAt(offset);
        if (view != null) {
            final ViewHolder vh = getChildViewHolderInt(view);
            if (vh != null) {
                if (vh.isTmpDetached() && !vh.shouldIgnore()) {
                    throw new IllegalArgumentException("called detach on an already"
                            + " detached child " + vh + exceptionLabel());
                }
                if (DEBUG) {
                    Log.d(TAG, "tmpDetach " + vh);
                }
                vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
            }
        }
        RecyclerView.this.detachViewFromParent(offset);
    }

    @Override
    public void onEnteredHiddenState(View child) {
      	// 当视图进入Hiden状态时的回调函数
        final ViewHolder vh = getChildViewHolderInt(child);
        if (vh != null) {
          	// 内部应该是一些和Accessibility相关的处理;
            vh.onEnteredHiddenState(RecyclerView.this);
        }
    }

    @Override
    public void onLeftHiddenState(View child) {
        final ViewHolder vh = getChildViewHolderInt(child);
        if (vh != null) {
            vh.onLeftHiddenState(RecyclerView.this);
        }
    }
});

1.2 Bucket

链表的结构的二进制Bitset,按位置来标记对应View的状态。单个Bucket中可以标记64个位置,如果访问时,超过了第64个位置(下标0~63),那么自然会去链表结构的下一个Bucket中查找,如果没有下一个会先创建。

void set(int index) {
    if (index >= BITS_PER_WORD) {
        ensureNext();// 如果访问的位置对于当前的「Bucket」来说越界了,那么就去尝试创建下一个(有则不创建)
        mNext.set(index - BITS_PER_WORD);
    } else {
        mData |= 1L << index;
    }
}

private void ensureNext() {
    if (mNext == null) {
        mNext = new Bucket();
    }
}

在ChildHelper对视图进行管理时,

void addView(View child, int index, boolean hidden) {
		// ……
    mBucket.insert(offset, hidden); //利用位运算将插入位置标记出来
    // ……
}

private int getOffset(int index) {
  	// ……
  	// 用于统计BitSet中有多少是有效位(二进制位上为1或者0)
  	final int removedBefore = mBucket.countOnesBefore(offset);
}

1.3 mHiddenViews

专门用于存储被隐藏的视图的List,在hideViewInternal中执行addunhideViewInternal中执行remove

HiddenViews,被ChildHelper手动隐藏掉的视图,该类视图对ChildHelper可见,但是对RecyclerView来说不能分辨,RecyclerView只能知道自己的Children数组中存储了哪些View,而这些View可能是隐藏的,也可能是显示的。同理,LayoutManager也无法预知这些隐藏的视图。所以,需要让LayoutManager进行两次布局,一次保留原有的修改项目和新增的项目,一次进行最终的布局以完成动画。

而每次发生变化的视图,旧的View、ViewHolder将会在动画结束之后,被隐藏。如果不开启动画,mHiddenViews中估计永远是空的。

1.4 ChildHelper的结构模型

正如之前提到的,在含有动画的情况下,RecyclerView会对布局进行两次布局,以快照的形式比较得出预测动画。而第一次布局时,会同时保留修改的内容和新增的内容,这样一来,就会在执行动画期间,造成数据集data和Recycler的Children无法完全对应的情况。

mBucket所做的事情,就是区分出这一类已经在data中删除的视图,mBucket中用第index位来标记ViewGroup当中,第index个视图是否是这类整处于动画状态的视图(AnimatingVIew);而这一类视图会被mHiddenViews存储为一个副本,mHiddenViews中的视图会被recycler#tryGetViewHolderForPositionByDeadline下的getScrapOrHiddenOrCachedHolderForPosition所调用,作用是:

LayoutManager请求Recycler分配ViewHolder时getScrapOrHiddenOrCachedHolderForPosition本身是从mAttachScrap缓存中尝试去取离屏ViewHolder or 从Hidden列表中去取数据 or 从缓存列表mCachedView中去取缓存的ViewHolder。

而这两部之间还插入了一个在非预演状态下(!dryRun)直接从mHiddenViews中取出,将其ViewHolder的状态修改为UN_HIDE,可以实现高效的复用,通常的触发场景是单个ViewHolder频繁的动画场景下。

比较简单的触发方式是在含有动画的情况下,快速点击修改RecyclerView某一项的内容,当前一个动画还未完成时,即可完成ViewHolder从mHiddenViews中的复用。

例如当前ViewHolder是ViewHolder-A,点击更新当前ViewHolder,ViewHolder在开始执行动画时,被记为隐藏的ViewHolder,并加入mHiddenViews

  • 如果在动画完成之前,再次点击更新ViewHolder,此时会直接从mHidden中取出ViewHolder-A显示
  • 如果在动画完成之前不作点击,那么ViewHolder会被更新为ViewHolder-BViewHolder-A会被从mHiddenViews中移入mCacheViews缓存。
  • 如果在动画完成之后点击,那么当前ViewHolder同样会被更新为ViewHolder-A,只不过是从mCachedView中取出的ViewHolder-A,而ViewHolder-B则暂时被存入mHiddenViews,动画完成后,被移入mCachedViews(体现在removeAnimatingView中调用了recycler#recycleViewHolderInternal)。

RecyclerView并没有实现自己的addView系列的方法,我们使用的任何RecyclerView#addView方法都是其超类ViewGroup的相关方法。RecyclerView由于AnimatingView这类特殊状态的视图存在,它将子View的管理完全委托给了ChildHelper。这也是为什么,在开头我们提到,ChildHelper中,含有一些ViewGroup同名方法的重复实现,它们内部嵌套了一些对于AnimatingView状态的管理,以及对RecyclerView的addView的统一调用,本质上也是和动画具有强相关的一个类,如果不开启动画,那么ChildHelper作用基本上就退化成对ViewGroup相关方法的一个单一的包裹类。

Unfiltered方法族,和原版方法的比较如下:

void removeAllViewsUnfiltered(){
  mBucket.reset();
  // 清空mHiddenViews,防止内存泄漏。回调相关的方法
  for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
      mCallback.onLeftHiddenState(mHiddenViews.get(i));
      mHiddenViews.remove(i);
  }
  mCallback.removeAllViews();
  if (DEBUG) {
      Log.d(TAG, "removeAllViewsUnfiltered");
  }
  // 调用RecyclerView的removeAllViews();
}
// 无额外实现removeAllViews方法,直接调用:mCallback.removeAllViews();


int getUnfilteredChildCount(){
  // 不区分AnimatingView实际上就是RecyclerView的子View数量
  return mCallback.getChildCount();
}
int getChildCount() {
    return mCallback.getChildCount() - mHiddenViews.size();
}


View getUnfilteredChildAt(int index){
  // 返回对应位置上的视图
  return mCallback.getChildAt(index);
}
View getChildAt(int index) {
    final int offset = getOffset(index);
  	// 获取第i个位置,但可能是被隐藏的视图,所以要根据一些条件,计算真正「可见」的第index个视图的位置,即偏移量。
    return mCallback.getChildAt(offset);
}
Reference
  1. RecyclerView机制解析: ChildHelper