RecyclerView 目前已成为 ListView,GridView 甚至 HorizontalListView 的高阶替代品。最初从开源项目 Telegram 中就见到 RecyclerView 的影子,当时还惊讶于它的聊天列表怎么那么顺滑。而且目前 github 出现了越来越多的基于 RecyclerView 的开源库,可见它的受欢迎程度。今天这篇文章就来分析一下 RecyclerView 的设计。
目录:
- RecyclerView 与 ListView 的对比
- RecyclerView 整体结构
- RecyclerView 绘制流程
- RecyclerView 缓存机制
- RecyclerView 使用优化
1. RecyclerView 与 ListView 的对比
上一篇文章分析了 ListView 的设计:Android应用篇 - ListView 设计分析,但是没有对比就没有伤害,来简单对比下 RecyclerView 和 ListView。
ListView 相比 RecyclerView 有一些优点:
- addHeaderView(),addFooterView() 添加头视图和尾视图。
- 通过 "android:divider" 设置自定义分割线。
- setOnItemClickListener() 和 setOnItemLongClickListener() 设置点击事件和长按事件。
这些功能在 RecyclerView 中都没有直接的接口,要自己实现 (虽然实现起来很简单),因此如果只是实现简单的显示功能,ListView 无疑更简单,不过目前 github 基于 RecyclerView 的开源库这么多,这已经不能作为 ListView 的优点了。
RecyclerView 相比 ListView,有一些明显的优点:
- 默认已经实现了 View 的复用,不需要类似 if(convertView == null) 的实现,而且回收机制更加完善。
- 默认支持局部刷新。
- 容易实现添加 item、删除 item 的动画效果。
- 通过支持水平、垂直和变革列表及其他更复杂形式,而 ListView 只支持具体某一种。
- 容易实现拖拽、侧滑删除等功能。
RecyclerView 是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。
2. RecyclerView 整体结构
RecyclerView 是在 22.1.0 开始添加到开发包中的。RecyclerView 继承于 ViewGroup 并实现了 ScrollingView 和 NestedScrollingChild2 接口。已知的直接子类有 BaseGridView 和 WearableRecycler,间接子类有 HorizontalGridView 和 VerticalGridView。
上图是 RecyclerView 的几个角色:LayoutManager,RecyclerView,ViewHolder,Adapter,DataSource。
3. RecyclerView 绘制流程
对于一个自定义 ViewGroup,主要从 onMeasure(),onLayout() 和 onDraw() 来分析。
- 3.1 onMeasure()
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// layoutManager 没有设置的话,直接走 default 的方法,所以会为空白
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// 如果测量是绝对值,则跳过 measure 过程直接走 layout
if (skipMeasure || mAdapter == null) {
return;
}
// mLayoutStep 默认值是 State.STEP_START
if (mState.mLayoutStep == State.STEP_START) {
// 分发第一步 layout
dispatchLayoutStep1();
// 执行完 dispatchLayoutStep1() 后是 State.STEP_LAYOUT
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
// 真正执行 LayoutManager 绘制的地方
dispatchLayoutStep2();
// 执行完后是 State.STEP_ANIMATIONS
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
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
}
}
一步步来看,先看看:
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
如果没有设置 LayoutManager,则直接走 defaultOnMeasure():
void defaultOnMeasure(int widthSpec, int heightSpec) {
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);
}
可以看到这里的 chooseSize() 方法其实就是根据宽高的 Mode 得到相应的值后直接调用 setMeasuredDimension() 设置宽高了,发现这里其实是没有进行 child 的测量就直接 return 结束了onMeasure() 的过程,这也就解释了为什么我们没有设置 LayoutManager 会导致显示空白了。然后后面的代码会做一个这样的判断:
if (mLayout.mAutoMeasure) {
} else {
}
mAutoMeasure 这个值,LinearLayoutManager 还是其他两个 Manager,默认值都是 true,所以往 mLayout.mAutoMeasure 里面的分支看:
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
// 如果测量是绝对值,则跳过 measure 过程直接走 layout
if (skipMeasure || mAdapter == null) {
return;
}
这种情况直接走 layout 流程的话,layout 中会进行测绘。后面的代码开始 mLayoutStep 的判断了,mLayoutStep 的默认值是 State.STEP_START,并且每次绘制流程结束后,会重置为 State.STEP_START:
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
进入 dispatchLayoutStep1():
/**
* 第一步 layout 工作主要是:
* - 处理 Adapter 的更新
* - 决定那些动画需要执行
* - 保存当前 View 的信息
* - 如果必要的话,执行上一个 Layout 的操作并且保存他的信息
*/
private void dispatchLayoutStep1() {
}
接下来就是我们的真正执行 LayoutManager 绘制的地方 dispatchLayoutStep2():
private void dispatchLayoutStep2() {
// ...
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
// 重写的 getItemCount() 方法
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
mLayout.onLayoutChildren(mRecycler, mState) 这句代码,就可以看出,RecyclerView 将绘制工作都交给 LayoutManager 了。来看看 LinearLayoutManager 的 onLayoutChildren() 实现:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
if (DEBUG) {
Log.d(TAG, "is pre layout:" + state.isPreLayout());
}
if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
resolveShouldLayoutReverse();
// ...
mLastStackFromEnd = mStackFromEnd;
if (DEBUG) {
validateChildOrder();
}
}
很复杂,但是根据注释可以总结为:
- 先寻找页面当前的锚点。
- 以这个锚点未基准,向上和向下分别填充。
- 填充完后,如果还有剩余的可填充大小,再填充一次。
- 3.2 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() {
// 适配器为空则返回
if (mAdapter == null) {
return;
}
// LayoutManager 为空则返回
if (mLayout == null) {
return;
}
mState.mIsMeasuring = false;
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 {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
这里的代码就比较好理解了,并且上面提到的问题也就迎刃而解了,当我们给 RecyclerView 设置固定的宽高的时候,onMeasure() 是直接跳过了执行,那么为什么子 View 仍然能绘制出来。这里可以看到,如果 onMeasure() 没有执行,mState.mLayoutStep == State.STEP_START 就成立,所以仍然会执行 dispatchLayoutStep1(),dispatchLayoutStep2()。也就对应的会绘制子 View。而后面的注释也比较清楚,由于我们在 layout 的时候改变了宽高,也会导致 dispatchLayoutStep2(),也就是子 View 的重新绘制。如果上面情况都没有,那么 onLayout() 的作用就仅仅是 dispatchLayoutStep3(),而 dispatchLayoutStep3() 方法的作用除了重置一些参数,外还和执行动画有关。
private void dispatchLayoutStep3() {
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
// 需要动画的情况。找出ViewHolder现在的位置,并且处理改变动画。最后触发动画。
}
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
}
4. RecyclerView 缓存机制
和 ListView 一样,对于 RecyclerView,View 的缓存机制是不得不了解一下的。盗用一张图:
可以看到,一共有好几级缓存,RecyclerView 内部也有一个 Recycler 类:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mViewCacheMax = DEFAULT_CACHE_SIZE;
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
private static final int DEFAULT_CACHE_SIZE = 2;
// ...
View getViewForPosition(int position, boolean dryRun) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount());
}
boolean fromScrap = false;
ViewHolder holder = null;
// 0) 如果有一个变更的废弃 view,先从这里面找到一个
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) 通过索引找到一个废弃的 view
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle this scrap
// ...
holder = null;
} else {
fromScrap = true;
}
}
}
// 如果还为空
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrap = true;
}
}
// 从 ViewCacheExtension 中找
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
// ...
}
}
if (holder == null) { // fallback to recycler
// 从 RecycledViewPool 中找
holder = getRecycledViewPool().getRecycledView(type);
// ...
}
if (holder == null) {
// 最后还为空,则 createViewHolder()
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
// ...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
holder.mOwnerRecyclerView = RecyclerView.this;
// 调用 bindViewHolder()
mAdapter.bindViewHolder(holder, offsetPosition);
attachAccessibilityDelegate(holder.itemView);
bound = true;
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
}
// ...
return holder.itemView;
}
// ...
}
这边就看看 getViewForPosition() 这个重要方法,mAttachedScrap,mCacheViews 只是对 View 的复用,并且不区分 type,ViewCacheExtension,RecycledViewPool 是对于 ViewHolder 的复用,区分 type。
5. RecyclerView 使用优化
由于篇幅问题,可以看看这篇文章:RecyclerView性能优化