作为三大常用控件的recycleview和scrollview有着同样广泛的用途,但是在源码分析难度上却不是一个等量级的,可以说,recyclview的控件是我可能过最复杂的控件了,所以务必要带着问题去分析,不然完全没有分析下去的勇气,先说几个问题吧:
1.recycleview有了layoutmanager为何setadapter后就把数据显示在屏幕上
2.recycleview的缓存是如何进行的
3.recycleview的更新流程是怎么样的
4.recycleview的局部更新和传统更新的差别
暂时先列这4点吧,因为recycleview实在是有太多东西要分析的了
1.首先列举最常规的情况:我们在onCreate中初始化,LinearLayoutManager然后在setAdapter了,假设在setadapter时已经有数据了
首先进入到LinearLayoutManager中
public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
setAutoMeasureEnabled(true);
}
很简单这里设置了自动measure为true
然后在setLayoutManager中有个这个方法
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
也就是最大缓存的view,默认是2,记住,等会又会调用到它,其他的方法无非就是重置的操作
而在setAdapter方法里会间接调用
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
removeAndRecycleViews();
}
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
setDataSetChangedAfterLayout();
}
这里由于没有重复调用adapter,所以也没有老数据,简单来说就是注册了观察者以及告诉recycleview,adapter被onAttached了
其中这两个地方都调用了requestLayout,也就是说会调用onMeasure和onLayout方法,在回到onMeasure中
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
....
由于在xml中给recyclview的宽高是EXACTLY的,所以这里就return了
并执行了onMeasure方法
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);
}
可以看到一开始确定了recyclview的宽高了,不了解onMeaure和requestLayout人可以看下关于onmeasure,onLayout, requestLayout ,invalidate你可能忽视的细节
既然在onMeasure中没做啥,来分析下onLayout方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
其中dispatchLayout方法是重点要分析的
void dispatchLayout() {
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();
}
可以看到这里有三个重要的方法,先来看下dispatchLayoutStep1方法
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
eatRequestLayout();
mViewInfoStore.clear();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
saveFocusInfo();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
...
这里有一个关键点是processAdapterUpdatesAndSetAnimationFlags方法和
第一个从名字上来看就是当adapter被局部更新时候所调用的方法,这个等会分析
紧接着分析dispatchLayoutStep2方法
private void dispatchLayoutStep2() {
eatRequestLayout();
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();
resumeRequestLayout(false);
}
重点在mLayout.onLayoutChildren方法
首先找到他的
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
if (DEBUG) {
Log.d(TAG, "is pre layout:" + state.isPreLayout());
}
if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
...
这里会走到if条件里面去,目的就是给开始放置位置赋值,由于没有设置reverse属性,所以 mAnchorInfo.mLayoutFromEnd的boolean是false
并且mAnchorInfo.mPosition的值是0
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
而后面会走到mAnchorInfo.mLayoutFromEnd为false的判断里,接调用fill方法,在此期间,会拿到recyclview的高度,然后赋值给mLayoutState.mAvailable属性
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
mLayoutState.mCurrentPosition = itemPosition;
mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
mLayoutState.mOffset = offset;
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}
来看下fill方法
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != 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;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (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 != 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;
}
其中会走到while循环里去,调用layoutChunk(recycler, state, layoutState, layoutChunkResult);方法,此时在recycle里只有一个mViewCacheMax,其他的recycleviewpool和cach都没有创建呢
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;
}
LayoutParams params = (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();
}
在这方法里有两个关键点,第一个是获取到view第一个view,这里关键代码在缓存这块细说,主要记住在这方法里没有任何缓存的话会调用我们熟悉的onCreateViewHolder和onBindViewHolder方法, 关键点二就是被加到了recycleview里面了,然后会获取每个item的高度,从而把remainingSpace和layoutState.mAvailable重新赋值,当不满足条件的情况下,就退出循环了,而在fill结束以后 startOffset和endOffset就是所加view实际的高度了
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;
}
}
也就是说最基本的dispatchLayoutStep2被分析完了,在没有动画和任何缓存的情况下还是很简单的,至此第一个问题也就有答案了,主要就是添加显示在屏幕view,然后add进去,而dispatchLayoutStep3里面主要对动画的处理和恢复初始值,这里就不分析了,接下来分析比较绕的缓存这一块
缓存
缓存的view的获取主要在tryGetViewHolderForPositionByDeadline里
@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;
}
首先我们来大概分析一下,这里在源码中也有注释,第一步是获取改变的废弃的view,前提是是在预布局阶段,那何时会有预布局呢,其实你如果单个view notify的话就会有预布局了,具体的后文会说,正常的显然不走此流程,第二种情况是按照位置获取废弃的view,这其实是当你滑动的时候,会将部分view放进去,具体的看后文,现在都是获取不到的,第三种是按照id获取废弃的view,这基本用不到可以跳过,第四种是根据ViewCacheExtension获取缓存的,当然源码中并不会给我们实现,显然意义不大,最后是根据getRecycledViewPool也就是相同的type类型来获取holder,当都获取不到的时候就调用createViewHolder方法来获取,当获取完以后会把此type放进scrapdata中,会间接调用getScrapDataForType方法
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
而后会调用bindViewHolder从而调用onBindViewHolder方法
public final void bindViewHolder(VH holder, int position) {
holder.mPosition = position;
if (hasStableIds()) {
holder.mItemId = getItemId(position);
}
holder.setFlags(ViewHolder.FLAG_BOUND,
ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
holder.clearPayload();
final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
if (layoutParams instanceof RecyclerView.LayoutParams) {
((LayoutParams) layoutParams).mInsetsDirty = true;
}
TraceCompat.endSection();
}
在分析出了缓存以后,我们来看下这些主要的缓存是如何触发的,我们轻轻滑动recycleview,很明显在其onTouchEvent中,然后会间接调用到
scrollVerticallyBy方法
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
从而调用scrollBy方法,间接的会调用fill方法,在fill方法中
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
有这样一个分之,触发条件是比如在屏幕上的view是0,1,2,在把第三个view完全滑出来的时候,第0个view就会被回收了,从而调用了removeAndRecycleViewAt方法
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
// 注释一
removeViewAt(index);
// 注释二
recycler.recycleView(view);
}
注释一主要是给adapter回掉的,回调的adapter的onViewDetachedFromWindow方法,注释二是添加此view到recycler里去的,着重看下注释二,会间接调用recycleViewHolderInternal方法
void recycleViewHolderInternal(ViewHolder holder) {
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw new IllegalArgumentException(
"Scrapped or attached views may not be recycled. isScrap:"
+ holder.isScrap() + " isAttached:"
+ (holder.itemView.getParent() != null) + exceptionLabel());
}
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed "
+ "from RecyclerView before it can be recycled: " + holder
+ exceptionLabel());
}
if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ " should first call stopIgnoringView(view) before calling recycle."
+ exceptionLabel());
}
//noinspection unchecked
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
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--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.
// TODO: consider cancelling an animation when an item is removed scrollBy,
// to return it to the pool faster
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists"
+ exceptionLabel());
}
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
可以看到由于第一次的cachedViewSize是0,而且mViewCacheMax的个数由于预加载的原因(recycleview在25以后做了优化,会在同一帧提前加载布局)这里本来是二的cache,变成了三,最后会走到里
mCachedViews.add(targetCacheIndex, holder);
cached = true;
可以说第一个view被划出去的时候放到了mCachedViews里面,现在展示的列表的view是1,2,3,当我们重新滑到第0个时,此时view就会从
mCachedViews里取出来,并且从列表中移除掉此view
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;
}
这里主要走的是cacheSize的方法,也就是说从mCachedViews里恢复的view是可以直接add的,不用调用任何的回掉,也就是最高效的
也就是recycleview的view都是先add在remove的
此时我们把视图初始化,回到0,1,2的位置,我这里手动关闭了预加载,也就是说mViewCacheMax的是二,来分析下,回收池的回收情况
这里滑到了2,3,4的位置,显然cache的size是二了,我们在往上滑
这里走到了如下代码中
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
来看下recycleCachedViewAt的方法
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
这里我们看到会把第一个的cached从列表里移出去,同时把第一个的view放进了recycledViewPool中
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
这个dispatchViewRecycled是放到缓存池的标志
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
这里我们看到其实是每一个type最大有5个scrap,因为recycleview的缓存池是和type相关的,并不是和位置相关的,继续看下recycleViewHolderInternal方法会发现,此时它又把第二个位置给放到缓存队列里了,所以现在的结果是
第0个在缓存池里,第一和第二个在cache里,第三,第四,第五个在列表的显示,继续滑动,会发现从第六个开始,就不再调用onCreateViewHolder方法了,但是由于是缓存池,缓存的是type,所以
onBindViewHolder方法还是会调用的原因如下
tryGetViewHolderForPositionByDeadline方法
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);
}
由于是缓存池会让此holder需要更新,所以会走到tryBindViewHolderByDeadline从而调用onBindViewHolder方法,而从cache恢复的却不会
现在出一个小问题:
recycleview的数量有20个都是同一个类型的type,界面上有3个itme,一共会调用几次onCreateViewHolder呢?(这里不考虑25+的预加载的情况)
当然第二个的答案也就出来了,第三和第四个的情况下篇分析把,不然篇幅太长了