Android应用篇 - RecyclerView 设计分析

RecyclerView 目前已成为 ListView,GridView 甚至 HorizontalListView 的高阶替代品。最初从开源项目 Telegram 中就见到 RecyclerView 的影子,当时还惊讶于它的聊天列表怎么那么顺滑。而且目前 github 出现了越来越多的基于 RecyclerView 的开源库,可见它的受欢迎程度。今天这篇文章就来分析一下 RecyclerView 的设计。

目录:

  1. RecyclerView 与 ListView 的对比
  2. RecyclerView 整体结构
  3. RecyclerView 绘制流程
  4. RecyclerView 缓存机制
  5. RecyclerView 使用优化

1. RecyclerView 与 ListView 的对比

上一篇文章分析了 ListView 的设计:Android应用篇 - ListView 设计分析,但是没有对比就没有伤害,来简单对比下 RecyclerView 和 ListView。

ListView 相比 RecyclerView 有一些优点:

  • addHeaderView(),addFooterView() 添加头视图和尾视图。
  • 通过 "android:divider" 设置自定义分割线。
  • setOnItemClickListener() 和 setOnItemLongClickListener() 设置点击事件和长按事件。

这些功能在 RecyclerView 中都没有直接的接口,要自己实现 (虽然实现起来很简单),因此如果只是实现简单的显示功能,ListView 无疑更简单,不过目前 github 基于 RecyclerView 的开源库这么多,这已经不能作为 ListView 的优点了。

RecyclerView 相比 ListView,有一些明显的优点:

  • 默认已经实现了 View 的复用,不需要类似 if(convertView == null) 的实现,而且回收机制更加完善。
  • 默认支持局部刷新。
  • 容易实现添加 item、删除 item 的动画效果。
  • 通过支持水平、垂直和变革列表及其他更复杂形式,而 ListView 只支持具体某一种。
  • 容易实现拖拽、侧滑删除等功能。

RecyclerView 是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。

2. RecyclerView 整体结构

RecyclerView 是在 22.1.0 开始添加到开发包中的。RecyclerView 继承于 ViewGroup 并实现了 ScrollingView 和 NestedScrollingChild2 接口。已知的直接子类有 BaseGridView 和 WearableRecycler,间接子类有 HorizontalGridView 和 VerticalGridView。

上图是 RecyclerView 的几个角色:LayoutManager,RecyclerView,ViewHolder,Adapter,DataSource。

3. RecyclerView 绘制流程

对于一个自定义 ViewGroup,主要从 onMeasure(),onLayout() 和 onDraw() 来分析。

  • 3.1 onMeasure()
 @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        // layoutManager 没有设置的话,直接走 default 的方法,所以会为空白
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            // 如果测量是绝对值,则跳过 measure 过程直接走 layout
            if (skipMeasure || mAdapter == null) {
                return;
            }
           // mLayoutStep 默认值是 State.STEP_START
            if (mState.mLayoutStep == State.STEP_START) {
                // 分发第一步 layout 
                dispatchLayoutStep1();
                // 执行完 dispatchLayoutStep1() 后是 State.STEP_LAYOUT
            }
            
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();

            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(
                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                // 真正执行 LayoutManager 绘制的地方
                dispatchLayoutStep2();
                // 执行完后是 State.STEP_ANIMATIONS
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        } else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            // custom onMeasure
            if (mAdapterUpdateDuringMeasure) {
                eatRequestLayout();
                processAdapterUpdatesAndSetAnimationFlags();

                if (mState.mRunPredictiveAnimations) {
                    mState.mInPreLayout = true;
                } else {
                    mAdapterHelper.consumeUpdatesInOnePass();
                    mState.mInPreLayout = false;
                }
                mAdapterUpdateDuringMeasure = false;
                resumeRequestLayout(false);
            }

            if (mAdapter != null) {
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            eatRequestLayout();
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            resumeRequestLayout(false);
            mState.mInPreLayout = false; // clear
        }
    }

一步步来看,先看看:

        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }

如果没有设置 LayoutManager,则直接走 defaultOnMeasure():

    void defaultOnMeasure(int widthSpec, int heightSpec) {
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));

        setMeasuredDimension(width, height);
    }

可以看到这里的 chooseSize() 方法其实就是根据宽高的 Mode 得到相应的值后直接调用 setMeasuredDimension() 设置宽高了,发现这里其实是没有进行 child 的测量就直接 return 结束了onMeasure() 的过程,这也就解释了为什么我们没有设置 LayoutManager 会导致显示空白了。然后后面的代码会做一个这样的判断:

if (mLayout.mAutoMeasure) {
} else {
}

mAutoMeasure 这个值,LinearLayoutManager 还是其他两个 Manager,默认值都是 true,所以往 mLayout.mAutoMeasure 里面的分支看:

            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            // 如果测量是绝对值,则跳过 measure 过程直接走 layout
            if (skipMeasure || mAdapter == null) {
                return;
            }

这种情况直接走 layout 流程的话,layout 中会进行测绘。后面的代码开始 mLayoutStep 的判断了,mLayoutStep 的默认值是 State.STEP_START,并且每次绘制流程结束后,会重置为 State.STEP_START:

            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }

进入 dispatchLayoutStep1():

    /**
     * 第一步 layout 工作主要是:
     * - 处理 Adapter 的更新
     * - 决定那些动画需要执行
     * - 保存当前 View 的信息
     * - 如果必要的话,执行上一个 Layout 的操作并且保存他的信息
     */
    private void dispatchLayoutStep1() {
    }

接下来就是我们的真正执行 LayoutManager 绘制的地方 dispatchLayoutStep2():

    private void dispatchLayoutStep2() {
        // ...
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        // 重写的 getItemCount() 方法
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
    }

mLayout.onLayoutChildren(mRecycler, mState) 这句代码,就可以看出,RecyclerView 将绘制工作都交给 LayoutManager 了。来看看 LinearLayoutManager 的 onLayoutChildren() 实现:

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
        }

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // resolve layout direction
        resolveShouldLayoutReverse();

        // ...
        mLastStackFromEnd = mStackFromEnd;
        if (DEBUG) {
            validateChildOrder();
        }
    }

很复杂,但是根据注释可以总结为:

  • 先寻找页面当前的锚点。
  • 以这个锚点未基准,向上和向下分别填充。
  • 填充完后,如果还有剩余的可填充大小,再填充一次。
  • 3.2 onLayout()
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

进入 dispatchLayout() 看看:

    void dispatchLayout() {
        // 适配器为空则返回
        if (mAdapter == null) {
            return;
        }
        // LayoutManager 为空则返回
        if (mLayout == null) {
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
                mLayout.getHeight() != getHeight()) {
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

这里的代码就比较好理解了,并且上面提到的问题也就迎刃而解了,当我们给 RecyclerView 设置固定的宽高的时候,onMeasure() 是直接跳过了执行,那么为什么子 View 仍然能绘制出来。这里可以看到,如果 onMeasure() 没有执行,mState.mLayoutStep == State.STEP_START 就成立,所以仍然会执行 dispatchLayoutStep1(),dispatchLayoutStep2()。也就对应的会绘制子 View。而后面的注释也比较清楚,由于我们在 layout 的时候改变了宽高,也会导致 dispatchLayoutStep2(),也就是子 View 的重新绘制。如果上面情况都没有,那么 onLayout() 的作用就仅仅是 dispatchLayoutStep3(),而 dispatchLayoutStep3() 方法的作用除了重置一些参数,外还和执行动画有关。

    private void dispatchLayoutStep3() {
        if (mState.mRunSimpleAnimations) {
            // Step 3: Find out where things are now, and process change animations.
            // traverse list in reverse because we may call animateChange in the loop which may
            // remove the target view holder.
            // 需要动画的情况。找出ViewHolder现在的位置,并且处理改变动画。最后触发动画。
            }
            // Step 4: Process view info lists and trigger animations
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
    }

4. RecyclerView 缓存机制

和 ListView 一样,对于 RecyclerView,View 的缓存机制是不得不了解一下的。盗用一张图:

可以看到,一共有好几级缓存,RecyclerView 内部也有一个 Recycler 类:


public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    private ArrayList<ViewHolder> mChangedScrap = null;
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    private final List<ViewHolder>
            mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

    private int mViewCacheMax = DEFAULT_CACHE_SIZE;

    private RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;

    private static final int DEFAULT_CACHE_SIZE = 2;

    // ...
    View getViewForPosition(int position, boolean dryRun) {
        if (position < 0 || position >= mState.getItemCount()) {
            throw new IndexOutOfBoundsException("Invalid item position " + position
                    + "(" + position + "). Item count:" + mState.getItemCount());
        }
        boolean fromScrap = false;
        ViewHolder holder = null;
        // 0) 如果有一个变更的废弃 view,先从这里面找到一个
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrap = holder != null;
        }
        // 1) 通过索引找到一个废弃的 view
        if (holder == null) {
            holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
            if (holder != null) {
                if (!validateViewHolderForOffsetPosition(holder)) {
                    // recycle this scrap
                    // ...
                    holder = null;
                } else {
                    fromScrap = true;
                }
            }
        }
        // 如果还为空
        if (holder == null) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                        + "position " + position + "(offset:" + offsetPosition + ")."
                        + "state:" + mState.getItemCount());
            }

            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2) Find from scrap via stable ids, if exists
            if (mAdapter.hasStableIds()) {
                holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                if (holder != null) {
                    // update position
                    holder.mPosition = offsetPosition;
                    fromScrap = true;
                }
            }
            // 从 ViewCacheExtension 中找
            if (holder == null && mViewCacheExtension != null) {
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    // ...
                }
            }
            if (holder == null) { // fallback to recycler
                // 从 RecycledViewPool 中找
                holder = getRecycledViewPool().getRecycledView(type);
                // ...
            }
            if (holder == null) {
                // 最后还为空,则 createViewHolder()
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
            }
        }
        // ...

        boolean bound = false;
        if (mState.isPreLayout() && holder.isBound()) {
            // do not update unless we absolutely have to.
            holder.mPreLayoutPosition = position;
        } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
            if (DEBUG && holder.isRemoved()) {
                throw new IllegalStateException("Removed holder should be bound and it should"
                        + " come here only in pre-layout. Holder: " + holder);
            }
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            holder.mOwnerRecyclerView = RecyclerView.this;
            // 调用 bindViewHolder()
            mAdapter.bindViewHolder(holder, offsetPosition);
            attachAccessibilityDelegate(holder.itemView);
            bound = true;
            if (mState.isPreLayout()) {
                holder.mPreLayoutPosition = position;
            }
        }

        // ...
        return holder.itemView;
    }
    
    // ...
}

这边就看看 getViewForPosition() 这个重要方法,mAttachedScrap,mCacheViews 只是对 View 的复用,并且不区分 type,ViewCacheExtension,RecycledViewPool 是对于 ViewHolder 的复用,区分 type。

5. RecyclerView 使用优化

由于篇幅问题,可以看看这篇文章:RecyclerView性能优化

发布了126 篇原创文章 · 获赞 215 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/88637035
今日推荐