美文网首页Android开发社区Android UI程序员
最全面的RecyclerView源码解析(一)

最全面的RecyclerView源码解析(一)

作者: JJQ3 | 来源:发表于2017-04-17 00:16 被阅读962次

    相信很多人用RecyclerView已经很久了,但还是不得不感叹 RecyclerView的强大,性能、扩展性等方面都很强大。网上看了很多源码方面对RecyclerView,觉得还不够全面,而且自己不走一遍源码总感觉会很容易忘记。 打开RecyclerView类,发现有11090行代码,千万不要震惊,慢慢看。 整体看了下RecyclerView的架构,你会惊奇于这个优雅的设计,高度解耦,灵活性很强,给开发者一种插拔式的体验,使用者只要通过设置不同的LayoutManager, ItemDecoration, ItemAnimator就可以实现各种各样的效果了。

    Recycler

    打开RecyclerView的结构,复杂,我们就从上往下说好了。首先来分析Recycler这个内部类,Recycler是整个RecyclerView的精髓所在,那么Recycler到底是什么呢:
    Recycler的职责是管理那些已经废弃了的或者从RecyclerView中分离的item view用于复用。
    废弃的View是指那些仍然依附于RecyclerView但是已经被标记为可以被移除或者复用的View。
    Recycler典型的用法就是当LayoutManager去获取Adapter中的某一项View的时候,如果这个View失效了,则需要重新绑定View,当复用的View是有效的话,View就会被直接被复用。有效的View如果不主动调用requestLayout,那么该View不需要重新测量就可以被复用。
    说了一大堆,还是边看代码边解释更加清晰一点,首先来看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;
    

    先来看RecycledViewPool和ViewCacheExtension这两个类:
    RecycledViewPool让开发者可以在多个RecyclerView之间共享View。
    如果你想要垮RecyclerView复用View,创建一个RecycledViewPool实例,然后调用setRecycledViewPool(RecycledViewPool)方法就可以了。
    RecyclerView会自动创建一个RecycledViewPool的实例。
    有了RecycledViewPool的存在,就能很大程度上减少View的创建,提高性能。
    先看下面这两个方法:

    private SparseArray<ArrayList<ViewHolder>> mScrap =
                    new SparseArray<ArrayList<ViewHolder>>();
            private SparseIntArray mMaxScrap = new SparseIntArray();
    
    //此处省略部分代码 
    //……
    public void setMaxRecycledViews(int viewType, int max) {
                mMaxScrap.put(viewType, max);
                final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
                if (scrapHeap != null) {
                    while (scrapHeap.size() > max) {
                        scrapHeap.remove(scrapHeap.size() - 1);
                    }
                }
            }
    
    public ViewHolder getRecycledView(int viewType) {
                final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
                if (scrapHeap != null && !scrapHeap.isEmpty()) {
                    final int index = scrapHeap.size() - 1;
                    final ViewHolder scrap = scrapHeap.get(index);
                    scrapHeap.remove(index);
                    return scrap;
                }
                return null;
            }
    

    可以看到,mScrap是一个《viewType, List>的映射,mMaxScrap是一个《viewType, maxNum>的映射,我们可以调用setMaxRecycledViews方法来设置每种viewType的view容量。从源码可以看出,如果viewType类型的list的size大于制定的最大数字的话,会先从列表的末尾开始丢弃超出的部分。调用getRecycledView(int viewType)方法呢,可以将scrapHeap中的最后一项移除并返回viewType对应的List的末尾项。这里需要注意的是,因为是跨RecyclerView进行操作,所以要特别注意对于同一个RecycledViewPool,对ViewType的定义要统一,因为这里是根据viewType来取ViewHolder的。

    再来看ViewCacheExtension这个类:

    public abstract static class ViewCacheExtension {
            abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
        }
    

    ViewCacheExtension是一个帮助类,给开发者提供了一个可以由他们自己控制View缓存的这么一个类。当调用Recycler的getViewForPosition(int position)方法等时候,Recycler会先去检查attached scrap和一级缓存来这个View,如果都找不到匹配的View的话,Recycler会调用ViewCacheExtension的getViewForPositionAndType方法检查,如果还是没有,再去检查RecycledViewPool。
    需要注意的是:Recycler不会在这个类中做任何缓存View的操作,是否需要缓存View由开发者自己控制。
    然后回到Recycler,Recycler最终目的还是要去获取到来看这个方法:

     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) If there is a changed scrap, try to find from there
                if (mState.isPreLayout()) {
                    holder = getChangedScrapViewForPosition(position);
                    fromScrap = holder != null;
                }
                // 1) Find from scrap by position
                if (holder == null) {
                    holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
                    if (holder != null) {
                        if (!validateViewHolderForOffsetPosition(holder)) {
                            // recycle this scrap
                            if (!dryRun) {
                                // we would like to recycle this but need to make sure it is not used by
                                // animation logic etc.
                                holder.addFlags(ViewHolder.FLAG_INVALID);
                                if (holder.isScrap()) {
                                    removeDetachedView(holder.itemView, false);
                                    holder.unScrap();
                                } else if (holder.wasReturnedFromScrap()) {
                                    holder.clearReturnedFromScrapFlag();
                                }
                                recycleViewHolderInternal(holder);
                            }
                            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;
                        }
                    }
                    if (holder == null && mViewCacheExtension != null) {
                        // We are NOT sending the offsetPosition because LayoutManager does not
                        // know it.
                        final View view = mViewCacheExtension
                                .getViewForPositionAndType(this, position, type);
                        if (view != null) {
                            holder = getChildViewHolder(view);
                            if (holder == null) {
                                throw new IllegalArgumentException("getViewForPositionAndType returned"
                                        + " a view which does not have a ViewHolder");
                            } else if (holder.shouldIgnore()) {
                                throw new IllegalArgumentException("getViewForPositionAndType returned"
                                        + " a view that is ignored. You must call stopIgnoring before"
                                        + " returning this view.");
                            }
                        }
                    }
                    if (holder == null) { // fallback to recycler
                        // try recycler.
                        // Head to the shared pool.
                        if (DEBUG) {
                            Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "
                                    + "pool");
                        }
                        holder = getRecycledViewPool().getRecycledView(type);
                        if (holder != null) {
                            holder.resetInternal();
                            if (FORCE_INVALIDATE_DISPLAY_LIST) {
                                invalidateDisplayListInt(holder);
                            }
                        }
                    }
                    if (holder == null) {
                        holder = mAdapter.createViewHolder(RecyclerView.this, type);
                        if (DEBUG) {
                            Log.d(TAG, "getViewForPosition created new ViewHolder");
                        }
                    }
                }
    
                // This is very ugly but the only place we can grab this information
                // before the View is rebound and returned to the LayoutManager for post layout ops.
                // We don't need this in pre-layout since the VH is not updated by the LM.
                if (fromScrap && !mState.isPreLayout() && holder
                        .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
                    holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    if (mState.mRunSimpleAnimations) {
                        int changeFlags = ItemAnimator
                                .buildAdapterChangeFlagsForAnimations(holder);
                        changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                        final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                                holder, changeFlags, holder.getUnmodifiedPayloads());
                        recordAnimationInfoIfBouncedHiddenView(holder, info);
                    }
                }
    
                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;
                    mAdapter.bindViewHolder(holder, offsetPosition);
                    attachAccessibilityDelegate(holder.itemView);
                    bound = true;
                    if (mState.isPreLayout()) {
                        holder.mPreLayoutPosition = position;
                    }
                }
    
                final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
                final LayoutParams rvLayoutParams;
                if (lp == null) {
                    rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                    holder.itemView.setLayoutParams(rvLayoutParams);
                } else if (!checkLayoutParams(lp)) {
                    rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                    holder.itemView.setLayoutParams(rvLayoutParams);
                } else {
                    rvLayoutParams = (LayoutParams) lp;
                }
                rvLayoutParams.mViewHolder = holder;
                rvLayoutParams.mPendingInvalidate = fromScrap && bound;
                return holder.itemView;
            }
    

    从代码中可以看出,要获取到某个position下的view,有这么一个过程:
    首先Recycler会先去检查mChangedScrap,如果匹配成功则返回相应的viewHolder。mChangedScrap就是所谓的detachedView,与RecyclerView分离的viewHolder列表。
    然后是mAttachedScrap,mViewCacheExtension,mRecyclerPool,如果都没有,则去创建,调用Adapter.createViewHolder()。
    其实相当于RecyclerView也做到了二级缓存的概念,mCachedViews是一层,默认大小是DEFAULT_CACHE_SIZE = 2, 还有一层就是RecycledViewPool。我们可以调用这个方法来控制第一层的缓存数量:

    public void setViewCacheSize(int viewCount) {
                mViewCacheMax = viewCount;
                // first, try the views that can be recycled
                for (int i = mCachedViews.size() - 1; i >= 0 && mCachedViews.size() > viewCount; i--) {
                    recycleCachedViewAt(i);
                }
            }
    

    这就是Recycler去根据position获取相应的itemView的过程,既然有获取,那么就肯定有放入,来看这个方法:

    void recycleViewHolderInternal(ViewHolder holder) {
                //省略一部分代码。。。
                if (forceRecycle || holder.isRecyclable()) {
                    if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                            | ViewHolder.FLAG_UPDATE)) {
                        // Retire oldest cached view
                        final int cachedViewSize = mCachedViews.size();
                        if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                            recycleCachedViewAt(0);
                        }
                        if (cachedViewSize < mViewCacheMax) {
                            mCachedViews.add(holder);
                            cached = true;
                        }
                    }
                    if (!cached) {
                        addViewHolderToRecycledViewPool(holder);
                        recycled = true;
                    }
                } else if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists");
                }
                mViewInfoStore.removeViewHolder(holder);
                if (!cached && !recycled && transientStatePreventsRecycling) {
                    holder.mOwnerRecyclerView = null;
                }
            }
    

    这个方法其实是先去判断了下mCachedViews是否已经满了,如果满了,则移除一个到recycledViewPool中去,然后将新的itemView添加到mCachedViews,如果没满那最好了,直接添加。

    LayoutManager

    LayoutManager的主要职责就是去测量和摆放itemView和当itemView对用户来说不再能看见的时候去复用它。LayoutManager牵扯到的东西其实也蛮多的,我特意去翻看了下LinearLayoutManager的源码,挺复杂的,就不再这篇讲了,到时候另起一篇来讲下这个东西。
    可以看到,RecyclerView的onMeasure和onLayout方法都是交给了LayoutManager来处理。

    @Override
        protected void onMeasure(int widthSpec, int heightSpec) {
            //省略一段代码
            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);
                if (skipMeasure || mAdapter == null) {
                    return;
                }
                if (mState.mLayoutStep == State.STEP_START) {
                    dispatchLayoutStep1();
                }
                mLayout.setMeasureSpecs(widthSpec, heightSpec);
                //省略一段代码
            } else {
                if (mHasFixedSize) {
                    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                    return;
                }
                //省略一段代码。。。。。。
                if (mAdapter != null) {
                    mState.mItemCount = mAdapter.getItemCount();
                } else {
                    mState.mItemCount = 0;
                }
                eatRequestLayout();
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                resumeRequestLayout(false);
                mState.mInPreLayout = false; 
            }
        }
    

    滑动

    RecyclerView的onTouchEvent方法比较长,这里就不贴了。其实滑动的套路都差不多的,RecyclerView也一样。那就讲一下我看了源码后的理解吧,这里以垂直方向的滑动为力。
    在ACTION_MOVE中,首先计算出手指滑动的距离,先跟TouchSlop进行比较,如果大雨这个值,那么就是确定为师滑动了,而不是点击,然后调用scrollByInternal()方法,scrollByInternal其实最后还是用了scrollBy方法,拖拽性质的滑动就解决了。当然我们也发现当我们拖拽RecyclerView后,当我们的手指离开屏幕以后,RecyclerView还会滑动一段时间,这个其实就是fling事件。RecyclerView在ACTION_UP事件的时候,VelocityTracker根据拖拽时候滑动的距离和时间计算出一个速度,然后调用fling方法就行了,其实fling里面还是用Scrooler来实现的。这里贴下fling方法的代码:

    public void fling(int velocityX, int velocityY) {
        setScrollState(SCROLL_STATE_SETTLING);
        mLastFlingX = mLastFlingY = 0;
        mScroller.fling(0, 0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        postOnAnimation();
    }
    

    源码这个东西真的是越看越多,尤其RecyclerView感觉更是。还是分篇写吧打算,下一篇继续分析下LayoutManager,ViewHolder,ItemDecoration这些。第一次分析源码,如果有错误的地方请及时指出

    我的微信公共账号,欢迎关注

    相关文章

      网友评论

        本文标题:最全面的RecyclerView源码解析(一)

        本文链接:https://www.haomeiwen.com/subject/llpzattx.html