RecyclerView源码分析

RecyclerView自从出道也有几年的光景了,大家对于它的赞扬一直络绎不绝,所以对于如此受欢迎的控件我们必须好好的了解一下。本文尝试带领大家更加全面的理解RecyclerView。

设计思路

RecyclerView官网给出的定义是A flexible view for providing a limited window into a large data set.,也就是在限定的试图内展示大量的数据,来一张通俗明了的图:

image

RecyclerView的职责就是将Datas中的数据以一定的规则展示在它的上面,但说破天RecyclerView只是一个ViewGroup,它只认识View,不清楚Data数据的具体结构,所以两个陌生人之间想构建通话,我们很容易想到适配器模式,因此,RecyclerView需要一个Adapter来与Datas进行交流:

image

如上如所示,RecyclerView表示只会和ViewHolder进行接触,而Adapter的工作就是将Data转换为RecyclerView认识的ViewHolder,因此RecyclerView就间接地认识了Datas。

事情虽然进展愉快,但RecyclerView是个很懒惰的人,尽管Adapter已经将Datas转换为RecyclerView所熟知的View,但RecyclerView并不想自己管理些子View,因此,它雇佣了一个叫做LayoutManager的大祭司来帮其完成布局,现在,图示变成下面这样:

/pic/understand-recycler/o_1av9iv5731k3422htsd14uess1j.png

如上图所示,LayoutManager协助RecyclerView来完成布局。但LayoutManager这个大祭司也有弱点,就是它只知道如何将一个一个的View布局在RecyclerView上,但它并不懂得如何管理这些View,如果大祭司肆无忌惮的玩弄View的话肯定会出事情,所以,必须有个管理View的护法,它就是Recycler,LayoutManager在需要View的时候回向护法进行索取,当LayoutManager不需要View(试图滑出)的时候,就直接将废弃的View丢给Recycler,图示如下:

image

到了这里,有负责翻译数据的Adapter,有负责布局的LayoutManager,有负责管理View的Recycler,一切都很完美,但RecyclerView乃何等神也,它下令说当子View变动的时候姿态要优雅(动画),所以用雇佣了一个舞者ItemAnimator,因此,舞者也进入了这个图示:

image

如上,我们就是从宏观层面来对RecylerView有个大致的了解,可以看到,RecyclerView作为一个View,它只负责接受用户的各种讯息,然后将信息各司其职的分发出去。接下来我们将深入源码,看看RecyclerView都是怎么来操作各个组件工作的。

源码分析

整个RecyclerView还是相当复杂的,我画了一个与RecyclerView相关类的脑图:

image

可见RecyclerView涉及的类相当多,所以看代码的时候很容易迷失。因此我们需要抽丝剥茧,按照主线来进行分析。

既然RecyclerView是一个View,那无外乎还是measure、layout、draw几个过程,所以我们依次来看一下。

onMeasure

前面说过,RecyclerView会将测量与布局交给LayoutManager来做,并且LayoutManager有一个叫做mAutoMeasure的属性,这个属性用来控制LayoutManager是否开启自动测量,开启自动测量的话布局就交由RecyclerView使用一套默认的测量机制,否则,自定义的LayoutManager需要重写onMeasure来处理自身的测量工作。RecyclerView目前提供的几种LayoutManager都开启了自动测量,所以这里我们关注一下自动测量部分的逻辑:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. if (mLayout.mAutoMeasure) {
  2. final int widthMode = MeasureSpec.getMode(widthSpec);
  3. final int heightMode = MeasureSpec.getMode(heightSpec);
  4. final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
  5. && heightMode == MeasureSpec.EXACTLY;
  6. mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
  7. if (skipMeasure || mAdapter == null) {
  8. return;
  9. }
  10. if (mState.mLayoutStep == State.STEP_START) {
  11. dispatchLayoutStep1();
  12. }
  13. mLayout.setMeasureSpecs(widthSpec, heightSpec);
  14. mState.mIsMeasuring = true;
  15. dispatchLayoutStep2();
  16. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
  17. if (mLayout.shouldMeasureTwice()) {
  18. mLayout.setMeasureSpecs(
  19. MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
  20. MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
  21. mState.mIsMeasuring = true;
  22. dispatchLayoutStep2();
  23. // now we can get the width and height from the children.
  24. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
  25. }
  26. }

</pre>

自动测量的原理如下:

当RecyclerView的宽高都为EXACTLY时,可以直接设置对应的宽高,然后返回,结束测量。

如果宽高的测量规则不是EXACTLY的,则会在onMeasure()中开始布局的处理,这里首先要介绍一个很重要的类:RecyclerView.State ,这个类封装了当前RecyclerView的有用信息。State的一个变量mLayoutStep表示了RecyclerView当前的布局状态,包括STEP_START、STEP_LAYOUT 、 STEP_ANIMATIONS三个,而RecyclerView的布局过程也分为三步,其中,STEP_START表示即将开始布局,需要调用dispatchLayoutStep1来执行第一步布局,接下来,布局状态变为STEP_LAYOUT,表示接下来需要调用dispatchLayoutStep2里进行第二步布局,同理,第二步布局后状态变为STEP_ANIMATIONS,需要执行第三步布局dispatchLayoutStep3。

这三个步骤的工作也各不相同,step1负责记录状态,step2负责布局,step3则与step1进行比较,根据变化来触发动画。

RecyclerView将布局划分的如此细致必然是有其原因的,在开启自动测量模式的情况,RecyclerView是支持WRAP_CONTENT属性的,比如我们可以很容易的在RecyclerView的下面放置其它的View,RecyclerView会根据子View所占大小动态调整自己的大小,这时候,RecyclerView就会将子控件的measure与layout提前到Recycler的onMeasure中,因为它需要确定子空间的大小与位置后,再来设置自己的大小。所以这时候就会在onMeasure中完成step1与step2。否则,就需要在onLayout中去完成整个布局过程。

综上,整个mLayout.mAutoMeasure就是在做前两步的布局,可见RecylerView的measure与layout是紧密相关的,所以我们来赶快瞧一瞧RecyclerView是如何layout的。

onLayout

我们直接看下onLayout的代码:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  2. TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
  3. dispatchLayout();
  4. TraceCompat.endSection();
  5. mFirstLayoutComplete = true;
  6. }

</pre>

直接追进dispatchLayout:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. void dispatchLayout() {
  2. ...
  3. mState.mIsMeasuring = false;
  4. if (mState.mLayoutStep == State.STEP_START) {
  5. dispatchLayoutStep1();
  6. mLayout.setExactMeasureSpecsFrom(this);
  7. dispatchLayoutStep2();
  8. } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||mLayout.getHeight() != getHeight()) {
  9. // First 2 steps are done in onMeasure but looks like we have to run again due to
  10. // changed size.
  11. mLayout.setExactMeasureSpecsFrom(this);
  12. dispatchLayoutStep2();
  13. } else {
  14. // always make sure we sync them (to ensure mode is exact)
  15. mLayout.setExactMeasureSpecsFrom(this);
  16. }
  17. dispatchLayoutStep3();
  18. }

</pre>

通过查看dispatchLayout的代码正好验证了我们前文关于RecyclerView的layout三步走原则,如果在onMeasure中已经完成了step1与step2,则只会执行step3,否则三步会依次触发。接下来我们一步一步的进行分析

dispatchLayoutStep1

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private void dispatchLayoutStep1(){
  2. ...
  3. if (mState.mRunSimpleAnimations) {
  4. int count = mChildHelper.getChildCount();
  5. for (int i = 0; i < count; ++i) {
  6. final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
  7. final ItemHolderInfo animationInfo = mItemAnimator
  8. .recordPreLayoutInformation(mState, holder,
  9. ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
  10. holder.getUnmodifiedPayloads());
  11. mViewInfoStore.addToPreLayout(holder, animationInfo);
  12. ...
  13. }
  14. }
  15. ...
  16. mState.mLayoutStep = State.STEP_LAYOUT;
  17. }

</pre>

step的第一步目的就是在记录View的状态,首先遍历当前所有的View依次进行处理,mItemAnimator会根据每个View的信息封装成一个ItemHolderInfo,这个ItemHolderInfo中主要包含的就是当前View的位置状态等。然后ItemHolderInfo 就被存入mViewInfoStore中,注意这里调用的是mViewInfoStore的addToPreLayout方法,我们追进:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
  2. InfoRecord record = mLayoutHolderMap.get(holder);
  3. if (record == null) {
  4. record = InfoRecord.obtain();
  5. mLayoutHolderMap.put(holder, record);
  6. }
  7. record.preInfo = info;
  8. record.flags |= FLAG_PRE;
  9. }

</pre>

addToPreLayout方法中会根据holder来查询InfoRecord信息,如果没有,则生成,然后将info信息赋值给InfoRecord的preInfo变量。最后标记FLAG_PRE信息,如此,完成函数。

所以纵观整个layout的第一步,就是在记录当前的View信息,因为进入第二步后,View的信息就将被改变了。我们来看第二步:

dispatchLayoutStep2

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private void dispatchLayoutStep2() {
  2. ...
  3. mLayout.onLayoutChildren(mRecycler, mState);
  4. ...
  5. mState.mLayoutStep = State.STEP_ANIMATIONS;
  6. }

</pre>

layout的第二步主要就是真正的去布局View了,前面也说过,RecyclerView的布局是由LayoutManager负责的,所以第二步的主要工作也都在LayoutManager中,由于每种布局的方式不一样,这里我们以常见的LinearLayoutManager为例。我们看其onLayoutChildren方法:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

  2. ...

  3. if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||

  4. mPendingSavedState != null) {

  5. updateAnchorInfoForLayout(recycler, state, mAnchorInfo);

  6. }

  7. ...

  8. if (mAnchorInfo.mLayoutFromEnd) {

  9. firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :

  10. LayoutState.ITEM_DIRECTION_HEAD;

  11. } else {

  12. firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :

  13. LayoutState.ITEM_DIRECTION_TAIL;

  14. }

  15. ...

  16. onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

  17. ...

  18. if (mAnchorInfo.mLayoutFromEnd) {

  19. ...

  20. } else {

  21. // fill towards end

  22. updateLayoutStateToFillEnd(mAnchorInfo);

  23. fill(recycler, mLayoutState, state, false);

  24. ...

  25. // fill towards start

  26. updateLayoutStateToFillStart(mAnchorInfo);

  27. ...

  28. fill(recycler, mLayoutState, state, false);

  29. ...

  30. }

  31. ...

  32. }

</pre>

整个onLayoutChildren过程还是很复杂的,这里我尽量省略了一些与流程关系不大的细节处理代码。整个onLayoutChildren过程可以大致整理如下:

  • 找到anchor点
  • 根据anchor一直向前布局,直至填充满anchor点前面的所有区域
  • 根据anchor一直向后布局,直至填充满anchor点后面的所有区域

anchor点的寻找是由updateAnchorInfoForLayout函数负责的:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
  2. AnchorInfo anchorInfo) {
  3. ...
  4. if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
  5. return;
  6. }
  7. ...
  8. anchorInfo.assignCoordinateFromPadding();
  9. anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
  10. }

</pre>

函数内首先通过子view来获取anchor,如果没有获取到,就根据就取头/尾点来作为anchor。所以这里我们主要关注updateAnchorFromChildren函数:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
  2. RecyclerView.State state, AnchorInfo anchorInfo) {
  3. if (getChildCount() == 0) {
  4. return false;
  5. }
  6. final View focused = getFocusedChild();
  7. if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
  8. anchorInfo.assignFromViewAndKeepVisibleRect(focused);
  9. return true;
  10. }
  11. if (mLastStackFromEnd != mStackFromEnd) {
  12. return false;
  13. }
  14. View referenceChild = anchorInfo.mLayoutFromEnd
  15. ? findReferenceChildClosestToEnd(recycler, state)
  16. : findReferenceChildClosestToStart(recycler, state);
  17. if (referenceChild != null) {
  18. anchorInfo.assignFromView(referenceChild);
  19. ...
  20. return true;
  21. }
  22. return false;
  23. }

</pre>

updateAnchorFromChildren内部做的事情也很容易理解,首先寻找被focus的child,找到的话以此child作为anchor,否则根据布局的方向寻找最合适的child来作为anchor,如果找到则将child的信息赋值给anchorInfo,其实anchorInfo主要记录的信息就是view的物理位置与在adapter中的位置。找到后返回true,否则返回false则交由上一步的函数做处理。

综上,刚刚的所追踪的代码都是在寻找anchor点。在我们寻找后,LinearLayoutManager还给了我们更改anchor的时机,就是onAnchorReady函数,我们可以继承LinearLayoutManager 来重写onAnchorReady方法,就可以实现某些特定的功能,比如进入RecyclerView时定位在某一项等等。

总之,我们现在找到了anchor信息,接下来就是根据anchor来布局了。无论从上到下还是从下到上布局,都调用的是fill方法,我们进入fill方法来查看一番:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
  2. RecyclerView.State state, boolean stopOnFocusable) {
  3. final int start = layoutState.mAvailable;
  4. if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
  5. recycleByLayoutState(recycler, layoutState);
  6. }
  7. int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
  8. LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
  9. while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
  10. layoutChunk(recycler, state, layoutState, layoutChunkResult);
  11. ...
  12. }
  13. return start - layoutState.mAvailable;
  14. }

</pre>

这里同样省略了很多代码,我们关注重点:

首先比较重要的函数是recycleByLayoutState,这个函数就厉害了,它会根据当前信息对不需要的View进行回收:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
  2. if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
  3. ...
  4. } else {
  5. recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
  6. }
  7. }

</pre>

我们继续追进recycleViewsFromStart:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
  2. final int limit = dt;
  3. final int childCount = getChildCount();
  4. if (mShouldReverseLayout) {
  5. ...
  6. } else {
  7. for (int i = 0; i < childCount; i++) {
  8. View child = getChildAt(i);
  9. if (mOrientationHelper.getDecoratedEnd(child) > limit
  10. || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
  11. recycleChildren(recycler, 0, i);
  12. return;
  13. }
  14. }
  15. }
  16. }

</pre>

这个函数的作用就是遍历所有的子View,找出逃离边界的View进行回收,回收函数我们锁定在recycleChildren里,而这个函数最后又会调到removeAndRecycleViewAt:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. public void removeAndRecycleViewAt(int index, Recycler recycler) {
  2. final View view = getChildAt(index);
  3. removeViewAt(index);
  4. recycler.recycleView(view);
  5. }

</pre>

这个函数首先调用removeViewAt函数,这个函数的作用是将View从RecyclerView中移除, 紧接着我们看到,是recycler执行了view的回收逻辑。这里我们暂且打住,关于recycler我们会单独进行说明,这里我们只需要理解,在fill函数的一开始会去回收逃离出屏幕的view。我们再次回到fill函数,关注这里:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
  2. layoutChunk(recycler, state, layoutState, layoutChunkResult);
  3. ...
  4. }

</pre>

这段代码很容易理解,只要还有剩余空间,就会执行layoutChunk方法:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
  2. LayoutState layoutState, LayoutChunkResult result) {
  3. View view = layoutState.next(recycler);
  4. ...
  5. LayoutParams params = (LayoutParams) view.getLayoutParams();
  6. if (layoutState.mScrapList == null) {
  7. if (mShouldReverseLayout == (layoutState.mLayoutDirection
  8. == LayoutState.LAYOUT_START)) {
  9. addView(view);
  10. } else {
  11. addView(view, 0);
  12. }
  13. } else {
  14. ...
  15. }
  16. ...
  17. layoutDecoratedWithMargins(view, left, top, right, bottom);
  18. ...
  19. }

</pre>

我们首先看到,layoutState的next方法返回了一个View,凭空变出一个View,好神奇,追进去看一下:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. View next(RecyclerView.Recycler recycler) {
  2. ...
  3. final View view = recycler.getViewForPosition(mCurrentPosition);
  4. return view;
  5. }

</pre>

可见,view的获取逻辑也由recycler来负责,所以,这里我们同样打住,只需要清楚recycler可以根据位置返回一个view即可。

再回到layoutChunk看一下对刚刚生成的view作何处理:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. if (mShouldReverseLayout == (layoutState.mLayoutDirection
  2. == LayoutState.LAYOUT_START)) {
  3. addView(view);
  4. } else {
  5. addView(view, 0);
  6. }

</pre>

很明显调用了addView方法,虽然这个方法是LayoutManager的,但这个方法最终会多次辗转调用到RecyclerView的addView方法,将view添加在RecyclerView中。

综上,我们就梳理了整个第二步布局的过程,此过程完成了子View的测量与布局,任务还是相当繁重。

dispatchLayoutStep3

接下来,就到了布局的最后一步了,我们直接看下dispatchLayoutStep3方法:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. private void dispatchLayoutStep3() {

  2. mState.mLayoutStep = State.STEP_START;

  3. if (mState.mRunSimpleAnimations) {

  4. for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {

  5. ...

  6. final ItemHolderInfo animationInfo = mItemAnimator

  7. .recordPostLayoutInformation(mState, holder);

  8. mViewInfoStore.addToPostLayout(holder, animationInfo);

  9. }

  10. mViewInfoStore.process(mViewInfoProcessCallback);

  11. }

  12. ...

  13. }

</pre>

这一步是与第一步呼应的的,此时由于子View都已完成布局,所以子View的信息都发生了变化。我们会看到第一步出现的mViewInfoStore和mItemAnimator再次登场,这次mItemAnimator调用的是recordPostLayoutInformation方法,而mViewInfoStore调用的是addToPostLayout方法,还记得刚刚我强调的吗,之前是pre,也就是真正布局之前的状态,而现在要记录布局之后的状态,我们追进addToPostLayout:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
  2. InfoRecord record = mLayoutHolderMap.get(holder);
  3. if (record == null) {
  4. record = InfoRecord.obtain();
  5. mLayoutHolderMap.put(holder, record);
  6. }
  7. record.postInfo = info;
  8. record.flags |= FLAG_POST;
  9. }

</pre>

和第一步的addToPreLayout类似,不过这次info信息被赋值给了record的postInfo变量,这样,一个record中就包含了布局前后view的状态。

最后,mViewInfoStore调用了process方法,这个方法就是根据mViewInfoStore中的View信息,来执行动画逻辑,这又是一个可以展看很多的点,这里不做探讨,感兴趣的可以深入的看一下,会对动画流程有更直观的体会。

缓存逻辑

前面的章节对于Recycler这个类相关的操作我们都直接进行了忽略,这里我们好好的来看下RecylerView是如何工作的。

与ListView不同,RecyclerView的缓存是分为多级的,但其实整个的缓存逻辑还是很容易理解的,我们首先看一下刚刚获取View的方法getViewForPosition:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. View getViewForPosition(int position, boolean dryRun) {

  2. boolean fromScrap = false;

  3. ViewHolder holder = null;

  4. if (mState.isPreLayout()) {

  5. holder = getChangedScrapViewForPosition(position);

  6. fromScrap = holder != null;

  7. }

  8. if (holder == null) {

  9. holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);

  10. ...

  11. }

  12. if (holder == null) {

  13. final int offsetPosition = mAdapterHelper.findPositionOffset(position);

  14. final int type = mAdapter.getItemViewType(offsetPosition);

  15. if (mAdapter.hasStableIds()) {

  16. holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);

  17. }

  18. if (holder == null && mViewCacheExtension != null) {

  19. final View view = mViewCacheExtension

  20. .getViewForPositionAndType(this, position, type);

  21. ...

  22. }

  23. if (holder == null) { // fallback to recycler

  24. holder = getRecycledViewPool().getRecycledView(type);

  25. if (holder != null) {

  26. holder.resetInternal();

  27. if (FORCE_INVALIDATE_DISPLAY_LIST) {

  28. invalidateDisplayListInt(holder);

  29. }

  30. }

  31. }

  32. if (holder == null) {

  33. holder = mAdapter.createViewHolder(RecyclerView.this, type);

  34. }

  35. }

  36. //生成LayoutParams的代码 ...

  37. return holder.itemView;

  38. }

  39. }

</pre>

获取View的逻辑可以整理成如下:

  • 搜索mChangedScrap,如果找到则返回相应holder。
  • 搜索mAttachedScrap与mCachedViews,如果找到且holder有效则返回相应holder。
  • 如果设置了mViewCacheExtension,对其调用getViewForPositionAndType方法进行获取,若该方法返回结果则生成对应的holder。
  • 搜索mRecyclerPool,如果找到到则返回相应holder
  • 如果上述过程都没有找到对应的holder,则执行我们熟悉的Adapter.createViewHolder(),创建新的holder实例

综上,只要数据合法,该方法最终肯定会返回符合条件的View。这里可能大家比较关注的是这么多级别的缓存到底有什么区别,这个问题可能大家看完回收View的代码会有更深的理解:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. void recycleViewHolderInternal(ViewHolder holder) {

  2. ...

  3. if (holder.isRecyclable()) {

  4. if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED

  5. | ViewHolder.FLAG_UPDATE)) {

  6. int cachedViewSize = mCachedViews.size();

  7. if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {

  8. recycleCachedViewAt(0);

  9. cachedViewSize --;

  10. }

  11. if (cachedViewSize < mViewCacheMax) {

  12. mCachedViews.add(holder);

  13. cached = true;

  14. }

  15. }

  16. if (!cached) {

  17. addViewHolderToRecycledViewPool(holder);

  18. recycled = true;

  19. }

  20. }

  21. ...

  22. }

</pre>

View的回收并不像View的创建那么复杂,这里只涉及了两层缓存mCachedViews与mRecyclerPool,mCachedViews相当于一个先进先出的数据结构,每当有新的View需要缓存时都会将新的View存入mCachedViews,而mCachedViews则会移除头元素,并将头元素放入mRecyclerPool,所以mCachedViews相当于一级缓存,mRecyclerPool则相当于二级缓存,并且mRecyclerPool是可以多个RecyclerView共享的,这在类似于多Tab的新闻类应用会有很大的用处,因为多个Tab下的多个RecyclerView可以共用一个二级缓存。减少内存开销。

如此,就是对RecyclerView的缓存逻辑的简要分析。

与AdapterView比较

谈到RecyclerView,总避免不了与它的前辈AdapterView家族进行一撕,这里我整理了一下RecylerView与AdapterView的各自特点:

image

前面四点两位都提供了各自的实现,但也各有各自的特点:

  • 点击事件

    ListView原生提供Item单击、长按的事件, 而RecyclerView则需要使用onTouchListener,相对自己实现会比较复杂。

  • 分割线

    ListView可以很轻松的设置divider属性来显示Item之间的分割线,RecyclerView需要我们自己实现ItemDecoration,前者使用简单,后者可定制性强。

  • 布局类型

    AdapterView提供了ListView与GridView两种类型,分别对应流式布局与网格式布局。RecyclerView提供了LinearLayoutManager、GridLayoutManager与之抗衡,相对而言,使用RecyclerView来进行更换布局方式更为轻松。只需要更换一个变量即可,而对于AdapterView而言则是需要更换一个View了。

  • 缓存方式

    ListView使用了一个名为RecyclerBin的类负责试图的缓存,而Recycler则使用Recycler来进行缓存,原理上两者基本一致。

在对比了几个相近点之外,我们再分别来看一下两者的不同点,先看RecyclerView:

  • 局部刷新

    这是一个很有用的功能,在ListView中我们想局部刷新某个Item需要自己来编写刷新逻辑,而在RecyclerView中我们可以通过notifyItemChanged(position)来刷新单个Item,甚至可以通过notifyItemChanged(position, payload)来传入一个payload信息来刷新单个Item中的特定内容。

  • 动画

    作为视觉动物,我相信很多人喜欢RecylerView都和它简单的动画API有关,因为之前对ListView做动画比较困难,并且不舒服。

  • 嵌套布局

    嵌套布局也是最近比较火的一个概念,RecyclerView实现了NestedScrollingChild接口,使得它可以和一些嵌套组件很好的工作。

我们再来看ListView原生独有的几个特点:

  • 头部与尾部的支持

    ListView原生支持添加头部与尾部,虽然RecyclerView可以通过定义不同的Type来做支持,但实际应用中,如果封装的不好,是很容易出问题的,因为Adapter中的数据位置与物理数据位置发生了偏移。

  • 多选

    支持多选、单选也是ListView的一大长处,其实如果要我们自己在RecyclerView中去做支持还是需要不少代码量的。

  • 多数据源的支持

    ListView提供了CursorAdapter、ArrayAdapter,可以让我们很方便的从数据库或者数组中获取数据,这在测试的时候很有用。

综上,我们会发现RecycerView的最大特点就是灵活,正因为这种灵活,因此会牺牲了某些便利性。而AdapterView相对来讲就比较刻板,但它原生为我们提供了很多有用的方法来便于我们快速开发。ListView并不像当年的ActivityGroup,在Fragment出来后就被标记为Deprecated。两者目前还是一种互补的关系,起码在短时间内RecyclerView还并不能完全替代AdapterView,个人感觉原因有两个,一是目前太多的应用使用了ListView,并且ListView向RecyclerView转变也没有无损的方法。第二点,比如我就是想添加个头部,每个item带个点击事件这类简单的需求,ListView完全可以很轻松的胜任,没必要舍近求远来使用RecyclerView。因此,在实际应用中选择更适合自己的就好。

当然,从Google最近几次的更新来看,RecyclerView的进化还是很迅速的,而ListView则几乎没什么变动,所以RecyclerView绝对是大大的潜力股呀。

设计精巧的类

在翻看RecycleView源码的过程中也遇见了许多之前没有注意过的类,这些类都可以复用在我们的日常工作当中。这里列举出其中具有代表性的几位。

Bucket

如果一个对象有大量的是与非的状态需要表示,通常我们会使用BitMask 技术来节省内存,在 Java 中,一个 byte 类型,有 8 位(bit),可以表达 8 个不同的状态,而 int 类型,则有 32 位,可以表达 32 种状态。再比如Long类型,有64位,则可以表达64中状态。一般情况下使用一个Long已经足够我们使用了。但如果有不设上限的状态需要我们表示呢?

在ChildHelper里有一个静态内部类Bucket,基本源码如下:

<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">

  1. static class Bucket {

  2. final static int BITS_PER_WORD = Long.SIZE;

  3. final static long LAST_BIT = 1L << (Long.SIZE - 1);

  4. long mData = 0;

  5. Bucket next;

  6. void set(int index) {

  7. if (index >= BITS_PER_WORD) {

  8. ensureNext();

  9. next.set(index - BITS_PER_WORD);

  10. } else {

  11. mData |= 1L << index;

  12. }

  13. }

  14. ...

  15. }

</pre>

可以看到,Bucket是一个链表结构,当index大于64的时候,它便会去下一个Bucket去寻找,所以,Bucket可以不设上限的表示状态。

Pools

熟悉Message回收机制的朋友可能了解,在使用Message对象时最好通过Message.obtain()方法来获取,这样可以在很多情况下避免创建新对象。在使用完之后调用message.recycle()来回收消息。

谷歌为这种机制也提供了抽象的实现,就是位于v4包下Pools类, 内部接口Pool提供了acquirerelease两个方法,不过需要注意的是这个acquire方法可能返回空,毕竟Pools不是业务类,它不应该清楚对象的具体创建逻辑.

还有一点是Pools与Message类的实现机制不同,每个Message对象内部都持有一个引用下一个message的指针,相当于一个链表结构,而Pool的实现类SimplePool中使用的是数组.

Pool机制在 RecycleView 中有如下几处应用:

  • RecycleView将item的增删改封装为UpdateOp类。

  • ViewInfoStore类中静态内部类InfoRecord

缺点

RecyclerView也不是万能的,它的灵活性也是有一定限制的,比如我就遇到了一不是很好解决的问题:

Recyler的缓存级别是一个Item的整个View,而我们没办法自定义缓存级别,这样说比较抽象,举个例子,我的某些Item的某个子View加载很耗时,所以我希望我在上下滑动的时候,Item的其它View是可以被回收利用的,但这个加载很耗时的View是不要重复使用的。即我希望用空间换取时间来获取滑动的流畅性。当然,这样的需求不常见,RecyclerView也不能很好的满足这一点。

总结

RecyclerView也应该算作一个明星控件了,自从其诞生开始就备受欢迎,仔细的学习也能让我们在工作中更容易的、更恰当的使用。本文也只是分析了RecyclerView的一部分,关于动画、滑动、嵌套滑动等等还需要大家自行去研究。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容