前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >郭神的抽丝剥茧心法修炼: 深剖RecyclerView

郭神的抽丝剥茧心法修炼: 深剖RecyclerView

作者头像
陈宇明
发布2020-12-16 10:40:04
1.4K0
发布2020-12-16 10:40:04
举报
文章被收录于专栏:设计模式
作者:TeaOf

博客:https://www.jianshu.com/p/1ae2f2fcff2c

前言

抽丝剥茧RecyclerView 系列文章的目的在于帮助 Android 开发者提高对 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 的使用方式:

代码语言:javascript
复制
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();
代码语言:javascript
复制

以及 RecyclerView 各个部分的作用:

代码语言:javascript
复制
负责 RecyclerView 子 View 的布局,常用的有 LinearLayoutManager(线性布局),还有 GridLayoutManager(网格布局) 和 StaggeredGridLayoutManager(瀑布布局) 等。

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

代码语言:javascript
复制
负责 ViewHolder 的回收和提供。

二,源码分析

1. RecyclerView 三大工作流程

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

1.1 构造函数

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

代码语言:javascript
复制
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 讲起:

代码语言:javascript
复制
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 方法:

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

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

代码语言:javascript
复制
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 工作流程的第一步:

代码语言:javascript
复制
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 工作流程的第二步:

代码语言:javascript
复制
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方法肯定是要进入的:

代码语言:javascript
复制
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方法:

代码语言:javascript
复制
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,代码较多,精简后如下:

代码语言:javascript
复制
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 中的动画如何执行:

代码语言:javascript
复制
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 中的回调,最后查看一下具体实现:

代码语言:javascript
复制
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,同样需要用户自定义实现:

代码语言:javascript
复制
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);
 }
}
代码语言:javascript
复制

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

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

2. View 管理 - Recycler

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

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

代码语言:javascript
复制
mLayout.onLayoutChildren(mRecycler, mState);
代码语言:javascript
复制

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

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

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

2.1 Recycler 重要组成

先看组成部分:

缓存级别

参与对象

作用

一级缓存

mAttachedScrap、mChangedScrap

mChangedScrap 仅参与预布局,mAttachedScrap 存放还会被复用的 ViewHolder

二级缓存

mCachedViews

最多存放 2 个缓存 ViewHolder

三级缓存

mViewCacheExtension

需开发者自定义实现

四级缓存

mRecyclerPool

可以理解 RecyclerPool 是 (int,ArrayList<ViewHolder>) 的 SparseArray,键是 viewType,每个 viewType 最多可以存放 5 个 ViewHolder

2.2 获取 ViewHolder

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

代码语言:javascript
复制
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 是如何获取的呢?

代码语言:javascript
复制
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

代码语言:javascript
复制
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 方法:

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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 方法:

代码语言:javascript
复制
void recycleCachedViewAt(int cachedViewIndex) {
    // ...
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    // 添加进缓存池RecyclerPool
    addViewHolderToRecycledViewPool(viewHolder, true);
    // 从mCachedViews中移除
    mCachedViews.remove(cachedViewIndex);
}

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

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

需要指明的是:

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

有了 Recycler 以后:

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

三、浅谈设计模式

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

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

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

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

四、总结

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

后续博客中,我将和大家一起学习 RecyclerView 中的其他部分。敬请期待!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码个蛋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
    • 1. RecyclerView 三大工作流程
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档