抽丝剥茧RecyclerView - 化整为零

前言

抽丝剥茧RecyclerView系列文章的目的在于帮助Android开发者提高对RecyclerView的认知,本文是整个系列的第一章。

RecyclerView已经出来很久了,很多开发者对于RecyclerView的使用早已信手拈来。如下就是一张使用网格布局的RecyclerView:

RecyclerView

不过,对于RecyclerView这种明星控件的了解仅仅停留在使用的程度,显然是不能够让我们成为高级工程师的。如果你看过RecyclerView包中的源码,那你应该和我的心情一样复杂,光一个RecyclerView.class文件的源码就多达13000行。

对于源码阅读方式,我很赞成郭神在Glide源码分析中所说:

抽丝剥茧、点到即止。抽丝剥茧、点到即止。应该认准一个功能点,然后去分析这个功能点是如何实现的。但只要去追寻主体的实现逻辑即可,千万不要试图去搞懂每一行代码都是什么意思,那样很容易会陷入到思维黑洞当中,而且越陷越深。

所以,我在阅读RecyclerView源码的时候先确定好自己想好了解的功能点:

  1. 数据转化为具体的子视图。
  2. 视图回收利用方式。
  3. 布局多样性原因。
  4. 布局动画多样性原因。

阅读姿势:我选择了版本为25.3.1RecyclerView,不知道什么原因,我点进28.0.0版本的RecyclerView库中查看RecyclerView.class代码时,虽然类缩短至7000行,但是注释没了以及其他的问题,我不得不使用其他版本的RecyclerView库。

想要深入原理,没有什么是一遍调试解决不了的,如果有,那就是调试第二遍。

目录

目录

一、RecyclerView使用和介绍

LinearLayoutManager为例,我们看一下RecyclerView的使用方式:

RecyclerView mRecyclerView = findViewById(R.id.recycle);
// 设置布局方式
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
// 适配器,MainAdapter继承自RecyclerView.Adapter<VH extends RecyclerView.ViewHolder>
MainAdapter mAdapter = new MainAdapter();
mRecyclerView.setAdapter(mAdapter);
// 添加分割线的方法
// mRecyclerView.addItemDecoration();
// 设置布局动画的方法,可以自定义
// mRecyclerView.setItemAnimator();

以及RecyclerView各个部分的作用:

主要的类 作用
LayoutManager 负责RecyclerViewView的布局,常用的有LinearLayoutManager(线性布局),还有GridLayoutManager(网格布局)和StaggeredGridLayoutManager(瀑布布局)等。
Adapter 负责将数据转变成视图,使用时需要继承该类。
ItemAnimator 子视图动画,RecyclerView有默认的子视图动画,也可自定义实现。
ItemDecoration 分隔线,需自定义实现。

以上是我们使用RecyclerView的时候能够直观看到的部分,还有一个很重要但是不直接使用的类:

主要的类 作用
Recycler 负责ViewHolder的回收和提供。

二、源码分析

1. RecyclerView三大工作流程

RecyclerView的源码那么多,我们先按照使用时的路线进行分析。

1.1 构造函数

通常,我们会在布局文件中使用RecyclerView,所以我们的入口就变成了:

public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    // ... 省略一些实例的初始化
    
    if (attrs != null) {
        int defStyleRes = 0;
        TypedArray a = context.obtainStyledAttributes(attrs, styleable.RecyclerView, defStyle, defStyleRes);
        String layoutManagerName = a.getString(styleable.RecyclerView_layoutManager);
        // ... 这里唯一值得关注就是看布局文件是否指定LayoutManager
        a.recycle();
        this.createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);
       // ...
    } else {
        // ...
    }
    // ...
}

由于我们可以在RecyclerView的布局文件中使用app:layoutManager指定LayoutManager,如果指定了具体的LayoutManager,最终会在上面的RecyclerView#createLayoutManager方法中利用反射生成一个具体的LayoutManager实例。

1.2 设置LayoutManager和Adapter

研究自定义View的时候,最快的研究方法就是直接查看onMeasureonLayoutonDraw三大方法,研究RecyclerView也是如此。

上面我们说到了布局文件,之后,我们会在Activity或者其他地方获取RecyclerView,再往下,我们会为RecyclerView设置LayoutManager(如未在布局文件中设置的情况下)、Adapter以及可能使用的ItemDecoration,这些方法都会调用RecyclerView#requestLayout方法,从而刷新RecyclerView

先从RecyclerView#setLayoutManager讲起:

public void setLayoutManager(@Nullable RecyclerView.LayoutManager layout) {
    if (layout != this.mLayout) {
        // 停止滚动
        this.stopScroll();
        if (this.mLayout != null) {
            // 因为是第一次设置,所以mLayout为空
            // ... 代码省略 主要是对之前的LayoutManager 进行移除前的操作
        } else {
            this.mRecycler.clear();
        }
        this.mChildHelper.removeAllViewsUnfiltered();
        this.mLayout = layout;
        if (layout != null) {
            // 对新的LayoutManager进行设置
            this.mLayout.setRecyclerView(this);
            if (this.mIsAttached) {
                this.mLayout.dispatchAttachedToWindow(this);
            }
        }
        this.mRecycler.updateViewCacheSize();
        // 重点 通知界面重新布局和重绘
        this.requestLayout();
    }
}

RecyclerView#requestLayout会刷新布局,所以该跳到ViewGroup绘制的相关方法了?不,因为RecyclView中的Adapter为空,Adapter为空,就没有数据,那看一个空视图还有什么意思呢?So,我们还需要看设置适配器的RecyclerView#setAdapter方法:

public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
    // 冻结当前布局,不让进行子布局的更新
    this.setLayoutFrozen(false);
    // 重点关注的方法
    this.setAdapterInternal(adapter, false, true);
    this.processDataSetCompletelyChanged(false);
    // 再次请求布局的重新绘制
    this.requestLayout();
}

继续深入查看RecyclerView#setAdapterInternal方法:

private void setAdapterInternal(@Nullable RecyclerView.Adapter adapter, Boolean compatibleWithPrevious, Boolean removeAndRecycleViews) {
    if (this.mAdapter != null) {
        // 第一次进入mAdapter为null,故不会进入该代码块
        // 主要是对旧的mAdapter的数据监听器解除注册
        this.mAdapter.unregisterAdapterDataObserver(this.mObserver);
        this.mAdapter.onDetachedFromRecyclerView(this);
    }
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        // 更换适配器的时候移除所有的子View
        this.removeAndRecycleViews();
    }
    this.mAdapterHelper.reset();
    RecyclerView.Adapter oldAdapter = this.mAdapter;
    this.mAdapter = adapter;
    if (adapter != null) {
        // 新的适配器注册数据监听器
        adapter.registerAdapterDataObserver(this.mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (this.mLayout != null) {
        this.mLayout.onAdapterChanged(oldAdapter, this.mAdapter);
    }
    this.mRecycler.onAdapterChanged(oldAdapter, this.mAdapter, compatibleWithPrevious);
    this.mState.mStructureChanged = true;
}

可以看出,上面的代码主要是针对Adapter发生变化的情况下做出的一些修改,RecyclerView.AdapterDataObserver是数据变化接口,当适配器中的数据发生增删改的时候最终会调用该接口的实现类,从该接口的命名以及注册操作和取消注册操作可以看出其使用的是观察者模式。LayoutManagerAdapter设置完成以后就可以直奔主题了。

1.3 onMeasure

View工作流程的第一步:

protected void onMeasure(int widthSpec, int heightSpec) {
    if (this.mLayout == null) {
        this.defaultOnMeasure(widthSpec, heightSpec);
    } else {
        // LinearLayoutManager#isAutoMeasureEnabled为True
        // GridLayoutManager继承子LinearLayoutManager isAutoMeasureEnabled同样为true
        // 这种情况下,我们主要分析this.mLayout.isAutoMeasureEnabled()为true的场景下
        if (!this.mLayout.isAutoMeasureEnabled()) {
            // ... 省略
        } else {
            int widthMode = MeasureSpec.getMode(widthSpec);
            int heightMode = MeasureSpec.getMode(heightSpec);
            // ... 测量 最后还是走ViewGroup测量子布局的那套
            this.mLayout.onMeasure(this.mRecycler, this.mState, widthSpec, heightSpec);
            Boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            // 如果当前的RecyclerView的布局方式是设置了具体高宽或Match_Parent或mAdapter为null就直接返回
            if (measureSpecModeIsExactly || this.mAdapter == null) {
                return;
            }
            if (this.mState.mLayoutStep == State.STEP_START) {
                this.dispatchLayoutStep1();
            }
            this.mLayout.setMeasureSpecs(widthSpec, heightSpec);
            this.mState.mIsMeasuring = true;
            this.dispatchLayoutStep2();
            this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            if (this.mLayout.shouldMeasureTwice()) {
                this.mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(this.getMeasuredHeight(), MeasureSpec.EXACTLY));
                this.mState.mIsMeasuring = true;
                this.dispatchLayoutStep2();
                this.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        }
    }
}

显然,从上面的代码我们可以得出结论:measureSpecModeIsExactlytrue或者Adapter为空,我们会提前结束onMeasure的测量过程。

如果看过View的工作流程的同学应该对SpecMode很熟悉,什么情况下SpecMode会为EXACITY呢?以RecyclerView为例,通常情况下,如果RecyclerView的宽为具体数值或者Match_Parent的时候,那么它的SpecMode很大程度就为EXACITYmeasureSpecModeIsExactlytrue需要保证高和宽的SpecMode都为EXACITY当然,ViewSpecMode还与父布局有关,不了解的的同学可以查阅一下相关的资料。

如果你的代码中的RecyclerView没有使用Wrap_Content,那么大部分使用场景中的RecyclerView长宽的SpecMode都为EXACITY,我这么说,不是意味着我要抛弃return下方的关键方法RecyclerView#dispatchLayoutStep1RecyclerView#dispatchLayoutStep2,因为它们在另一个工作流程onLayout中也会执行,所以我们放到onLayout中讲解。

1.4 onLayout

View工作流程的第二步:

protected void onLayout(Boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection("RV OnLayout");
    this.dispatchLayout();
    TraceCompat.endSection();
    this.mFirstLayoutComplete = true;
}

void dispatchLayout() {
    if (this.mAdapter == null) {
        // ...
    } else if (this.mLayout == null) {
        // ...
    } else {
        this.mState.mIsMeasuring = false;
        // 根据当前State的不同执行不同的流程
        if (this.mState.mLayoutStep == STEP_START) {
            this.dispatchLayoutStep1();
            this.mLayout.setExactMeasureSpecsFrom(this);
            this.dispatchLayoutStep2();
        } else if (!this.mAdapterHelper.hasUpdates() && this.mLayout.getWidth() == this.getWidth() && this.mLayout.getHeight() == this.getHeight()) {
            this.mLayout.setExactMeasureSpecsFrom(this);
        } else {
            this.mLayout.setExactMeasureSpecsFrom(this);
            this.dispatchLayoutStep2();
        }
        this.dispatchLayoutStep3();
    }
}

mState实例初始化中,mState.mLayoutStep默认为STEP_STARTRecyclerView#dispatchLayoutStep1方法肯定是要进入的:

private void dispatchLayoutStep1() {
    // 全部清空位置信息
    mViewInfoStore.clear();
    // 确定mState.mRunSimpleAnimations和mState.mRunPredictiveAnimations
    // ...
    // 预布局状态跟mState.mRunPredictiveAnimations相关
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    // ...
    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            // ...
            // 存储子View的位置信息...
            mViewInfoStore.addToPreLayout(holder, animationInfo);
        }
    }
    if (mState.mRunPredictiveAnimations) {
        // 其实我也不太理解PreLayout布局的意义,放出来看看
        // Step 1: run prelayout: This will use the old positions of items. The layout manager
        // is expected to layout everything, even removed items (though not to add removed
        // items back to the container). This gives the pre-layout position of APPEARING views
        // which come into existence as part of the real layout.
      
        // 真实布局之前尝试布局一次
        // temporarily disable flag because we are asking for previous layout
        mLayout.onLayoutChildren(mRecycler, mState);
        for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
            //...
            if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                // ...
                if (wasHidden) {
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else {
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
        }
        // we don't process disappearing list because they may re-appear in post layout pass.
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    // 
    mState.mLayoutStep = State.STEP_LAYOUT;
}

private void processAdapterUpdatesAndSetAnimationFlags() {
    // ...
    // mFirstLayoutComplete 会在RecyclerView第一次完成onLayout变为True
    Boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
    mState.mRunSimpleAnimations = mFirstLayoutComplete
                    && mItemAnimator != null
                    && (mDataSetHasChangedAfterLayout
                    || animationTypeSupported
                    || mLayout.mRequestedSimpleAnimations)
                    && (!mDataSetHasChangedAfterLayout
                    || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                    && animationTypeSupported
                    && !mDataSetHasChangedAfterLayout
                    && predictiveItemAnimationsEnabled();
}

我们需要关注mState.mRunSimpleAnimationsmState.mRunPredictiveAnimations为true时机,从代码上来看,这两个属性为true必须存在mItemAnimator,是否意味着子View动画的执行者mItemAnimator,另外,mViewInfoStore.addToPreLayout(holder, animationInfo);也得关注,ViewInfoStoreRecyclerView记录了ViewHolder中子View的位置信息和状态。

再看RecyclerView#dispatchLayoutStep2方法:

private void dispatchLayoutStep2() {
    // ...
    // 预布局结束 进入真实的布局过程
    this.mState.mInPreLayout = false;
    // 实际的布局交给了LayoutManager
    this.mLayout.onLayoutChildren(this.mRecycler, this.mState);
    // ...
    // 是否有动画
    this.mState.mRunSimpleAnimations = this.mState.mRunSimpleAnimations && this.mItemAnimator != null;
    // 变更状态 准备播放动画 STEP_ANIMATIONS-4
    this.mState.mLayoutStep = State.STEP_ANIMATIONS;
    // ...
}

RecyclerView#dispatchLayoutStep2方法中我们可以看到,RecyclerView自身没有实现给子View布局,而是将布局方式交给了LayoutManagerLayoutManager的深入研究我会在之后的博客和大家讨论。

打铁趁热,我们查看RecyclerView#dispatchLayoutStep3,代码较多,精简后如下:

private void dispatchLayoutStep3() {
    this.mState.assertLayoutStep(State.STEP_ANIMATIONS);
    // ... 省略
    this.mState.mLayoutStep = State.STEP_START;
    if (this.mState.mRunSimpleAnimations) {
        for (int i = this.mChildHelper.getChildCount() - 1; i >= 0; --i) {
            // ...省略
            // 总结下来就是两个步骤:
            // 1.添加真实的布局信息
            this.mViewInfoStore.addToPostLayout(holder, animationInfo);
        }
        // 2.挨个执行动画
        this.mViewInfoStore.process(this.mViewInfoProcessCallback);
    }
    //... 清空信息
    this.mViewInfoStore.clear();
}

调用执行动画函数ViewInfoStore#process的时候,可以看到放入参数mViewInfoProcessCallback,从名字可以看出,这是一个回调的接口,所以,我猜动画的真实的执行应该在实现接口的方法中实现,不过,我们还是要先看ViewInfoStore中的动画如何执行:

void process(ProcessCallback callback) {
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index --) {
        final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
        final InfoRecord record = mLayoutHolderMap.removeAt(index);
        if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
            // Appeared then disappeared. Not useful for animations.
            callback.unused(viewHolder);
        } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
            // Set as "disappeared" by the LayoutManager (addDisappearingView)
            if (record.preInfo == null) {
                // similar to appear disappear but happened between different layout passes.
                // this can happen when the layout manager is using auto-measure
                callback.unused(viewHolder);
            } else {
                callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
            }
        } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
            // Appeared in the layout but not in the adapter (e.g. entered the viewport)
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
            // Persistent in both passes. Animate persistence
            callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE) != 0) {
            // Was in pre-layout, never been added to post layout
            callback.processDisappeared(viewHolder, record.preInfo, null);
        } else if ((record.flags & FLAG_POST) != 0) {
            // Was not in pre-layout, been added to post layout
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_APPEAR) != 0) {
            // Scrap view. RecyclerView will handle removing/recycling this.
        } else if (DEBUG) {
            throw new IllegalStateException("record without any reasonable flag combination:/");
        }
        // 释放record
        InfoRecord.recycle(record);
    }
}

// 回调的接口
interface ProcessCallback {
    void processDisappeared(ViewHolder var1, @NonNull ItemHolderInfo var2, @Nullable ItemHolderInfo var3);
    void processAppeared(ViewHolder var1, @Nullable ItemHolderInfo var2, ItemHolderInfo var3);
    void processPersistent(ViewHolder var1, @NonNull ItemHolderInfo var2, @NonNull ItemHolderInfo var3);
    void unused(ViewHolder var1);
}

之前存储的和ViewHolder位置状态相关InfoRecord被一个个取出,然后将ViewHolderInfoRecord交给ProcessCallback,如我们所料,ViewInfoStore#process只是对ViewHolder进行分类,具体的实现还是在RecyclerView中的回调,最后查看一下具体实现:

this.mViewInfoProcessCallback = new ProcessCallback() {
    // ... 这里我们只展示一个方法就行了
    public void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        RecyclerView.this.animateAppearance(viewHolder, preInfo, info);
    }
    // ...
};

void animateAppearance(@NonNull RecyclerView.ViewHolder itemHolder, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preLayoutInfo, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo postLayoutInfo) {
    itemHolder.setIsRecyclable(false);
    if (this.mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
        this.postAnimationRunner();
    }
}

限于篇幅,这里我只展示了ProcessCallback中实现的一个方法processAppeared,在该方法中,它调用了RecyclerView#animateAppearance方法,动画的任务最终也交给了RecyclerView.ItemAnimatorRecyclerView.ItemAnimator可由用户自定义实现。

这里有必要说明一下,一些删除或者新增操作,通过使用适配器中通知删除或者新增的方法,最终还是会通知界面进行重绘。

到这儿,我们可以总结一下,onLayout过程中,RecyclerView将子视图布局的任务交给了LayoutMananger,同样的,子视图动画也不是RecyclerView自身完成的,动画任务被交给了RecyclerView.ItemAnimator,这也就解决了我们一开始提出的两个问题:

  1. 布局多样性的原因
  2. 布局动画多样性的原因

至于LayoutManagerRecyclerView.ItemAnimator更深层次的探讨,我将会在后面的博客中进行。

1.5 onDraw

RecylcerView中的onDraw方法比较简单,仅仅绘制了ItemDecoration,同样需要用户自定义实现:

public void onDraw(Canvas c) {
    super.onDraw(c);
    int count = this.mItemDecorations.size();
    for (int i = 0; i < count; ++i) {
        ((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).onDraw(c, this, this.mState);
    }
}

而子View的绘制其实在ViewGroup#dispatchDraw实现的,这里不再继续讨论了。

如果你没看懂,没关系,RecyclerView在三大工程流程中大概做了如下的事:

View的三大流程

2. View管理-Recycler

在上文中,我们简要了解RecyclerView绘制的三大流程以及LayoutManagerItemAnimator承担的任务。显然,我们忽略了适配器Adapter和缓存管理Recycler,下面我们就重点谈谈这两位。

上文中,我们了解到在RecyclerView#dispatchLayoutStep2方法中,给子View定位的任务交给了LayoutManager

mLayout.onLayoutChildren(mRecycler, mState);

简要的介绍一下LayoutManger#onLayoutChildren的工作内容:

  1. 如果当前RecyclerView中还存在子View,移除所有的子View,将移除的ViewHolder添加进Recycler
  2. 一次通过Recycler获取一个子View。
  3. 重复进行2,直到获取的子View填充完RecyclerView即可。

虽然上面的内容很简单,但是LayoutManager的实际工作内容要复杂的多,那么 Recycler工作机制是怎样的呢?我们来一探究竟。

2.1 Recycler重要组成

先看组成部分:

缓存级别 参与对象 作用
一级缓存 mAttachedScrapmChangedScrap mChangedScrap仅参与预布局,mAttachedScrap存放还会被复用的ViewHolder
二级缓存 mCachedViews 最多存放2个缓存ViewHolder
三级缓存 mViewCacheExtension 需开发者自定义实现
四级缓存 mRecyclerPool 可以理解RecyclerPool(int,ArrayList<ViewHolder>)SparseArray,键是viewType,每个viewType最多可以存放5个ViewHolder
2.2 获取ViewHolder

入口是Recycler#getViewForPosition,有一个位置的参数:

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

// 看函数名称就知道,它是尝试获取ViewHolder
View getViewForPosition(int position, Boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

通过名字就可以猜到函数的意思了,ViewHolder中的itemView就是我们要获取的子视图,ViewHolder是如何获取的呢?

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                Boolean dryRun, long deadlineNs) {
    //...
    ViewHolder holder = null;
    // 第一步 从 mChangedScrap 中获取
    // PreLayout从名字可以看出,它不是真实的布局,不过我不是特别清楚
    // 预布局的意义。
    // 除此之外,它其实没有意义的,没有参与实际布局的缓存过程中。
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 第二步 从 mAttachedScrap或者mCachedViews 中获取
    // 如果RecyclerView之前就有ViewHolder,并且这些ViewHolder之后还要
    // 继续展现,在Layout过程中,它会将这些ViewHolder先取出来存放进mAttachedScrap,
    // 填充的时候再从mAttachedScrap取出
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        // ...
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 第三步 Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            // StableId可以被当做ViewHolder的唯一标识
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                                    type, dryRun);
            //...
        }
        // 第四步 mViewCacheExtension需要用户自定义实现并设置
        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 (holder == null) {
            // 第五步 从RecycledViewPool中获取
            // 通过RecycledViewPool获取
            // 每种ViewType的ViewHolder最多可以存放五个
            holder = getRecycledViewPool().getRecycledView(type);
            //...
        }
        if (holder == null) {
            // 第六步 缓存中都没有就重新创建
            // 如果缓存中都没有,就需要重新创建
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            // ...
        }
    }
    Boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // ...
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // ...
        // 没有绑定就重新绑定
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    // ...
    return holder;
}

从注释中我们可以看到,前三步ViewHolder的获取是利用的Recycler的一级缓存和二级缓存,第四步通过mViewCacheExtension获取,第五步通过RecyuclerPool的方式获取,如果连缓存池中都没有,那么Recycler只好调用Adapter#createViewHolder重新创建,这个名称是我们的老朋友了,而且还是在Adapter中,我们简单了解一下Adapter#createViewHolder

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

public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);

真正创建ViewHolder的是Adapter#onCreateViewHolder方法,这也是我们继承适配器Adapter必须要实现的抽象方法,通常,我们在继承Adapter不会只创建ViewHolder,还会做子View和数据的绑定,在返回视图之前,视图的绑定肯定是完成了的,我们看看视图绑定发生在哪里?

我们再返回上一个方法Recycler#tryGetViewHolderForPositionByDeadline中,可以看到在倒数第四行,在执行Recycler#tryBindViewHolderByDeadline方法:

private Boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
                int position, long deadlineNs) {
    // ...
    // 最关键的方法就是调用了Adapter#bindViewHolder方法
    mAdapter.bindViewHolder(holder, offsetPosition);
    // ...
}

public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
    onBindViewHolder(holder, position);
}

public abstract void onBindViewHolder(VH holder, int position);

成功见到我们必须实现的Adapter#onBindViewHolder方法,这些完成以后,子View就会被交给LayoutManager管理了。

2.2 回收ViewHolder

ViewHolder回收的场景有很多种,比如说滑动、数据删除等等。我们在这里以滑动作为回收的场景,并且只分析手指触摸时的滑动,滑动的入口在RecyclerView#onTouchEvent

public Boolean onTouchEvent(MotionEvent e) {
    // ...
    switch (action) {
        // ...
        case MotionEvent.ACTION_MOVE: {
            // ...
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                // 当前滑动状态设置为SCROLL_STATE_DRAGGING 需要滑动距离大于阈值
                if (scrollByInternal(
                                            canScrollHorizontally ? dx : 0,
                                            canScrollVertically ? dy : 0,
                                            vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                // ...
            }
        }
        break;
        // ...
    }
    // ...
    return true;
}

代码简化以后,我们仅需要关注RecyclerView#scrollByInternal

Boolean scrollByInternal(int x, int y, MotionEvent ev) {
    // ...
    if (mAdapter != null) {
        // ...
        // 无论是横向或者纵向都交给了LayoutManager处理
        if (x != 0) {
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            unconsumedY = y - consumedY;
        }
        // ...
    }
    // ...
    return consumedX != 0 || consumedY != 0;
}

最后还是交给了LayoutManager处理,除去函数嵌套之后,最后又回到了LayoutManager的视图填充的过程,在2.2章节中,我们仅仅讨论了该过程中视图的获取,其实,该过程中,还会涉及到视图的回收,LayoutManager在回收的过程中,大概做了如下的事情:

  1. 找出需要回收的视图。
  2. 通知父布局也就是RecyclerView移除子视图。
  3. 通知Recycler进行回收管理。

我们着重探究Recycler进行回收管理,回收的入口是Recycler#recycleView

public void recycleView(View view) {
    // ...
    ViewHolder holder = getChildViewHolderint(view);
    // ...
    recycleViewHolderInternal(holder);
}

void recycleViewHolderInternal(ViewHolder holder) {
    // 一系列检查
    // ...
    Boolean cached = false;
    Boolean recycled = false;
    // ...
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_REMOVED
                                | ViewHolder.FLAG_UPDATE
                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // mViewCacheMax 默认最大值为2
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 缓存数量大于2的时候将最先进来的ViewHolder移除
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
            // ...
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        // ...
    }
    // ViewInfoStore 中移除
    mViewInfoStore.removeViewHolder(holder);
}

从上述的Recycler#recycleViewHolderInternal方法可以看出,ViewHolder会被优先加入mCachedViews,当mCachedViews数量大于2的时候,会调用Recycler#recycleCachedViewAt方法:

void recycleCachedViewAt(int cachedViewIndex) {
    // ...
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    // 添加进缓存池RecyclerPool
    addViewHolderToRecycledViewPool(viewHolder, true);
    // 从mCachedViews中移除
    mCachedViews.remove(cachedViewIndex);
}

因为cachedViewIndex是2,所以mCachedViewsViewHolder数量为2的时候,会先添加到mCachedViews,然后从mCachedViews中移除先进来的ViewHolder添加进缓存池。

我在这里选取了一些常用的场景,整合出如下图片:


常见使用Recycler缓存场景

需要指明的是:

  1. mChangedScrap实际并未参加真实的缓存过程,它的添加和移除ViewHolder都出现在RecyclerView#dispatchLayoutStep1方法中的PreLayout(预布局)过程中。
  2. 对于RecyclerView中已经显示并将继续展示的ViewHolder,重绘过程中,会将ViewHolder以及其中的子ViewRecyclerView移出,添加进mAttachedScrap中,并在后续的填充子View过程中,从mAttachedScrap取出。
  3. mCachedViews最多只能缓存两个ViewHolder,如果大于最大缓存数量,会将先进来的ViewHolder取出加入RecycledViewPool
  4. RecycledViewPool针对每种viewTypeViewHolder提供最大最大数量为5的缓存。

有了Recycler以后:

android手机界面首页.jpg

灰色的是小T同学的手机屏幕,查看聊天记录的时候,RecyclerView不会每次都创建新的ViewHolder,也不会一次性将所有的ViewHolder都建好,减少了内存和时间的损耗,所以,小T同学就可以流畅的查看和女友的上千条聊天记录了~

三、浅谈设计模式

阅读源码的过程中,发现RecyclerView运用了很多设计模式。

Adapter类这个名字,就可以看出它使用了适配器模式,因为涉及到将数据集转变成RecyclerView需要的子视图。除了适配器模式之外,Adapter中还使用观察者模式,这一点可以从RecyclerView#setAdapter方法中可以看出,设置适配器的时候,会对旧的Adapter取消注册监听器,接着对新的Adapter注册监听器,等到数据发生变化的时候,通知给观察者,观察者就可以在RecyclerView内愉快地删除或者新增子视图了。

接着,看LayoutManager这个类,RecyclerView将给View布局这个任务交给了抽象类LayoutManager,根据不同需求,比如线性布局可以用LinearLayoutManager实现,网格布局可以用GridLayoutManager。应对同一个布局问题,RecyclerView使用了策略模式,给出了不同的解决方案,ItemAnimator也是如此。

如果感兴趣的话,同学们可以查看对应的源码。

四、总结

本文中,除了对Recycler进行深层次研究外,其他则点到为止,大致得到如下结论:

总结

后续博客中,我将和大家一起学习RecyclerView中的其他部分。这大概是我写的最难受的博客之一了,一是RecyclerView的源码很长,看着有点累;二是源码分析的博客确实不知道怎么写,还在持续探索中。本人水平有限,难免有误,欢迎指出哟~

如果你对本系列文章感兴趣

第一篇:《抽丝剥茧RecyclerView - 化整为零》

参考文章

《RecyclerView 源码解析》
《RecyclerView缓存原理,有图有真相》
《RecyclerView缓存机制(咋复用?)》
《RecyclerView动画源码浅析》
《Understanding RecyclerView Components. Part -2》

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

推荐阅读更多精彩内容