一、简单使用
1.导包
implementation 'androidx.recyclerview:recyclerview:1.1.0'
2.使用
mAdapter = new MyAdapter(getActivity());
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
//设置布局方向
// layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
二、RecyclerView家族图谱
LayoutManager:
布局管理器,用来管理RecyclerView的布局,负责measure、layout的功能,都会被LayoutManager代理,不同的LayoutManager所展示出来的样式是不一样的
默认提供LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager三大类,分别可以实现ListView,GridView以及流式布局的列表效果。
Android Sdk默认提供了三种样式:
- LinearLayoutManager:线性
- GridLayoutManager:网格
- StaggeredGridLayoutManager:流式布局
还可以自定义一些特殊的布局样式
Recyler:
相当于item的缓存池,为RecyclerView提供高效、复用的回收机制
里面存在四种缓存机制:
- mAttachedScrap
- mCacheViews
- mViewCacheExetensions
- mRecylerPool
SmoothScroller
用来控制RecyclerView滑动时的速度,也可以自定义
SnapHelper:
可以控制RecyclerView松手之后惯性滑动的,可以控制来决定惯性停止后的位置
ItemAnimator:
处理列表上的item add、remove、change 的动画效果,自带默认动画,同时也可以自定义item的动画效果
ItemDecoration:
提供列表上item的装饰的能力,比如DeviderItemDecoration为列表上的item提供分割线、item间距的配置
ItemTouchHelper:
用来控制item的手势行为,比如可以实现列表的侧滑删除、拖动排序的功能,还可以扩展ItemDecoration实现吸顶悬浮的效果
OnItemTouchListener:
可以用来处理RecyclerView的手势分发,可以用来处理item的点击效果
DiffUtil:
用来对比两组数据前后的差异,为RecyclerView提供定向更新的能力
三、RecyclerView和ListView的区别
- 从效果上看:RecyclerView不仅能实现ListView、GridView的效果,还可以完成瀑布流的效果,还能设置列表的滚动方向(垂直、水平);
- 从实现上看: RecyclerView中view的复用不需要开发者自己写代码,系统内部已经封装完成了,可以进行局部刷新,提供API来实现item的动画效果
- 从性能上:频繁的刷新数据,需要添加动画,RecyclerView优势很大,如果只是展示效果,区别并不大
四、RecyclerView 源码分析
1.从setLayoutManager出发
mRecyclerView.setLayoutManager(layoutManager);
在RecyclerView 的setLayoutManager 方法中,首先会判断RecyclerView以前是否已经关联了layoutManager
mLayout != null;
如果之前已经关联了,会对之前关联的layoutManager做一些清理和解除关联的操作,比如结束列表上的动画,移除列表上的items,清理缓存池的缓存
清理之后会把新设置的layoutManager 保存起来
mLayout = layout;
紧接着判断新设置的layoutManager是否已经和其他的RecyclerView已经关联了,关联了就会抛出异常,
"LayoutManager " + layout + " is already attached to a RecyclerView:"
一个LayoutManager 只能和一个RecyclerView相关联
在方法的最后调用了requestLayout
整个源码:
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();
// TODO We should do this switch a dispatchLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
if (mLayout != null) {
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear();
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout
+ " is already attached to a RecyclerView:"
+ layout.mRecyclerView.exceptionLabel());
}
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
mRecycler.updateViewCacheSize();
requestLayout();
}
2.onMeasure
RecyclerView也是一个ViewGroup,在它的onMeasure 方法中,首先也会判断RecyclerView 是否跟layoutManager相关联了,如果没有关联列表上的item 就无法测量,RecyclerView的宽高也就无法得到测量,就会调用defaultOnMeasure()来给RecyclerView设置一个尽量合理的宽高值
这个方法根据父容器的宽高值及RecyclerView宽高测量模式和padding、minimumWidth minmumHeight 来计算,将计算结果设置给RecyclerView
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);
}
接下来在onMeasure中判断layoutManager 是否开启了自动测量模式,也就是layoutManager 是否接管了对列表的自动测量工作,开启之后能保证RecyclerView 的宽高在不确定的情况下,同时列表的宽高也在不确定的情况下,能测量出一个正确的值
默认三种layoutManager 都是开启了
@Override
public boolean isAutoMeasureEnabled() {
return true;
}
对于RecyclerView来说并没有遍历它的子View去测量,而是把子View的测量工作交给了layoutManager
就是因为RecyclerView 把子View 布局的测量和布局的工作暴露了出去,才得以自定义各种layoutManager的样式,但是这里只是把RecyclerView的子View的宽高值给暴露了出去,RecyclerView 本身的宽高值如果在布局中没有指定确定的值还是需要在这里测量得到的
接下来调用layout 的onMeasure 方法
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
在这个方法里面,调用了recyclerView的defaultMeasure 方法,并没有开启列表item 的测量工作
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
列表子item的测量工作是在onLayout中开始的
接下来判断RecyclerView宽高值是不是都有一个确切的值,比如设置了具体的宽高值,或者都设置了match_parent
measureSpecModeIsExactly才会为true,就不需要执行测量工作
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
State:
RecyclerView的内部类,保存RecyclerView 在当前滑动状态下的所有信息,比如:
- mTargetPosition:保存了调用scrollToPosition传递的值
- mItemCount:当前滑动状态下列表上可以显示item的数量
- mLayoutStep:布局的阶段。layoutmanager 把一次新的布局工作分成了三个阶段
- STEP_START:第一阶段,预布局阶段,对应的方法是dispatchLayoutStep1,作用:在开启新的布局之前先收集需要做动画的item对应的动画信息
- STEP_LAYOUT:第二阶段,真正开始布局的阶段,对应的方法是dispatchLayoutStep2,作用:让layoutmanager 测量并布局列表上的item,从而期望计算出RecyclerView的宽高,并不一定能准确的测出,如果RecyclerView没有一个确切的宽高并且列表上至少存在一个item也没有一个确切的宽高,在onMeasure阶段通过两次测量从而能计算出一个确切的宽高
- STEP_ANIMATIONS:第三阶段,动画阶段,对应的方法是dispatchLayoutStep3,作用:主要是在布局完成之后通过阶段1收集的需要做动画的view信息,开启动画的执行
可以得出RecyclerView 优化的方向:
尽可能的在布局中指定RecyclerView的宽高为固定的值或者设置为Match_parent,在列表中item的宽高也尽可能的设置固定值
如果没有设置确切的宽高值,就会继续执行以下代码
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
3.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;
}
在onLayout中进而调用了dispatchLayout()
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
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()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
注意:如果在onMeasure()中调用了dispatchLayoutStep1,state就会被改变,否则才会执行到这里
如果state等于STEP_START 就执行dispatchLayoutStep1() ,然后执行dispatchLayoutStep2
dispatchStep2可能会被多次调用,比如没有给RecyclerView 和item设置确切的宽和高,在onMeasure 中就会调用两次
dispatchLayoutStep2 中
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
mLayout.onLayoutChildren
layoutManager 去负责item的布局工作,item的布局怎么摆放交由layoutManager负责,可以是线性,网格,瀑布流,也可以是自定义
注意:
任何自定义的layoutManager 都必须复写这个方法,摆放列表上view的位置
4.LinearLayoutManager
在onLayoutChildren()中
- 首先判断savedState是否不为空,说明RecyclerView所在的页面不可见了,此时需要把之前缓存的viewHolder全部移除,把view回收掉
2. 接着会调用ensureLayoutState来创建layoutState 对象,存储了layoutManager在当前滑动下一些关联信息,
void ensureLayoutState() {
if (mLayoutState == null) {
mLayoutState = createLayoutState();
}
}
比如:
- available:还有多少可用的空间用来摆放列表上的子view
- currentPosition:当前摆放到了第几个item
- ScrollingOffset:当前滑动的偏移量
在滑动的时候判断列表还有多少空间来继续向屏幕上填充item,在列表滑动时也会去更新layoutState 中字段的信息
3. 接下来会调用resolveShouldLayoutReverse()判断要不要反转布局,从屏幕下方到屏幕上方开始布局
- 如果调用了setReverseLayout 就会从下往上进行布局
- 如果调用了setStackFromEnd 列表上item还是从上往下开始布局的,但是布局完成之后会自动滚动到最后一个item上,比如加载历史聊天记录
4. 接下来判断AnchorInfo是否处于无效状态
mAnchorInfo
作用:用来存储锚点view的position位置信息和锚点view的位置坐标
这个对象的mValid在创建时默认是false,会进入if 分支
5. 调用updateAnchorInfoForLayout() 收集锚点view的坐标和position信息
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from pending information");
}
return;
}
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from existing children");
}
return;
}
if (DEBUG) {
Log.d(TAG, "deciding anchor info for fresh state");
}
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
在更新时提供了三种策略
- 如果之前页面被回收了,本次页面恢复了以后会从pendingData对象中去恢复上一次的布局状态,恢复AnchorInfo 的数据
- 列表上找这个焦点的view来更新AnchorInfo的数据
- 否则根据布局的排列方式来取列表中第一个后者最后一个item来更新AnchorInfo 的数据
问题:
列表在加载完成之后会自动滚动到某一个item的位置上,就是这个原因,列表上某一个item获取了焦点
解决办法:去除item的焦点,或者在这个item 前放一个能够过去焦点的view
问题:
LinearLayoutManager在开始布局之前为什么要先去获取AnchorInfo?
原因:
因为LinearLayout 在往列表上填充数据时并没有从上往下逐一填充,它是从锚点的位置也就是具备焦点item的位置,先是从上往下排列,再是从锚点位置从下往上继续填充
这就是它往屏幕填充item的一个策略
6. 接着要计算开始布局的偏移量,也就是刚开始设置layoutManager 时就调用了scrollToPosition,这时需要在布局开始之前就要去计算下开始布局位置的偏移量
7. 接着就调用detachAndScrapAttachedViews(),在新的布局开始之前,需要对列表上已经存在的item对应的viewHolder 把它们分门别类的回收,只是暂时的回收到对应的缓存集合里,目的是为了接下来在布局时能够从recycle 回收池中复用viewHolder
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 != RecyclerView.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();
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
// This case relates to when the anchor child is the focused view and due to layout
// shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
// up after tapping an EditText which shrinks RV causing the focused view (The tapped
// EditText which is the anchor child) to get kicked out of the screen. Will update the
// anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
// the available space in layoutState will be calculated as negative preventing the
// focused view from being laid out in fill.
// Note that we won't update the anchor position between layout passes (refer to
// TestResizingRelayoutWithAutoMeasure), which happens if we were to call
// updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
// child which can change between layout passes).
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
if (DEBUG) {
Log.d(TAG, "Anchor info:" + mAnchorInfo);
}
// LLM may decide to layout items for "extra" pixels to account for scrolling target,
// caching or predictive animations.
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
// if the child is visible and we are going to move it around, we should layout
// extra items in the opposite direction to make sure new items animate nicely
// instead of just fading in
final View existing = findViewByPosition(mPendingScrollPosition);
if (existing != null) {
final int current;
final int upcomingOffset;
if (mShouldReverseLayout) {
current = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(existing);
upcomingOffset = current - mPendingScrollPositionOffset;
} else {
current = mOrientationHelper.getDecoratedStart(existing)
- mOrientationHelper.getStartAfterPadding();
upcomingOffset = mPendingScrollPositionOffset - current;
}
if (upcomingOffset > 0) {
extraForStart += upcomingOffset;
} else {
extraForEnd -= upcomingOffset;
}
}
}
int startOffset;
int endOffset;
final int firstLayoutDirection;
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
: LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
}
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
// noRecycleSpace not needed: recycling doesn't happen in below's fill
// invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
// changes may cause gaps on the UI, try to fix them.
// TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
// changed
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
}
}
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
if (DEBUG) {
validateChildOrder();
}
}
5.Recycle
Recycle是RecycleView的内部类,记录了viewHolder 回收的四级缓存
- mAttachedScrap、mChangedScrap:同属于第一级缓存,存储的viewHolder 有一个特性,就是在复用的时候不需要重新调用bindviewHolder 去重新绑定数据,RecyclerView 认为这种情况下的viewHolder 还会再次出现在屏幕上,这两个缓存起来的ViewHolder 数据和状态是不会被重置的
- mChcheViews:属于第二级缓存,存放随着列表的上下滑动,被滑出去的item对应的viewHolder ,默认容量大小是2,可以通过setItenlmDataSize来适当的扩容,即便是滑出屏幕了也可以多缓存几个,再次滑进屏幕时也是不需要重新绑定数据的
- mViewCacheExtension:属于第三级缓存,是RecyclerView 缓存能力的一种拓展,允许开发者自定义ViewHolder 的缓存位置和实现方式
- mRecyclerPool:属于第四级缓存,当且仅当mCacheViews放不下的时候,才会把ViewHolder放进mRecyclerPool 中,它在存放ViewHolder 时是会根据viewHolder对应的viewType 来分门别类的存储,在默认情况下每个viewType 对应的集合容量为5,也可以通过setRecyclerViewPool 来调整每个viewType 中viewHolder 的容量
举例:
1.当调用notifyItemChanged item_02刷新RecycleView时
item_02对应的ViewHolder就会被放到mChangedScrap里面,因为RecycleView认为这个item发生了变化,其余item是没有发生变化的,存放在mAttachedScrap集合里
总结:
- mAttachedScrap是存放原封不动的ViewHolder
- mChangedScrap是存放发生变化了的ViewHolder
- mAttachedScrap、mChangedScrap存储的ViewHolder在复用的时候是不需要重新绑定数据的
2.当RecycleView向下滑动时,item_01被滑出屏幕
刚被滑出屏幕的item对应的ViewHolder是会被缓存到mChcheViews这个集合中的,RecycleView认为很有可能会再次反向滑动列表使得item_01又会进入屏幕,被缓存到mChcheViews这个集合中的ViewHolder在复用的时候如果和原来的位置一致也是不需要重新调用bindViewHolder来重新绑定数据的,而mChcheViews集合是有容量限制的,默认是2,当列表一直往下滑动时,就会有很多item被滑出屏幕了,在添加mChcheViews集合之前会校验容量是否已满,满的话就会把最先添加到集合中的ViewHolder移除,并添加到mRecyclerPool集合中
总结:
mRecyclerPool这个缓存所缓存ViewHolder都是需要重新绑定数据的,也就是说当调用了notifyDataSetChanged时,屏幕上几乎所有的item都需要重新绑定数据,所以在更新列表时,不到万不得已不要直接使用notifyDataSetChanged
回收机制源码分析
在调用notifyItemChanged、notifyDataSetChanged都会触发LayoutManager的onLayoutChildren
在这个方法中,LayoutManager向列表填充item时,直接向Recycler中去获取ViewHolder,如果获取不到,就说明缓存中没有可复用的ViewHolder,此时把item填充到列表上时就会去创建一个ViewHolder,再来重新绑定数据,这样是非常浪费的,所以在复用之前要先回收,从缓存中得到的itemView,在调用addView时,会判断这个view是否已经被添加过了,不用担心重复添加View的情况
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
在这个方法中对屏幕上可见的view进行遍历,然后逐个调用scrapOrRecycleView
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
这个方法在回收之前,会做一些判断 如果数据无效了且没有移除,就是调用了notifyDataSetChanged的时候,RecyclerView认为列表上的数据集已经全面发生了变化,列表上所有的item都需要重新绑定数据,就会给列表上的ViewHolder标记上一个无效的flag,进入if分支
- 先判断viewHolder.isInvalid(),item的ViewHolder里面所存储的数据无效了,发生变化了,就说明这个viewHolder无效了
- 然后判断!viewHolder.isRemoved(),这个ViewHolder的数据有没有被删除
- 判断!mRecyclerView.mAdapter.hasStableIds() 列表上每一个item是否都有一个long类型身份id的标志,默认都是false,除非手动去指定
1.if分支
在if分支中,会调用
recycler.recycleViewHolderInternal(viewHolder);
在这个方法中会去回收ViewHolder
在回收的时候首先去判断是否有存储到mCacheViews里
如果没有就会判断mCacheViews缓存的数量是否已经溢出了,溢出的话就会把CacheViews中最先添加进来的,也就是最老的ViewHolder进行移除,移除后放到RecyclerPool里面去
然后把本次需要缓存的ViewHolder添加到mCacheViews集合中
mCachedViews.add(targetCacheIndex, holder);
1.else分支
如果不是notifyDataSetChanged导致的列表item无效,就会进入else分支,调用:
recycler.scrapView(view);
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
判断ViewHolder有没有被移除,有没有被置为无效,有没有被更新,如果没有的话RecycleView会认为在本次布局阶段依旧想留在屏幕上面的,就把它存储到mAttachedScrap集合中
mAttachedScrap.add(holder);
如果对应的ViewHolder被更新了,就会进入到else分支,也就是调用了notifyItemChanged触发了重新布局,就会把这个ViewHolder存放到mChangedScrap集合中
mChangedScrap.add(holder);
列表上填充Item
在onLayoutChildren中执行了detachAndScrapAttachedViews(recycler)后
判断是否需要倒序布局,也就是从屏幕的下方往屏幕的上方开始布局
只有调用setStackFromEnd、setReverseLayout显性的要求它倒序布局,都是false
else中会先从锚点的位置从上往下往列表上面填充item,紧接着会从从锚点的位置从下往上往列表上面填充item,都是调用fill方法进行填充
在网列表上填充item的时候开启一个while循环,while循环中有一个判断remainingSpace,在当前的方向上是否有多余的空间可用于填充item,在while循环中调用了layoutChunk,一个个的把item填充到列表上
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
1.从view对象的缓冲池中得到一个view对象
View view = layoutState.next(recycler);
进而调用layoutManager的addView方法
在这个addView方法中,会判断这个view对应的ViewHolder是否是一个即将被删除的View,如果是就把它添加到即将被删除的集合中
mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
如果这个View之前已经被添加过了,就不需要再次添加了
child.getParent() == mRecyclerView
否则的话就把这个view添加到RecyclerView这个ViewGroup上了
mChildHelper.addView(child, index, false);
2.调用measureChildWithMargins,在测量时考虑到了item的上下左右的padding、margin所需要的空间
measureChildWithMargins(view, 0, 0);
3.紧接着会调用layoutDecoratedWithMargins,把view摆放到界面适当的位置上去,摆放子view位置时会考虑到decoration的存在,不至于把decoration遮盖住了
layoutDecoratedWithMargins(view, left, top, right, bottom);
复用机制流程
复用流程的结构
LayoutManager每次向列表上填充item时都会向Recycle去索取一个ViewHolder,
- 在索取ViewHolder时Recycle会按照优先级先到mAttachedScrap、mChangeScrap这两个一级缓存中去查找是否有可复用的ViewHolder,这两个存储的ViewHolder不需要重新绑定数据,存储的都是屏幕内的ViewHolder
- 如果一级缓存中找不到,就会向二级缓存mCacheViews去查找,如果找到,就会来一个位置一致性校验,因为mCacheViews里存储的ViewHolder是被滑出屏幕的,相同位置的才可以直接复用,如果是被下面滑上来的被复用的就需要重新绑定数据了
- 如果二级缓存中找不到,就会向三级缓存允许开发者自定义mViewCacheExt中去查找
- 如果都没找到,就会去四级缓存mRecyclerPool中去查找,回去根据item的viewType去查找,如果有就返回了,如果没有还是会调用adapter的onCreateViewHolder来创建一个并返回
复用流程最终会调用到tryGetViewHolderForPositionByDeadline 去尝试下的获取ViewHolder
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = 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() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
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 (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}
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
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
在这个方法中首先去调用getChangedScrapViewForPosition 从一级缓存ChangedScrap里读取
holder = getChangedScrapViewForPosition(position);
如果ChangedScrap没有任何内容就直接返回了,接下来对mChangedScrap进行遍历,校验之前是否没有被复用过,之前列表上的位置和正要填充的位置是否一致,一致就返回
如果holder等于null,就会调用getScrapOrHiddenOrCachedHolderForPosition,从三个集合中尝试获取holder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh + exceptionLabel());
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
首先是到mAttachScrap中获取,判断方式跟mChangedScrap类似,如果添加都满足就返回
这里是第一次向mAttachScrap中去查找,如果在这里没找到,就会向正在做删除动画的item集合中去查找是否有可以满足复用的ViewHolder,虽然这里向mHiddenViews里面去查找有没有存在复用的ViewHolder,但它并不属于四级缓存,这个集合中存储的只是需要做删除动画的ViewHolder,动画执行完成后,这里缓存的ViewHolder就会被移除
View view = mChildHelper.findHiddenNonRemovedView(position);
View findHiddenNonRemovedView(int position) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getLayoutPosition() == position
&& !holder.isInvalid()
&& !holder.isRemoved()) {
return view;
}
}
return null;
}
如果这里也没找到,就会去mCachedViews里面去查找
如果满足跳转就直接返回ViewHolder,否则就返回null
如果 getScrapOrHiddenOrCachedHolderForPosition 返回不为空
就会调用validateViewHolderForOffsetPosition
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
// if it is a removed holder, nothing to verify since we cannot ask adapter anymore
// if it is not removed, verify the type and id.
if (holder.isRemoved()) {
if (DEBUG && !mState.isPreLayout()) {
throw new IllegalStateException("should not receive a removed view unless it"
+ " is pre layout" + exceptionLabel());
}
return mState.isPreLayout();
}
if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ "adapter position" + holder + exceptionLabel());
}
if (!mState.isPreLayout()) {
// don't check type if it is pre-layout.
final int type = mAdapter.getItemViewType(holder.mPosition);
if (type != holder.getItemViewType()) {
return false;
}
}
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
return true;
}
在这个方法中会判断这个item的viewType和正在填充的item的viewType是否一致,
type != holder.getItemViewType()
如果给item开启了身份唯一标识,还会判断这个item前后id的一致性
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
都通过了都满足条件了才会复用
如果这个方法返回了null,还会向mViewCacheExtension中去获取
最后,根据viewType到RecyclerPool中去查找
如果RecyclerPool也查找不到就会调用adapter的createViewHolder,去创建一个新的ViewHolder了
holder = mAdapter.createViewHolder(RecyclerView.this, type);
五、总结
- 在设计一个大型框架的时候,可以使用插拔式的设计模式,增加组件的灵活性
- 在列表刷新的时候可以使用notifyItemChanged、notifyItemRangeChanged,而不是直接使用notifyDataSetChanged
- 使用DiffUtil差分异也能提高列表刷新的性能
- 在使用RecyclerView时尽可能的给它指定固定的宽和高,列表上的item也尽量指定确切的宽和高,这样在RecyclerView的onMeasure方法中,也就是它的测量阶段大大提升测量效率