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

2024-02-23 07:20

本文主要是介绍郭神的抽丝剥茧心法修炼: 深剖RecyclerView,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

码个蛋(codeegg)第 711 次推文

作者: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.1 的 RecyclerView,不知道什么原因,我点进 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负责 RecyclerView 子 View 的布局,常用的有 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);// ... 这里唯一值得关注就是看布局文件是否指定LayoutManagera.recycle();this.createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);// ...} else {// ...}// ...
}

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

1.2 设置 LayoutManager 和 Adapter

研究自定义 View 的时候,最快的研究方法就是直接查看 onMeasureonLayout 和 onDraw 三大方法,研究 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) {// 更换适配器的时候移除所有的子Viewthis.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 是数据变化接口,当适配器中的数据发生增删改的时候最终会调用该接口的实现类,从该接口的命名以及注册操作和取消注册操作可以看出其使用的是观察者模式。LayoutManager 和 Adapter 设置完成以后就可以直奔主题了。

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);}}}
}

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

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

如果你的代码中的 RecyclerView 没有使用 Wrap_Content,那么大部分使用场景中的 RecyclerView 长宽的 SpecMode 都为 EXACITY,我这么说,不是意味着我要抛弃 return 下方的关键方法 RecyclerView#dispatchLayoutStep1 和 RecyclerView#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-layoutint 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 layoutmLayout.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变为TrueBoolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;mState.mRunSimpleAnimations = mFirstLayoutComplete&& mItemAnimator != null&& (mDataSetHasChangedAfterLayout|| animationTypeSupported|| mLayout.mRequestedSimpleAnimations)&& (!mDataSetHasChangedAfterLayout|| mAdapter.hasStableIds());mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations&& animationTypeSupported&& !mDataSetHasChangedAfterLayout&& predictiveItemAnimationsEnabled();
}

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

再看RecyclerView#dispatchLayoutStep2方法:

private void dispatchLayoutStep2() {// ...// 预布局结束 进入真实的布局过程this.mState.mInPreLayout = false;// 实际的布局交给了LayoutManagerthis.mLayout.onLayoutChildren(this.mRecycler, this.mState);// ...// 是否有动画this.mState.mRunSimpleAnimations = this.mState.mRunSimpleAnimations && this.mItemAnimator != null;// 变更状态 准备播放动画 STEP_ANIMATIONS-4this.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-measurecallback.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 persistencecallback.processPersistent(viewHolder, record.preInfo, record.postInfo);} else if ((record.flags & FLAG_PRE) != 0) {// Was in pre-layout, never been added to post layoutcallback.processDisappeared(viewHolder, record.preInfo, null);} else if ((record.flags & FLAG_POST) != 0) {// Was not in pre-layout, been added to post layoutcallback.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:/");}// 释放recordInfoRecord.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 被一个个取出,然后将 ViewHolder 和 InfoRecord 交给 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. 布局动画多样性的原因

至于 LayoutManager 和 RecyclerView.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 在三大工程流程中大概做了如下的事:

2. View 管理 - Recycler

在上文中,我们简要了解 RecyclerView 绘制的三大流程以及LayoutManager 和 ItemAnimator 承担的任务。显然,我们忽略了适配器 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 重要组成

先看组成部分:

缓存级别参与对象作用
一级缓存mAttachedScrapmChangedScrapmChangedScrap 仅参与预布局,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 existsif (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 默认最大值为2int 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);// 添加进缓存池RecyclerPooladdViewHolderToRecycledViewPool(viewHolder, true);// 从mCachedViews中移除mCachedViews.remove(cachedViewIndex);
}

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

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


需要指明的是:

  1. mChangedScrap 实际并未参加真实的缓存过程,它的添加和移除 ViewHolder 都出现在 RecyclerView#dispatchLayoutStep1 方法中的 PreLayout(预布局) 过程中。

  2. 对于 RecyclerView 中已经显示并将继续展示的 ViewHolder,重绘过程中,会将 ViewHolder 以及其中的子 View 从 RecyclerView 移出,添加进 mAttachedScrap 中,并在后续的填充子 View 过程中,从 mAttachedScrap 取出。

  3. mCachedViews 最多只能缓存两个 ViewHolder,如果大于最大缓存数量,会将先进来的 ViewHolder 取出加入 RecycledViewPool

  4. RecycledViewPool 针对每种 viewType 的 ViewHolder 提供最大最大数量为 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 中的其他部分。敬请期待!

近期文章:

  • 码仔漫画:来自JVM的灵魂拷问:“你是什么垃圾?”(上)

  • Java四种引用,Java堆和栈,热修复,ANR,设计模式

  • 鸿蒙降世:一阵风雷惊世界,敢叫日月换新天!

日问题:

没想到每天都在用的RecyclerView这么复杂吧?

专属升级社区:《这件事情,我终于想明白了》 

这篇关于郭神的抽丝剥茧心法修炼: 深剖RecyclerView的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/737905

相关文章

《程序员修炼之道》读书笔记(8):注重实效的项目

第8章:注重实效的项目 随着项目开动,我们需要从个体的哲学与编码问题,转向为项目级别的问题。 本章将讨论影响项目成败的几个关键区域。 41《注重实效的团队》 本书在先前讨论了帮助程序员个体更好的方法,这些方法对团队也有效。 下面将针对团队,来重述前面部分章节。 不要留破窗户。团队不应该容忍那些小小的、无人修正的不完美。煮青蛙。团队更容易被煮熟,因为每个人都觉得别人会在监视环境的变化。交流

RecyclerView的itemView的点击效果

1,需要在 itemView 的布局 根节点上添加  android:clickable="true" 2、或者通过代码实现 @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {  ((ViewHolder)viewHolder).textView

ScrollView嵌套RecyclerView再嵌套RecyclerView导致的布局展示不完整问题

背景:页面布局,最外层有个ScrollView,然后里面有个RecyclerView,然后每个RecyclerView的item都是一个RecyclerView 异常:页面展示不完整,最底下的Item 展示一半,在往上滑就滑不动了   解决: // 每一个item渲染完后重新计算外层recyclerview高度// 因为外层的recyclerview是先渲染的,渲染时 内部recyc

js修炼——函数

很多技术从业者对技术水平的评价是代码的实现能力,形而上一点,我认为技术工作和足球运动(本人巴萨球迷,最爱内马尔)是一样的,最重要的是思想意识,古往今来,各行各业的大师讲究的是内功深厚,说的有些玄学的味了,总之,我薄如白纸,我需要修炼。 1.声明还是表达式 我们经常会看到函数定义的两种情况 function clc(num){//函数声明,定义未执行return num++;}//……

【项目FJU】使用SwipeRefreshLayout+RecyclerView制作下拉刷新上拉加载更多

效果截图 https://github.com/ydslib/Jianshu/tree/develop 需要用到的知识 setColorSchemeResources:设置下拉刷新进度条的颜色setOnRefreshListener:设置下拉刷新监听android:overScrollMode:设置滑动到边缘时无效果模式ContentLoadingProgressBar:内容加载进度条,继承

ListView和RecyclerView比较

1.ViewHolder 作用: View复用 Listview: 自己定义 RecyclerView: RecyclerView.ViewHolder则变成了必须 2.LayoutManager 作用: 布局管理器 Listview: 官方推荐只做垂直滚动功能 RecyclerView: LinearLayoutManager,可以支持水平和竖直方向上滚动的列表。 S

微服务修炼之配置中心

文章目录 概念关注点设计思路产品springcloud config demo 概念 在单体应用中,我们常常将一些配置数据放在配置文件中进行集中统一管理,避免硬编码造成频繁发布。比如,数据库,redis,线程池,等等。常见的windows中的ini文件,linux系统中的conf,springboot工程中的application.properties。但是在分布式系统中,经常

微服务修炼之性能调优---缓存

文章目录 基本概念缓存guavaredismemcache spring-cache缓存失效缓存穿透缓存雪崩缓存击穿 基本概念 命中:命中缓存,从缓存中获取数据失效:缓存被删除或者失效,总db中获取数据 缓存 缓存的使用场景是什么? 我们在进行开发的时候经常遇到的场景就是频繁的进行IO查询,缓存的主要目的就是将IO切换成内存操作,来提高系统的响应能力。 相对应的缓存就适合做

编程修炼之sql(RDMS)---Oracle,sqlserver,mysql数据库类型差异与部分语句差异

文章目录 数据类型DDLDML分页简谈 数据类型 Oracle 数据类型SQL Server 数据类型Mysql数据类型是否备用BFILEVARBINARY(MAX)是BLOB (4GB)VARBINARY(MAX)BLOB, LONGBLOB, MEDIUMBLOB是CHAR([1-2000])CHAR([1-2000])CHAR是CLOB (4GB)VARCHAR(MAX)T

大数据修炼之Hive

文章目录 Hive特点体系结构常用命令DDLDML 数据模型 官网 Hive特点 (1)不同的存储类型,例如纯文本文件、HBase中的文件。 (2)将元数据保存在关系数据库中,可大大减少在查询过程中执行语义检查的时间。 (3)可以直接使用存储在Hadoop文件系统中的数据。 (4)内置大量函数来操作时间、字符串和其他的数据挖掘工具,支持用户扩展UDF函数来完成内置函数无法实现的