RecyclerView源码解析

一.基本介绍

在平时的开发中我们或多或少地接触过ListView,所以我们对ListView并不陌生。不过ListView存在某些缺点,所以谷歌又在ListView的基础上推出了RecyclerView。记得RecyclerView刚推出时,网上许多文章都介绍了它的优点,甚至有人说RecyclerView可以完全取代ListView,我觉得这个说法太绝对了,虽然RecyclerView在ListView的基础上优化了,但是实际应用中要使用哪个控件,还是要根据实际情况来看的。接下来我们从源码的角度了解RecyclerView,看看其与ListView的而区别。

二.源码解析

在看RecyclerView源码中的方法之前,我们有必要先看看RecyclerView源码中的内部类,这将更好地帮助我们看懂源码。所以我们先来看看其内部类。

·LayoutManager

LayoutManager 负责测量和摆放 item view,可以帮助我们实现普通的竖直,水平,网格等列表,在ListView中不存在这个内部类,所以使用ListView时,我们一般只能实现竖直方向列表。
在这个内部类中的大部分方法是实现测量child的大小,我们这里不展开说。我们只说其中的一个方法,之所以说这个方法是因为我第一次看的时候,理解错了,之后再看的时候虽然知道了实际的用途,但是却存在疑问,到现在我也不太理解为什么要这么设置。先来看看这个方法吧。

 public void setAutoMeasureEnabled(boolean enabled) {
            mAutoMeasure = enabled;
        }

这个方法很简单,只是设置mAutoMeasure的值,而mAutoMeasure只是用来标记是否自动测量(将AutoMeasure直接翻译为自动测量了)。如果是自动测量,那么就将所有的测量工作都交给LayoutManager来做,这好像就是本来设置LayoutManager的目的。而如果mAutoMeasure值为false,也就是不是自动测量了,根据源码里面的说明,此时就应该由RecyclerView自己对child进行测量了。但是仔细阅读源码和理解之后,发现无论是不是自动测量,最终测量都是由LayoutManager来做。
上面所说的问题也是我存在疑惑的地方,看起来mAutoMeasure这个变量并不是必需的,因为本来layoutManager的职责就包括测量item view。
所以请有理解这个设计意图的读者能帮我解惑,或者我上面的理解有错的地方,恳请指正。

·Adapter

说起Adapter,我们都不陌生,因为在ListView里面我们就有使用到Adapter,不过,RecyclerView对Adapter进行了改进。
在ListView中,我们可能会在Adapter写一个内部类ViewHolder,减少不必要的findViewById操作。当然我们能自己选择要不要做这一步优化。但是在RecyclerView中的Adapter源码中存在下面的方法。

 public final VH createViewHolder(ViewGroup parent, int viewType) {
            TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
            final VH holder = onCreateViewHolder(parent, viewType);
            holder.mItemViewType = viewType;
            TraceCompat.endSection();
            return holder;
        }

从这里看出RecyclerView强制我们必须使用ViewHolder,这样做有什么好处呢?ViewHolder缓存了相关的View,当我们设置数据时,可以重复利用这些已经缓存的子View去显示不同item的数据,避免不必要的findViewById操作。

·Recycler

Recycler主要管理废弃或分离的item的复用。这个类主要和LayoutManager配合使用。
接下来介绍这个类中几个关键的变量和方法。

        //与RecyclerView绑定,但是已经标记了要移除或复用的ViewHolder列表
        ArrayList<ViewHolder> mAttachedScrap;

        //与RecyclerView分离的ViewHolder列表
        ArrayList<ViewHolder> mChangedScrap;

        //ViewHolder缓存列表    
        ArrayList<ViewHolder> mCachedViews;

        //提供复用ViewHolder池
        RecycledViewPool mRecyclerPool;

        //开发者控制的ViewHolder缓存
        ViewCacheExtension mViewCacheExtension;

        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }   

getViewForPosition(int):获取某个位置需要展示的View,先检查是否有可复用的View,如果没有就创建新的View并返回。
上面的具体过程如下:
a.检查mChangedScrap,若匹配到则返回相应的ViewHolder。
b.检查mAttachedScrap,操作同上。
c.检查mViewCacheExtension,操作同上。
d.检查mRecycledPool,操作同上。
e.如果上述操作都没有成功,则执行Adapter.createViewHolder(),新建ViewHolder实例。
f.返回holder.itemView。

为什么会按照上面次序检查呢?这涉及到RecyclerView中的缓存。一般系统预先提供了两级缓存,如果有特殊需求,可实现三级缓存。这三级缓存如下:
·第一级缓存:这级缓存主要用到的存储容器有mCacheViews,mAttachedScrap和mChangedScrap。
·第二级缓存:这级缓存不是系统预先提供的,而是利用ViewCacheExtension充当附加的缓存池,当RecyclerView从第一级缓存找不到需要的View时,将会从ViewCacheExtension中找,这个缓存时由开发者维护的,如果开发者不显示设置它,则默认不会启用。只有当我们有特殊需求时(例如在调用系统的缓存池前返回一个特定的视图),才会使用这级缓存。
·第三级缓存:这级的缓存主要用到最强大的缓存器RecycledViewPool。

·RecycledViewPool

上面说到RecycledViewPool是最强大的缓存器,之所以这么说是因为RecycledViewPool使我们能在RecycledView之间共享ViewHolder,所以其一般能绑定多个Adapter。

说完RecycledView中的内部类,接下来说说其内部的方法。RecycledView是一个控件,所以我们还是按照View的绘制流程来看RecycledView的源码。

·onMeasure

protected void onMeasure(int widthSpec, int heightSpec) {
       //如果LayoutManager为空则先初始化
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            ...
                //如果RecycledView是第一次进行测量则调用 dispatchLayoutStep1()
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            ...
            dispatchLayoutStep2();

            //只要RecycledView中有一个child无法得到确切的宽高,就要重新测量
            if (mLayout.shouldMeasureTwice()) {
                  ...
                dispatchLayoutStep2();

            }
        } else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
           ...
        }
    }

上面只贴出了onMeasure的部分代码,从上面我们可以看出在测量过程中会调用dispatchLayoutStep1()和dispatchLayoutStep2()方法,所以接下来我们分别看看这两个方法。

     /**
      根据源码的说明,我们可以知道这个方法会找出那些不需要移除的child,然后预先设置它们的位置
     */
     private void dispatchLayoutStep1() {
         ...
        if (mState.mRunSimpleAnimations) {      
            int count = mChildHelper.getChildCount();
            //遍历找出不需要移除的child
            for (int i = 0; i < count; ++i) {
                 ....
                }
            }
        //预先设置不需要移除的child的位置
        if (mState.mRunPredictiveAnimations) {
              ...
            mLayout.onLayoutChildren(mRecycler, mState);
              ...
            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                final View child = mChildHelper.getChildAt(i);
                final ViewHolder viewHolder = getChildViewHolderInt(child);
                if (viewHolder.shouldIgnore()) {
                    continue;
                }
                if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                    int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
                    boolean wasHidden = viewHolder
                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    if (!wasHidden) {
                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                    }
                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                            mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                    if (wasHidden) {
                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                    } else {
                        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                    }
                }
            }
            ...
        } else {
            clearOldPositions();
        }
       ...
    }
 /**
    在这个方法里面才真正根据View测得的状态去设置其位置,这个方法可能会被多次调用(比如当Adapter中的数据发生改变),不过经过这个方法
    之后,View的宽高完全确定下来了。
*/
 private void dispatchLayoutStep2() {
        eatRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
    }

·onLayout():在onLayout中会调用dispatchLayout()方法, dispatchLayout()实际就像layoutChildren()的封装,在这个方法完成对各个child位置的设置。具体实现还是调用了上面所说的dispatchLayoutStep1()和dispatchLayoutStep2()。在确保了这两个方法已经调用之后,还会调用dispatchLayoutStep3()。
dispatchLayoutStep3()主要是保存所有View相关的动画信息,然后触发动画和做一些必要的清理。

·onDraw():只是遍历了所有的child并调用他们的onDraw()方法。

关于RecycledView的部分源码介绍完了,通过学习源码和日常开发的使用,我们都能发现RecycledView和ListView的区别,虽然RecycledView比ListView性能好了一些,在布局方面也灵活许多,但个人觉得RecycledView在使用上比ListView麻烦了一些。所以实际开发中选择哪一个并不是绝对的,根据实际选择合适的就行了,也不用一昧的抬高RecycledView的地位。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值