RecyclerView流程学习
- 模块划分
- 绘制流程
- onMeasure
- mLayout为null
- mLayout开启自动测量
- 未开启自动测量
- onLayout
- onDraw
- onLayoutChildren
- 缓存
- 预加载
- 滚动和fling
模块划分
RecyclerView中根据其功能可以分为以下几个模块:
- Recycler mRecycler // 缓存管理者,final类型-不允许扩展
- LayoutManager mLayoutManager // 数据展示者
- RecyclerViewDataObserver mObserver // 数据观察者
- Adapter mAdapter // 数据提供者
- ItemAnimator mItemAnimator // 动画类
绘制流程
onMeasure
protected void onMeasure(int widthSpec, int heightSpec) {
//mLayout就是之前说过的LayoutManager
if (mLayout == null) {
// 第一种情况
}
if (mLayout.isAutoMeasureEnabled()) {
// 第二种情况
} else {
// 第三种情况
}
}
在onMeasure方法中,recyclerView根据mLayout分为了三种情况,接下来会对这三种情况进行梳理。
mLayout为null
第一种,当mLayout为null的时候:
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
//表示精确模式,View的大小已经确认,为SpecSize所指定的值
case View.MeasureSpec.EXACTLY:
return size;
//指定了最大大小
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
//父容器不对子View有限制,子View要多大给多大
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
可以看到在mLayout为Null的情况下,recyclerView还是做了测量操作,但是由于在onLayout方法中跳过了layout,因此不会展示任何东西。
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mLayout开启自动测量
在这种情况下,RecyclerView会先后调用dispatchLayoutStep1()
和dispatchLayoutStep2()
方法,除此之外,还有一个dispatchLayoutStep3()
方法会在onLayout
中进行调用,这三个方法对应了RecyclerView的三种不同状态,State.STEP_START、State.STEP_LAYOUT和State.STEP_ANIMATIONS。
State.STEP_START表示RecyclerView还未经历dispatchLayoutStep1()
。
State.STEP_LAYOUT表示此时处于layout阶段,这个阶段会调用dispatchLayoutStep2方法layout RecyclerView的children。调用dispatchLayoutStep2方法之后,此时mState.mLayoutStep变为了State.STEP_ANIMATIONS。
当mState.mLayoutStep为State.STEP_ANIMATIONS时,表示RecyclerView处于第三个阶段,也就是执行动画的阶段,也就是调用dispatchLayoutStep3方法。当dispatchLayoutStep3方法执行完毕之后,mState.mLayoutStep又变为了State.STEP_START。
具体地,先来看一下源码:
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
//官方说明:这里的调用已经废弃,事实上已经替换为defaultOnMeasure
//也就是说mLayout应该调用defaultOnMeasure()方法
//但是为了防止第三方代码被破坏,还是保留了下来
//所有开发人员在isAutoMeasureEnabled为true不应该覆盖onMeasure方法
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// 在第 2 步中设置尺寸。 为了保持一致性,应该使用旧尺寸进行预布局
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// 现在我们可以从孩子那里得到宽度和高度.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// 如果 RecyclerView 的宽度和高度不准确,并且至少有一个孩子的宽度和高度也不准确,我们必须重新测量。
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
可以看到这里最终还是调用了defaultOnMeasure()
这个方法在之前进行了介绍,但是与之前不同的一点是在此之后又调用了dispatchLayoutStep1()
和dispatchLayoutStep2()
,并且如果需要二次测量的话会重新执行
dispatchLayoutStep2()
。
那么这两个方法具体起到了什么作用呢?
先来看一下这两个方法的源码:
private void dispatchLayoutStep1() {
... ...
processAdapterUpdatesAndSetAnimationFlags();
... ...
if (mState.mRunSimpleAnimations) {
// 找到没有被remove的ItemView,保存OldViewHolder信息,准备预布局
}
if (mState.mRunPredictiveAnimations) {
// 进行预布局
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
private void processAdapterUpdatesAndSetAnimationFlags() {
if (mDataSetHasChangedAfterLayout) {
// 因为数据集意外更改,处理这些项目没有价值。
// 相反,我们只是重置它。
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this);
}
}
// 简单动画是高级动画的一个子集(这将导致预布局步骤)如果布局支持预测动画,
// 则进行预处理以决定我们是否要运行它们
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
//重点,这里主要设置了mRunSimpleAnimations和mRunPredictiveAnimations的值
//mFirstLayoutComplete是指第一次绘制流程完成,当未完成时为false
//因此当一次绘制的时候mRunSimpleAnimations和mRunPredictiveAnimations都为false
//不会加载动画
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
private void dispatchLayoutStep2() {
...
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
//重点,尝试执行layoutChildren
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
...
mState.mLayoutStep = State.STEP_ANIMATIONS;
...
}
总结一下,step1的功能是与Animations有关,控制是否加载动画,而step2的功能是尝试layoutChildren。
未开启自动测量
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear
如果mHasFixedSize为true,就直接调用LayoutManager.onMeasure方法进行测量,如果mHasFixedSize为false,则先判断是否有数据更新,有的话先处理数据更新,再调用LayoutManager.onMeasure方法进行测量。
onLayout
在onMeasure
方法完成之后,接下来应该进行onLayout
方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
这里最重要的就是dispatchLayout
方法,来具体地看一下这个方法的实现:
void dispatchLayout() {
... ...
mState.mIsMeasuring = false;
//前两步已经在onMeasure的时候完成,
//但是当size发生变化的时候,仍然需要重新onLayout
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
//布局的最后一步,我们保存有关动画视图的信息,触发动画并进行任何必要的清理。
//这里设置了mState.mLayoutStep = State.STEP_START
dispatchLayoutStep3();
}
onDraw
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
//调用了mItemDecorations的onDraw方法
//此时item已经在绘制了,这意味着ItemDecoration会在item上方被绘制
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
... ...
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
//调用了mItemDecorations的onDraw方法
//此时item还没有绘制,这意味着ItemDecoration会在item下方被绘制
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
这里有两个知识要点:
- 当view draw的时候会以适当执行以下方法(但是3一定会在4前面):
1. 绘制背景
2. 如有必要,保存画布的图层以备褪色
3. 绘制视图的内容(调用onDraw()方法)
4. 画孩子(dispatchDraw() 将draw事件分发给children)
5. 如有必要,绘制褪色边缘并恢复图层
6. 绘制装饰(例如滚动条)
7. 如有必要,绘制默认焦点高亮 - itemDecoration是指item的装饰,系统默认实现了一个DividerItemDecoration用作item之间的分割线。实际上,itemDecoration可以实现更多的效果,这里可以参考这篇文章RecyclerView系列之二ItemDecoration。
总而言之draw方法主要做了以下几件事情:
- 将draw事件分发给子类
- 绘制itemDecoration
- 根据setClipToPadding制定特殊的滑动效果
onLayoutChildren
在之前的dispatchLayoutStep2
中,RecyclerView调用了mLayout.onLayoutChildren
,LayoutManager是具体的item展示者,由它来确定item应该如何展示,如何布局。具体的应用可以查看这篇文章:LayoutManager及其自定义。
这里只讲述LayoutManager是如何绘制item的,以LinearLayoutManager为例:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//布局算法:
// 1) 通过检查children和其他变量,找到锚点坐标和锚点项位置。
// 2) 向开始填充,从底部堆叠
// 3) 向末端填充,从顶部堆叠
// 4) 滚动以满足从底部堆叠的要求。
// 创建布局状态
// ······
// 第一步
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 计算锚点的位置和坐标
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
// ······
// 移除回收子view
detachAndScrapAttachedViews(recycler);
mLayoutState.mIsPreLayout = state.isPreLayout();
// 开始填充
if (mAnchorInfo.mLayoutFromEnd) {
// 向开始填充,更新LayoutState
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// 向末端填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// 向开始填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// 向末端填充
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
// ······
}
可以看到在这个方法中,首先计算出了锚点的位置和坐标,然后以锚点开始向start方向或者end方向进行填充,如果还有剩余位置的话就从另一个方向开始进行填充。
简单总结一下整个流程就是:
fill
方法中,真正填充的方法是layoutChunk
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// ······
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ······
layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
// ······
}
layoutChunk的执行流程如下:
调用LayoutState的next方法获得一个ItemView。千万别小看这个next方法,RecyclerView缓存机制的起点就是从这个方法开始,可想而知,这个方法到底为我们做了多少事情。
如果RecyclerView是第一次布局Children的话(layoutState.mScrapList == null为true),会先调用addView,将View添加到RecyclerView里面去。
调用measureChildWithMargins方法,测量每个ItemView的宽高。注意这个方法测量ItemView的宽高考虑到了两个因素:1.margin属性;2.ItemDecoration的offset。
调用layoutDecoratedWithMargins方法,布局ItemView。这里也考虑上面的两个因素的。
缓存
在讲述缓存之前,不妨先思考一下,为什么RecyclerView需要缓存机制?
在RecyclerView的Adapter中,对于不同的数据类型会为其创建一个ViewHolder,并且将数据绑定到这个ViewHolder上。
public class MyAdapter extends RecyclerView.Adapter<MessageViewHolder> {
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
... ...
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position){
... ...
}
}
但是无论是onCreateViewHolder还是onBindViewHolder都会使用到findViewById方法,如果每次一个新item进入都需要回调这两个方法会导致效率很低,因此需要对ViewHolder进行缓存复用以减少回调频次。
先看看这篇文章吧
这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!
public final class Recycler {
...
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
*
* 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。
* ...
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (mState.isPreLayout()) {
// 0 尝试从mChangedScrap中获取ViewHolder对象
holder = getChangedScrapViewForPosition(position);
...
}
if (holder == null) {
// 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
...
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) {
// 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
...
}
if (holder == null && mViewCacheExtension != null) {
// 2 尝试从mViewCacheExtension中获取ViewHolder对象
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
...
}
}
if (holder == null) { // fallback to pool
// 3 尝试从mRecycledViewPool中获取ViewHolder对象
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
// 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
if (mState.isPreLayout() && holder.isBound()) {
...
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
// 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
...
}
从上述代码,可以看到RecyclerView在尝试获取ViewHolder的时候会依次从mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecycledViewPool获取,如果都获取不到才会创建viewHolder。
mChangedScrap、mAttachedScrap主要用于存放当前屏幕可见,但是被标记为“移除”或者“重用”的列表项。
mChangedScrap主要用于notifyItemChanged、notifyItemRangeChanged这类方法在动画开启下的场景,而mAttachedScrap则用于notifyItemMoved、notifyItemRemoved这类列表项发生移动的场景。
当调用notifyItemRemoved
后,RecyclerView首先会将所有的可见的viewHolder加入到mAttachedScrap中,等到重新布局完成,开始展示子视图之后再遍历mAttachedScrap找到对应position的viewHolder。
mCachedViews用于存放已经被移除屏幕,但是很快有可能进入屏幕的列表项,默认大小为2。这里比较好理解,假设RecyclerView向下滑动,当最上面的viewHolder滚动出可见区的时候,这个viewHolder会加入到mCachedViews中。之后如果反方向滑动,当最上面的viewHolder再次进入到可见区域之后,就会尝试从mCachedViews再次获取之前的viewHolder。
mViewCacheExtension主要用于提供额外的、开发人员使用的缓冲区。
mRecyclerPool主要用于按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项。
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
//自定义了数据类型ScrapData,每个ScrapData内部存有最多5个同一viewType类型的viewHolder。
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
...
}
预加载
先看看这篇文章吧
掌握这17张图,没人比你更懂RecyclerView的预加载
滚动和fling
RecyclerView 的滚动是怎么实现的?(一)
RecyclerView 的滚动时怎么实现的?(二)| Fling
简单来说,RecyclerView回去捕获MotionEvent.ACTION_MOVE事件,并且计算出移动的距离。
当移动超过一定距离则进入Fling状态,每一帧都会去执行移动任务。
当计算出移动距离之后,委托LayoutManager执行当前的item向反方向移动的动画,并且填充空闲区域。