先来看一段代码
recycler_view.layoutManager =
LinearLayoutManager(
this,x
LinearLayoutManager.VERTICAL,
false
)
recycler_view.adapter = MyAdapter(this)
一个很普通的 recyclerView 的使用片段,这篇文章主要介绍一下 RecyclerView 的测量,回收,复用这会引申出来两个类
LayoutManager:
负责 measurelayout;主要有 LinearLayoutManager,GridLawyoutManager,StaggeredGridLayoutManager
Recyler :
四级回收,复用机制; 主要有 mAttachedScrap和mChangedScrap,mCacheViews,mViewCahceExtensions,mRecylerPool
这里也顺便介绍一下 RecyclerView 相关的类吧
SmoothScroller : 滑动速度控制;LinearSmoothScroller
SnapHelper : 惯性滑动控制; LinearSnapHelper;
ItemDecoration: Item样式装饰; DividerItemDecoration,ItemTouchHelper
OnItemTouchListener: 手势拦截器
DiffUtil : 差分异刷新;
嗯....类介绍完了,看看代码吧,开始指定是 setLayoutManager 跑不了的,点击进去看看,
RecyclerView.java
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
```
} else {
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
```
}
mRecycler.updateViewCacheSize();
requestLayout();
}
首先判断了一下是否是同一个 LayoutManager,然后停止滚动,接着判断本类的 mLayout 是否为空,这里是为了判断 RecyclerView 之前是否已经关联了一个 LayoutManager,如果之前关联了,就需要做一些清除和解除关联的操作.
然后判断了一下传递进来的 layout,这里是为了判断这个 LayoutManager 是否已经和其他的 RecyclerView 相关联了.
最后呢调用了 requestLayout() 因为 RecyclerView 也是一个 ViewGroup,所以接下来也就进入了它的 onMeasure 和 onLayout 方法.
来吧,看看 onMeasure(int widthSpec, int heightSpec)
RecyclerView.java
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
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();
}
// 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);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
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);
}
} else {
...
}
}
这里面同样判断了一下 RecyclerView 是否与 LayoutManager 相关联,如果没有关联,列表上的 item 是无法测量的,recyclerView 的宽高也就无法测量.这时会调用 defaultOnMeasure() 方法来给 recyclerView 设置一个尽量合理的宽高值,这个方法会跟据 RecyclerView 父容器的宽高值,以及 recyclerView的宽高模式,它的 paddingLecft ,paddingRight MininumWidth 等值来计算一个尽量合理的宽高值,设置给 recyclerView.
接着往下看,判断了是否自动测量(就是 LayoutMananger 是否接管了对列表上 itme 的测量工作,官方也是建议在自定义 layoutMananger 的时候开启这个自动测量模式)
那么,为什么要开启这个自动测量模式?
开启之后,在下面的这个if 分支里面,它能够保证 RecyclerView 的宽高在不确定的情况下和列表中的宽高不确定的情况下,也能测量出一个正确的值,对于一个 viewGroup 一般都会在 onMeasure 中遍历它的子 view,挨个去测量,而在 RecyclerView 这里并没有去遍历测量,而是把 veiw 的测量交给了 LayoutManager,注意,这里只是 LayoutManager 接管了列表中子 view 的测量,recyclerView 本身的宽高如果在布局中没有指定确定的值,那还是需要在这个方法中测量才能得到.
我们看到它调用了 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
看名字好像是 LayoutManager 开启了对列表的测量工作,跟进看一下
RecyclerView.Java
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
他调用了上面提到过的 defaultOnMeasure 方法,此时并没有开启对列表上 item 的测量工作,为什么要这么设计?因为尽管 recyclerView 开启了自动测量,还是要测出recyclerView 兜底的宽高.
接下来会判断 measureSpecModeIsExactly ,也就是判断 recyclerView 的宽高是否都有一个确切的值,如果为 true,这直接返回.而如果没有给指定确切的宽高值,就执行下面的代码,来测量 recyclerView的宽高.
接下来需要看的是关于 State 的这个判断
if (mState.mLayoutStep == State.STEP_START)
这是 RecyclerView 的一个内部类,
这个对象呢是在 RecyclerView 创建的时候创建的,它保存了 recyclerView 在当前滑动状态下的所有信息,这里介绍一下 mLayoutStep 这字段
RecyclerView.State.Java
public static class State {
static final int STEP_START = 1;
static final int STEP_LAYOUT = 1 << 1;
static final int STEP_ANIMATIONS = 1 << 2;
```
int mLayoutStep = STEP_START;
```
}
实际上 LayoutMananger 把一次新的布局分成了三个阶段 STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
第一个阶段,预布局阶段,LayoutStep = STEP_START ,也是默认值,对应的方法为 dispatchLayoutStep1
第二个阶段是真正开始布局的地方,LayoutStep = STEP_LAYOUT, 对应的方法为 dispatchLayoutStep2
第三个阶段是动画阶段 LayoutStep = STEP_ANIMATIONS ,对应的方法为 dispatchLayoutStep3
而上面的 onMeasure 方法里面也能看到确实是调用了1和2 方法,而
dispatchLayoutStep1 主要是开启一次新的布局之前,收集需要做动画的item和他们对应的动画信息,并且把LayoutStep 设置成 STEP_LAYOUT.
dispatchLayoutStep2 看一下注释
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
第二个布局步骤,我们对最终状态的视图进行实际布局。如有必要,可多次执行此步骤(例如测量)。
回到 onMeasure 第一次调用 dispatchLayoutStep2 的地方,这里让 layoutManager 开始测量并布局列表上的 item,从而期望能够计算出 recyclerView 的宽高.此时是期望,并不一定能准确的测出,因为下面还有一种情况, mLayout.shouldMeasureTwice() 为true的时候还会再测量一遍,
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
翻译一下: 如果 RecyclerView 没有确切的宽高,与此同时列表上至少存在一个 item也没有确切的宽和高,就会进行第二次的测量来计算确切的宽高值比如:把 recyclerView 的宽和高设置成 wrap_content,item 设置成 match_parent 这时候当第一次调用 dispatchLayoutStep2 的时候还不知道 RecyclerView的宽和高 就会执行第二次测量,如果 item 有固定的宽高就不需要了.这里也能想到使用 RecylerView 的时候尽可能的指定确定的宽高来进行优化一下.
dispatchLayoutStep2 里面还有个重要的方法
private void dispatchLayoutStep2() {
...
mLayout.onLayoutChildren(mRecycler, mState);
...
}
从这里可以看出列表上的 item 具体怎么摆放,交由了 LayoutMananger 负责,跟进一下.
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
发现有一个 Log,自定义 LayoutManager ,你必须复写这个方法.我们就以 LinearLayoutMananger 来看看具体情况.
LinearLayoutMananger .java
@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();
...
}
首先判断了 mPendingSavedState 是否为空,顾名思义,保存状态的对象,如果不为空,就说明 RecyclerView 执行了 View的状态保存方法,也就是 onSaveInstanceState() ,这就说明了 RecyclerView 所在的页面已经不可见了,就需要把之前缓存的 ViewHolder 全部移除,并把 View 回收掉
来看下一个执行的方法,ensureLayoutState();
private LayoutState mLayoutState;
....
void ensureLayoutState() {
if (mLayoutState == null) {
mLayoutState = createLayoutState();
}
}
这里就是去创建了 LayoutState 对象.这个对象里面存了些滑动时的状态信息,比如还有多少空间用来摆放列表的子 View mAvailable,当前已经摆放到了第几个item mCurrentPosition,当前滑动的偏移量 mScrollingOffset.
接下来调用了 resolveShouldLayoutReverse(); 这个方法的意思是是否需要反转布局
private void resolveShouldLayoutReverse() {
// A == B is the same result, but we rather keep it readable
if (mOrientation == VERTICAL || !isLayoutRTL()) {
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
回到 onLayoutChildren ,而实际上布局是从上往下还是从下往上,是由下面的
mShouldReverseLayout ^ mStackFromEnd
决定的,正常情况下布局都是从上往下布局,如果调用了 setReverseLayout 就会从屏幕下方逐一往屏幕上方进行布局,而如果调用了 setStackFromEnd ,列表的布局还是从上往下布局,但是等布局完成之后,就会自动滚动到列表的最后一个 Item 上去,可用于即时通讯的会话列表.
这里还有个东西需要说一下 ,AnchorInfo mAnchorInfo,这个对象是在 RecyclerView 创建的时候就被创建了,它里面存储了 RecyclerView 锚点 view 的 position 信息和锚点 view 在屏幕上的坐标,所以下面也就会调用 updateAnchorInfoForLayout 这个方法来收集锚点 view 的position 和坐标信息, updateAnchorInfoForLayout 会有三种更新方式,我们来看一下.
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;
}
1.如果之前页面被回收了,本次页面恢复之后,就会从 PendingData 中恢复上一次的布局状态,也就是从 PendingData 中恢复 anchorInfo 的数据
2.从列表上找到一个具有焦点的 view 来更新anchorInfo 数据
3.根据布局的排列方式来取第一个或最后一个item来更新 anchorInfo数据
RecyclerView 出现列表加载完成之后自动滚动到某一个 item上去,就是这个原因了.
至于这里为什么要在开始布局之前获取 anchorInfo,是因为 LinearLayoutManager 在往列表上填充 item 的时候,并不是从上到下依次填充的,他是从锚点位置,从上往下填充给,接着继续从锚点位置从下往上在填充一次,这是 LinearLayoutManager 往屏幕上填充时的一个策略.有兴趣的可以瞅瞅.
回到 onLayoutChildren ,接着往下看,有这么一个注释
// LLM may decide to layout items for "extra" pixels to account for scrolling target,
// caching or predictive animations.
也就是说下面这段代码,就是为了解决我们在刚开始设置 LayoutMananger的时候,就调用了 scrollToPosition,这个时候在布局之前,就要去计算一下开始布局位置的一个偏移量.
再往下重点就来了
detachAndScrapAttachedViews(recycler)
这个方法的作用是,在一个新的布局之前需要对列表上存在的item对应的viewHolder,把它们分门别类的回收一下,这里只是暂时性的把viewHolder回收到对应的缓存集合里面,为了在接下来布局真正开始的时候能够从 Recycler 这个回收池当中能够复用的到这个 viewHolder,这里也就进入了回收机制当中.
来介绍一下 Rechcler 这个类吧,他是 RecyclerView 的内部类,定义了 RecyclerView 的四级缓存
public final class Recycler{
//#1 不需要重新 bindViewHolder,
//这里存储的viewHolder在复用的时候不需要从新调用 bindViewHolder 从新绑定数据,RecyclerView 认为这种情况的 viewHolder 还会重新出现在屏幕上,
//被这两个集合缓存起来的 ViewHolder ,他们的状态和数据是不会被重置的
ArrayList<ViewHolder> mAttachedScrap;
ArrayList<ViewHolder> mChangedScrap
//#2 可通过setItemCancheSize调整,默认容量大小为2
//列表上下滑动,被滑出去的item 对应的 ViewHolder 会被存放在这个集合里面,再次被滑进屏幕的时候也是不需要重新绑定数据的
ArrayList<ViewHolder> mCachedViews;
//#3 自定义拓展View缓存
//这个属于 RecyclerView 缓存能力的一种拓展,它允许开发者自定义 ViewHolder 的缓存位置和实现方式
ViewCacheExtension mViewCacheExtension;
//#4 根据veiwType存取ViewHolder,可通过setRecyclerViewPool调整,每个类型容量默认为5
//当切仅当二级缓存 mCachedViews; 放不下的时候,才会把 ViewHolder 放入到这里面,
RecyclerViewPool mRecyclerPool;
}
当调用 notifyItemChange() 刷新 RecyclerView 时,我们指定的要刷新的 item 对应的 ViewHolder 就会被放到 mChangeScrap 中,应为 RecyclerView 发生了变化,而屏幕内其他的item 没有发生变化,被回收的时候就会被放到 mAttachScrap 里面.也就是说, mAttachScrap 里面保存的时原封不动的 ViewHolder, mChangeScrap 保存的时发生变化的 ViewHolder.
而当我们的item被划出屏幕的时候,对应的 ViewHolder 就会被缓存到 cachedViews 里面,但是 RecyclerView 认为我们很有可能会再次反向滑动列表,使得刚刚被滑出去的 item 又再次进入屏幕,所以被缓存到 cachedViews 里面的 ViewHolder 被复用的时候,如果它和原来的位置一致,也就是不需要重新调用 bindViewHolder 来重新绑定数据的,但是这个集合有一个默认容量为 2 的限制,当我们的列表一直往下滑的时候,上半部分就会有越来越多的 item 被滑出屏幕,所以在一个新的 ViewHolder 被添加到 mCachedViews 缓存之前,他会校验容量是否已满,如果已经满了,就会把最先添加到里面的 ViewHolder 给移除,放入到 recyclerPool 这个集合里,而 recyclerPool 在缓存阶段是不参与的,只能在复用阶段发挥作用.
上面我们介绍了 detachAndScrapAttachedViews(recycler); 这个方法,在布局前,将所有在显示的HolderView从RecyclerView中剥离,但是为什么在开始一次新的布局之前要调用一次呢?
当我们每次调用 notifyDataSetChanged() 和 notifyItemChanged() 都会触发 LayoutManager 的 onLayoutChildren 这个方法,在这个方法中 LayoutMananger 向列表中填充 item 的时候,就会向 Recycler 中直接获取一个 ViewHolder,如果获取不到的话,就意味着缓存中没有 ViewHolder,那么此时把 item 填充到列表上的时候就会创建一个 ViewHolder ,然后再绑定数据,这样是非常浪费资源的,所以再复用之前要先回收,从缓存中获取item view,在调用 addView 方法的时候,是会判断这个 view是否已经添加过了,所以我们也不用担心造成重复添加 view 的情况.
来看看这个方法的实现.
RecyclerView.Java
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 方法,注意这里是屏幕上可见的,而当我们初次加载而不是调用刷新方法的时候,childCount 是为0的,但是当 RecyclerView 的大小是不确定的并且item的大小也是不确定的时候,就会把能能加载的item 数量都算进去,这也证明上面提到过的要尽量给一个确定的大小,而不是wrap_content,当然 RecyclerView 给 match_parent 是没有这种效果的.
来看看这个 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);
}
}
在回收之前呢,他会判断这个 viewHolder 是否是 isInvalid,这个 isInvalid 就是 item对应的 viewHolder 里面存储的数据已经无效了,发生了变化了,就说明这个 VeiwHolder 已经无效了.还有 isRemoved 判断是否移除,还有这个 hasStableIds 代表列表上的每个 item 是否都有着一个唯一的 long 类型的身份标识,这个方法在默认情况下都是 false.
那么在什么情况下才会走这个分支呢?数据无效了,且没有移除,会想到 notifyDataSetChanged 的时候,recyclerView 会认为列表上的数据集已经全面发生了变化,列表上的 item 都需要重新绑定数据,那么就会给列表上的viewHolder 标记上一个无效的flag,在这里会调用 recycler.recyclerViewHolderInternal(viewHolder)
来跟进一下这个方法
void recycleViewHolderInternal(ViewHolder holder) {
...
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder + exceptionLabel());
}
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
...
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
...
}
这个方法会回收 viewHolder ,mCachedViews.contains(holder) 回收的时候会判断是否存储在了 mCachedViews 里面,如果没有还会判断 mViewCacheMax 里面的缓存数量是否已经溢出了,if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) 如果已经溢出了,就调用 recycleCachedViewAt(0); 把最先添加进来的给移除掉,然后放入到 viewPool 里面,然后接着就调用 mCachedViews.add 会把我们需要缓存的 viewHolder 添加到 mCachedViews 里面,如果设置不用mCachedViewed缓存的话,那回收时就扔进ViewPool里等待复用.
好的,回到 scrapOrRecycleView
else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
如果不是 notifyDataSetChanged,notifyItemChanged 产生的更新,我们看看这个方法 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 是否被移除,是否无效,有没有被更新,如果没有的话.
那么 recyclerView 会认为他在本次布局阶段依旧是想留在屏幕上面的,所以会被存储到 mAttachedScrap里面去.否则也就放入了 mChangedScrap 里面.
好了,到这里这个方法就走完了,还记得是从哪里过来的么?回顾一下吧.
在 onLayoutChildren 里面说到了
detachAndScrapAttachedViews(recycler);
→scrapOrRecycleView(recycler, i, v);
→recycler.recycleViewHolderInternal(viewHolder);和recycler.scrapView(view);
好的,回到 onLayoutChildren 我们继续往下看.
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
// 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
...
// fill towards end
...
} 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;
}
}
...
}
发现了好多 fill ,什么意思呢?其实这里面是开始进入了向列表上填充item的工作,首先是判断了是否需要倒序布局,我们没有主动设置都是 false,来看 else分支,// fill towards start 之前说过,他会从锚点位置从上往下填充item ,填充的时候就是调用了这个 fill 方法, // fill towards end 之后呢又会从锚点位置从下往上进行填充,这两次填充都是调用了 fill 方法,跟进一下,fill(recycler, mLayoutState, state, false);
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);
...
}
...
return start - layoutState.mAvailable;
}
我们看到这里开启了一个 while 循环,还有个 remainingSpace 条件,意思为在当前方向上是否还有可用空间,在这里面实际上就是调用了 layoutChunk 方法,把 item 一个个填充到列表上面,跟进一下
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
...
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();
}
先略过 layoutState.next 看下下面这个方法的大概,把一条item填充到列表上面的时候,就是从 recycler 缓冲池里面得到一个 veiw,得到这个对象之后就会调用 layoutManager 的 addView 方法,在这个 addView 方法里面,首先会判断这个 view 对应的 view holder是否是即将删除的 view ,如果是则把他添加到即将删除的这个集合当中.
接下来就是调用 measureChildWithMargins(view, 0, 0); 这个方法,这个方法在测量的时候考虑到了item上下左右的margin以及 Decorated 需要的额外空间,在接下来会调用 layoutDecoratedWithMargins(view, left, top, right, bottom); 把 view摆到列表上适当的位置上去,linearLayoutMananger 逐一像列表上填充item.
回过头来看一下他在填充的时候获取view的工作流程,View view = layoutState.next(recycler);这个方法其实就是复用机制的一个开始.
先说一下复用的大体流程:
LayoutMananger 每次像列表上填充 item 的时候,都会向 recycler 索取一个 viewHolder,索取 viewHolder 的时候,recycler 就会按照优先级先到,mAttachedScrap和mChangedScrap 这两个一级缓存中查找是否有可复用的 viewHolder,这里面存储的 viewHolder 不需要重新绑定数据,实际上他们两个存储的 viewHolder 也都是屏幕内的 viewHodlder.
如果一级缓存查找不到,就会向二级缓存 mCacheViews 中查找,如果找到了,还需要做一个位置移植性的校验,因为 mCacheViews 里面存储的 viewHodler 是被滑出屏幕的,那么只有相同位置的才可以直接复用,如果被下面滑上来的复用了,就需要重新绑定数据了.
如果二级缓存里面还是找不到,就回去三级缓存,允许开发者自定义的 mViewCacheExt 中去查找,而实际上我们基本没有定义.
所以又会向四级缓存 mRecyclerPool 中查找在这里查找的时候会根据 item 的 viewType 去查找,如果有就返回,如果没有最后还是会调用 adapter 的 createViewHolder 来创建并返回.
看一下 layoutState.next
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
可以看到只有一个 recycler.getViewForPosition 跟进一下
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
发现了 tryGetViewHolderForPositionByDeadline ,尝试性的获取一个 viewHolder ,继续跟进.
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
...
}
这个方法里面首先调用了 holder = getChangedScrapViewForPosition(position); 从名字也能看出是从一级缓存 mChangedScrap 中去获取,看看这个方法吧.
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null;
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
return null;
}
首先判断了一下,啥也没有就返回了,接下来就是对 mChangedScrap 进行了一个遍历,遍历的时候会去校验这个 viewHolder 是否没有被复用过!holder.wasReturnedFromScrap(),还会判断这个 viewHolder 之前列表上的位置和正要填充的位置是否一致,如果一致就会返回,下面的通过id寻找,代码基本差不多.
回到 tryGetViewHolderForPositionByDeadline ,如果上面的方法没有找到,就会调用 getScrapOrHiddenOrCachedHolderForPosition 这个方法继续去寻找.
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
// 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;
}
}
}
...
}
跟进下 getScrapOrHiddenOrCachedHolderForPosition
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
&& !holder.isAttachedToTransitionOverlay()) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
,在这里面首先从 mAttachedScrap 里面去获取,同样是一个遍历,也会进行一个检验,
同样判断了这个viewHolder之前是否没有被复用过,这个viewHolder之前列表上的位置和正要填充的位置是否一致,这个viewHolder 之前的数据有没有被置为无效,这个viewHolder之前有没有被移除,都符合就返回,如果没有找到,就会调用下面的方法 findHiddenNonRemovedView,这个方法是去向正在做删除动画的集合中去查找是否有满足可复用的 viewHolder,这里是向 mHiddenViews 里面寻找,但它并不属于四级缓存里面的,当删除动画执行完的时候这个集合里面的数据也就被移除了,如果说这里面也没有找到,就会去mCachedViews里面去寻找,同样是一个遍历,然后检验.
好,继续回到上面的 tryGetViewHolderForPositionByDeadline 中,通过 getScrapOrHiddenOrCachedHolderForPosition 获取 viewHolder,我们来看看如果返回了一个 viewHolder 会怎么样,如果 viewHolder 不为空,会调用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 (!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;
}
这里我们我们看到了他判断了viewHolder 对应的 itemType 和正在填充的 viewType 是否一致,type != holder.getItemViewType().
接下来还会判断,如果给 adapter开启了这个item
的唯一身份标识,还会判断这个item前后id的一致性,只有都通过才会复用.
继续回到 tryGetViewHolderForPositionByDeadline
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
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());
}
}
}
...
}
如果 getScrapOrHiddenOrCachedHolderForPosition 返回 null,还会去 mViewCacheExtension 中去查找,而一般情况下开发者不会去自定义这个缓存策略,所以基本为空.
继续往下看
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
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);
}
}
...
}
最后会去 mRecyclerPool 里面根据 viewType 去查找,holder = getRecycledViewPool().getRecycledView(type) ,如果这里还是查找不到,那么就会调用 holder = mAdapter.createViewHolder(RecyclerView.this, type);去创建一个新的 viewHolder.
总结
插拔式的设计模式增加灵活性
尽量指定RecyclerView 和 item 的宽和高
尽量使用定向刷新 notyfyItemChanged