抽出了几天的时间,看了一下RecyclerView和ListView的源码,对这两个控件有了更深的理解。RecyclerView是平时经常用的,一直以为已经完全替代了ListView。但是深层次的学习了这两个View之后,才明白是各有所长吧。ListView的源码和原理,相对来说更加简单些,这里推荐郭霖的文章《Android ListView工作原理完全解析,带你从源码的角度彻底理解》,郭神出品,必是精品!2015年的文章了,但还没有过时。在不同的SDK中,ListView源码会有一些变化,View的绘制也有略微差别,但是原理上没有变化。郭霖的这边文章虽然贴了大量的源码,但是每一步都很细,所以本篇文章我也打算沿用这样的风格。
目录
一、Recycler
二、绘制流程
三、滑动
四、RecyclerView和ListView的缓存比较
五、注意
Recycler这个类是缓存的核心类,是RecyclerView的内部类,和ListView的RecycleBin作用一样,这里我就不过多的带入ListView的内容了。简单说一下,Recycler类中比较关键的几个属性和方法:
mAttachedScrap和mChangedScrap(一级缓存):这两个集合是RecyclerView的一级缓存,但是我测试过程中发现正常情况下,并没有向这两个集合中加入ViewHolder,其他情况有没有把ViewHolder加入到集合中我不太清楚,即便有,这两个集合也不是以及缓存了。网上好多博客都这么说的,我也很奇怪,有知道的朋友希望在留言区告诉我~
mCachedViews(二级缓存):从屏幕上完全移除后的ViewHolder都会加入到mCachedViews
mViewCacheExtension(三级缓存):自定义缓存,这个缓存由我们自己定义。这是一个抽象类,内部的方法需要覆写,存取的规则由我们自己定义。我没有使用过这级的缓存,现有的缓存完全支持我所了解的需求
mRecyclerPool(四级缓存):如果mCachedViews的数量达到阈值,那么就会移除mCachedViews中第一个ViewHolder(也就是最早加入的ViewHolder),加入到mRecyclerPool中。mRecyclerPool并不是数组,使用的是SpareseArray,以viewType为key,ViewHolder的数组为value进行存储的。也就是说,mRecyclerPool是根据viewType进行存储的。
对于缓存的获取过程,我画了一张图
我们暂时先了解到这里,之后在绘制和滑动流程中,再细致的了解,这里我们先混一个眼熟
View的绘制流程,大家应该都知道,measure--->layout--->draw。RecyclerView不例外,也是这个流程,我们先看onMeasure方法
@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);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode ==MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == RecyclerView.State.STEP_START) {
dispatchLayoutStep1();
}
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;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
....
}
}
因为篇幅的原因,我省略了部分代码和一些英文注释。mLayout其实就是我们所设置LayoutManager。第7行中的mLayout.isAutoMeasureEnabled方法,在LayoutManager的子类中都有覆写,返回值都为true。我们看到第11行的代码mLayout.onMeasure,也就是说RecyclerView的测量过程,委托给了LayoutManager方法。第13行代码中判断了RecyclerView的宽高模式,如果给RecyclerView指定了高度或者充满了父布局,那么onMeasure方法到这里就结束了,后续子条目的摆放不在执行,而是在onLayout方法中进行,这是为什么呢?其实我们可以思考一下,如果RecyclerView的宽或高是wrap_content,那么RecyclerView的宽高值会随子条目的摆放而变化;如果RecyclerView的高度确定了,那么直接摆放子条目即可。无论是在onMeasure中,还是onLayout方法中,子条目摆放的方法没有区别,分别是dispatchLayoutStep1和dispatchLayoutStep2。dispatchLayoutStep1方法中做了和动画相关及一些字段的赋值,和真正的layout关系不大,核心操作在dispatchLayoutStep2方法中,我们进入查看:
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(RecyclerView.State.STEP_LAYOUT | RecyclerView.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 = RecyclerView.State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
第11行代码mLayout.onLayoutChildren是子条目的摆放过程,我们进入查看
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
这个方法是空,可以推测出,这个方法一定在LayoutManager的子类中有实现,我们以LinearLayoutManager为例,进入查看
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// fill towards end
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;
}
// fill towards start
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;
}
}
}
这个方法的代码非常多,也做了很多的工作,主要做了两个工作:确定锚点和绘制。什么是锚点呢?也就是说RecyclerView开始绘制的点。确定锚点后,再根据绘制方向进行绘制。可以根据下图理解:
红色点为锚点,第一个图和第二个图,都是从锚点开始向箭头方向绘制;第三个图片,锚点在中间位置,或先向上再向下,或反之,具体怎么方向由代码确定。
看第4行的mAnchorInfo.mLayoutFromEnd,这个值表示绘制过程的方向。我认为我们没有必要去关心绘制的方向,这个对我们来说不重要。假设我们锚点在屏幕顶端(第二个图),第10行和第20行的fill只会执行一个,如果锚点在中间的话,这两个方法都会执行,这应该是很好理解,我们进入fill方法
int fill(RecyclerView.Recycler recycler, LinearLayoutManager.LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LinearLayoutManager.LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
第12行代码中的remainingSpace,从命名上我们基本上知道了具体意义,剩余的空间。也就是RecyclerView在平铺子条目时,剩余的空间,如果没有放子条目,那么剩余空间等于RecyclerView的高度(假设是LinearLayoutManager,纵向滑动),每摆放一个子条目,剩余空间则少一个子条目的高度。14行是一个while循环,所以这里会不断的摆放子条目,直到RecyclerView被完全冲慢,或者条目全都摆放完毕。每次摆放是,调用的方法是layoutChunk,我们看一看:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.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
== LinearLayoutManager.LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LinearLayoutManager.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 == LinearLayoutManager.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 == LinearLayoutManager.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();
}
第4行的代码很重要,获取缓存的核心代码,但是我们暂时先跳过,因为此时缓存中还没有任何数据,在滑动分析的时候,我们再回到这里。第18行和第20行代码,相信大家都明白它们的含义,就是把子条目放到RecyclerView中。绘制流程,到此完毕。onLayoutChildren方法中,fill执行了3次,我认为这里无伤大雅,只不过是准确性的判断。
RecyclerView的滑动,肯定会执行onTouchEvent方法,看一下这个方法
@Override
public boolean onTouchEvent(MotionEvent e) {
.....
switch (action) {
.....
case MotionEvent.ACTION_MOVE: {
.....
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
.....
}
.....
return true;
}
滑动时,事件类型为MotionEvent.ACTION_MOVE,所以我们只看这部分代码。第11行代码是关键的入口,我们进入方法内部:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
scrollStep(x, y, mScrollStepConsumed);
consumedX = mScrollStepConsumed[0];
consumedY = mScrollStepConsumed[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}
进入到第7行scrollStep方法中
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
再强调一下,mLayout我们假设为LinearLayoutManager,所以我们看15行代码mLayout.scrollVerticallyBy方法
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
return 0;
}
这个方法是空的,所以应该是在LinearLayoutManager中覆写了,查看LinearLayoutManager中的此方法
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
进入scrollBy方法中看
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true;
ensureLayoutState();
final int layoutDirection = dy > 0 ? LinearLayoutManager.LayoutState.LAYOUT_END : LinearLayoutManager.LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
在第12行中,我们又看见了fill方法,在绘制的时候我们说过这个方法,根据剩余的距离来摆放子条目。从这里进入的fill是一个方法,所以作用也是如此。我们想象一下,RecyclerView中最后一个条目一定是展示部分或正好展示全部,当我们向上慢慢滑动时,最后一个条目的底部会逐渐的进入到屏幕内,当底部进入到屏幕中时,fill方法中的remainingSpace的值会大于0,因此进入到while循环中,进入到layoutChunk方法,我们再看一下layoutChunk方法
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result) {
View view = layoutState.next(recycler);
......
}
layoutState.next方法我们简单说过,是获取缓存的ViewHolder,这次我们进入看看
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
进入第5行的getViewForPosition方法中
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
View view = tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
return view;
}
getViewForPosition有两个重载方法,最后会执行到第7行的tryGetViewHolderForPositionByDeadline方法,我们进入看一下
@Nullable
RecyclerView.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;
RecyclerView.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(RecyclerView.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(RecyclerView.ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, RecyclerView.ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = RecyclerView.ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= RecyclerView.ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemAnimator.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 RecyclerView.LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (RecyclerView.LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (RecyclerView.LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (RecyclerView.LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
这个方法我没有做任何的删减,因为都比较重要,执行的操作基本上和我所画的缓存流程图一样。我们继续说滑动时的流程,进入到这个方法后,会执行第18行代码,我们进入看看
RecyclerView.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 RecyclerView.ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(RecyclerView.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 RecyclerView.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(RecyclerView.ViewHolder.FLAG_RETURNED_FROM_SCRAP
| RecyclerView.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 RecyclerView.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;
}
我们在这里看到了两个眼熟的变量,mAttachedScrap和mCachedViews。mAttachedScrap此时是空的,所以略过。而mCachedViews是有值的,所以获取到了ViewHolder。等等等等,mCachedViews怎么突然就有值了,之前也没有说过什么时候向其中存过值啊,什么时候的事啊?!是的,因为还没有说到,关于向mCachedViews存值的过程,我们还要回到onToucheEvent方法中
@Override
public boolean onTouchEvent(MotionEvent e) {
.....
switch (action) {
.....
case MotionEvent.ACTION_MOVE: {
.....
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
.....
}
.....
return true;
}
我们看到第18行代码,进入到mGapWorker的postFromTraversal中查看
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
recyclerView.post(this);
}
}
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
第9行代码 recycler.post(this),那么mGapWorker类中一定有一个run方法,我们看一下
@Override
public void run() {
try {
TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query most recent vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
if (latestFrameVsyncMs == 0) {
// abort - either no views visible, or couldn't get last vsync for estimating next
return;
}
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
TraceCompat.endSection();
}
}
第29行代码,我们进入perfetch中
void prefetch(long deadlineNs) {
buildTaskList();
flushTasksWithDeadline(deadlineNs);
}
里面有两个方法,都很重要,我们一个一个看,先看buildTaskList方法
private void buildTaskList() {
.....
for (int i = 0; i < viewCount; i++) {
......
for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
final GapWorker.Task task;
if (totalTaskIndex >= mTasks.size()) {
task = new GapWorker.Task();
mTasks.add(task);
} else {
task = mTasks.get(totalTaskIndex);
}
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
task.immediate = distanceToItem <= viewVelocity;
task.viewVelocity = viewVelocity;
task.distanceToItem = distanceToItem;
task.view = view;
task.position = prefetchRegistry.mPrefetchArray[j];
totalTaskIndex++;
}
}
.....
}
篇幅原因,我贴部分代码,其中第14行代码是关键,distanceToItem是屏幕中最后一个条目的底部距离屏幕底部的距离,值为正数;viewVelocity是偏移量x的绝对值加上偏移量y的绝对值。简单来说,滑动的距离大于distanceToItem时,表示新的item要展示了,所以要创建新的,此时task.immediate为true。我们再看另一个方法flushTasksWithDeadline方法
private void flushTasksWithDeadline(long deadlineNs) {
for (int i = 0; i < mTasks.size(); i++) {
final GapWorker.Task task = mTasks.get(i);
if (task.view == null) {
break; // done with populated tasks
}
flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
再进入flushTaskWithDealine方法中
private void flushTaskWithDeadline(GapWorker.Task task, long deadlineNs) {
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
if (holder != null
&& holder.mNestedRecyclerView != null
&& holder.isBound()
&& !holder.isInvalid()) {
prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
}
}
第2行用到了task.immediate这个值,如果这个值为true的话,taskDeadlinedNs为RecyclerView.FOREVER_NS,然后传入到了第3行代码的方法里,第3行代码返回了一个ViewHolder,我们进入查看
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, int position, long deadlineNs) {
if (isPrefetchPositionAttached(view, position)) {
// don't attempt to prefetch attached views
return null;
}
RecyclerView.Recycler recycler = view.mRecycler;
RecyclerView.ViewHolder holder;
try {
view.onEnterLayoutOrScroll();
holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
if (holder != null) {
if (holder.isBound() && !holder.isInvalid()) {
// Only give the view a chance to go into the cache if binding succeeded
// Note that we must use public method, since item may need cleanup
recycler.recycleView(holder.itemView);
} else {
// Didn't bind, so we can't cache the view, but it will stay in the pool until
// next prefetch/traversal. If a View fails to bind, it means we didn't have
// enough time prior to the deadline (and won't for other instances of this
// type, during this GapWorker prefetch pass).
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
} finally {
view.onExitLayoutOrScroll(false);
}
return holder;
}
我们在第11行看见到了熟悉的方法tryGetViewHolderForPositionByDeadline,这个方法不就是RecyclerView从缓存中获取新条目的方法嘛,从这里进入的话,会如何执行呢?我们再进入看看
@Nullable
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
.....
if (holder == null) {
.....
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);
....
}
}
.....
return holder;
}
代码太多了,我进行了阉割。这个方法的代码就是从缓存中获取ViewHolder,现在缓存中都是空的。我们看第9行代码,deadlineNs这个变量,刚刚不就是被赋值为FOREVER_NS吗,所以执行了第14行的代码mAdapter.createViewHolder方法,这段代码很眼熟吧,就是我们自定义Adapter中覆写的方法,这时候ViewHolder就被创建出来了,那是什么时候被放入到缓存的呢?我们需要回到上一个方法prefetchPositionWithDeadline中
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, int position, long deadlineNs) {
if (isPrefetchPositionAttached(view, position)) {
// don't attempt to prefetch attached views
return null;
}
RecyclerView.Recycler recycler = view.mRecycler;
RecyclerView.ViewHolder holder;
try {
view.onEnterLayoutOrScroll();
holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
if (holder != null) {
if (holder.isBound() && !holder.isInvalid()) {
// Only give the view a chance to go into the cache if binding succeeded
// Note that we must use public method, since item may need cleanup
recycler.recycleView(holder.itemView);
} else {
// Didn't bind, so we can't cache the view, but it will stay in the pool until
// next prefetch/traversal. If a View fails to bind, it means we didn't have
// enough time prior to the deadline (and won't for other instances of this
// type, during this GapWorker prefetch pass).
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
} finally {
view.onExitLayoutOrScroll(false);
}
return holder;
}
我们看第18行和第24行,第18行是把ViewHolder加入到了mCacheViews,而24行是把ViewHolder加入到RecycledViewPool中,当然这里是加入到mCacheViews中。这样从onTouchEvent的scrollByInternal进入获取的新ViewHolder不就获取到了吗。稍等稍等!!有同学可能发现了,在onToucheEvent中scrollByInternal是先执行的,而mGapWorker.postFromTraversal是后执行的,后执行的代码创建,先执行的获取?这不是错了吗?而且mGapWorker还是子线程,执行结果的时间都不确定,更不对了啊。从scrollByInternal进入,有一个fill方法,大家还记得吗,fill方法中有一个remainingSpace变量,在滑动前,如果最后一个条目没有完全展示,那么这个值就是负数,向上滑动时,这个值逐渐向0靠近;执行mGapWorker.postFromTraversal方法时,有一个buildTaskList方法,其中有一个变量为distanceToItem,这个变量和remainingSpace含义相似,但是这个值确永远大于0,相当于是remainingSpace的绝对值。我举一个例子:比如滑动RecyclerView是,最后一个条目即将完全露出,此时remainingSpace等于-5,而remainingSpace则等于5,假设滑动的偏移量为10,那么创建ViewHolder的流程,肯定要比从缓存获取的流程前置。滑动流程,至此结束了
(1)RecyclerView是四级缓存,而ListView是两级缓存,缓存策略上RecyclerView更有优势
(2)RecyclerView中RecycledViewPool可以供多个RecyclerView公用,这是ListView所没有的
(3)从RecyclerView的mCachedViews获取的View不需要重新bindView,因为缓存存储的是ViewHolder;而ListView中的缓存存储的是View,需要重新绑定;如果是从RecyclerView的RecycledViewPool中获取的话,也需要重新bindView一次
第3点我当时有些不太明白,我记得我是用ListView的时候没有重新绑定啊,如下
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if(convertView==null){
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_layout,parent,false);
viewHolder = new ViewHolder();
viewHolder.titleView = view.findViewById(R.id.title_view);
view.setTag(viewHolder);
}else{
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.titleView.setText(dataList.get(position));
return view;
}
其实,这应该是算是一种优化的写法(我一直都不知道。。。),而非优化的写法如下
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
(1)在不同的SDK版本中,onMeasure,onLayout,onDraw执行的次数不一样,我使用的是SDK29,onLayout只执行了一次
(2)RecyclerView源码调试方法有很多种,我使用了最笨的一种,把RecyclerView的源码复制了一份,这样更方便打log,大概有20个左右文件吧,还能接受
(3)如果直接在RecyclerView中打断点的话,一定要注意:最好使用模拟器,因为不同品牌的手机源码可能有变动;SDK版本最好和模拟器的版本相同
有问题的同学,欢迎在留言区讨论
参考文章:
《Android ListView工作原理完全解析,带你从源码的角度彻底理解》
《【腾讯Bugly干货分享】Android ListView与RecyclerView对比浅析--缓存机制》
《【进阶】RecyclerView源码解析(一)——绘制流程》
《【进阶】RecyclerView源码解析(二)——缓存机制》
《RecyclerView与ListView 对比浅析:缓存机制》