在开始缓存前,我们先从RecyclerView的绘制开始分析,都知道RecyclerView的绘制是在LayoutManager中,真正执行LayoutManager绘制的地方dispatchLayoutStep2(),同样,放上代码:
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
...
}
mLayout就是LayoutManager,以LinearLayoutManager为例,跟进去看onLayoutChildren方法做了什么:
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;
// 1. 确定布局方向
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
//2. 计算锚点位置
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())) {
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
if (DEBUG) {
Log.d(TAG, "Anchor info:" + mAnchorInfo);
}
...
//3. item 放入缓存
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
// noRecycleSpace not needed: recycling doesn't happen in below's fill
// invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
// 4. 具体填充方法
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
// changes may cause gaps on the UI, try to fix them.
// TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
// changed
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
}
}
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
if (DEBUG) {
validateChildOrder();
}
}
代码1处解决布局方向,实际就是纵向还是横向布局;代码2 处在锚点信息已过期或者滚动位置不是初始位置,或者预存储状态不为null,则重置锚点,默认mAnchorInfo.mValid 为false,所以会进入if逻辑中,mLayoutFromEnd的值,在VERTICAL下mShouldReverseLayout为false ,mStackFromEnd默认也为false,所以异或的结果mLayoutFromEnd为false,其中mStackFromEnd是用来确定是否为正向布局,简单来说在VERTICAL下mStackFromEnd为false为从上到下,为true反之。代码3处放入缓存,稍后分析;先看一下确定锚点后怎么填充的,具体填充方法就是代码4处,看fill是如何做的:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
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);
...
return start - layoutState.mAvailable;
}
fill 核心的就是调用layoutChunk方法,点进去继续看:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
这个方法就是关键的布局了,首先是调用layoutState.next(recycler)方法,这个方法就是在缓存中取出需要的view,然后继续就是往ViewGroup中addview,毕竟RecyclerView还是继承自ViewGroup。之后就是测量子view的宽高,在layout到指定位置。
到此,我们在总结一下绘制的过程:
RecyclerView的绘制是由LayoutManager来处理,处理过程中先确认布局方向,在根据布局方向确定锚点位置,之后是在确定填充方向(toStart 、toEnd),最后先存入缓存再在缓存中拿到子View,在addView到ViewGroup中
四级缓存
在绘制流程中,会将子view放置到缓存中,缓存怎么处理的,又有哪些缓存?首先,关于缓存都知道其存在四级缓存,先分别介绍各个缓存:
缓存级别 | 对应属性 | 含义 |
---|---|---|
一级缓存 | mAttachedScrap和mChangedScrap | detach的view相关, mAttachedScrap存储的是当前还在屏幕中的ViewHolder。mChangedScrap存储的是数据被更新的ViewHolder ,通常在预布局中使用。 |
二级缓存 | mCachedViews | 默认大小为2,remove掉的view会先进入此缓存 |
三级缓存 | mViewCacheExtension | 需自定义的,用不到 |
四级缓存 | mRecyclerPool | 按照viewType缓存holder,每个type对应的缓存大小为5 |
需要注意的是一级缓存跟detach的view相关,其他是remove掉的,这两个的区别可以看这篇文章
简单的介绍之后,我们还是在源码角度看看怎么做的,还是在onLayoutChildren方法中,看到在找到锚点后会执行detachAndScrapAttachedViews
方法,点进去看看:
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);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
首先得到child数量,这地方需要注意的是getChildCount()
并不等同于adapter.getItemCount()
,而是attach到recyclerView的数量或者可以看的到的child。其次就是scrapOrRecycleView开始放入缓存,我们看if逻辑判断,当viewHolder是invalid状态并且没有移除且没有设置StableIds标识会执行removeView,invalid的状态改变,在分析notifyDataSetChanged时在说,这地方直接看else的代码,先detach掉在scrapView,scrapView又是怎么操作的:
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);
}
}
当调用notifyDataSetChanged时ViewHolder.FLAG_INVALID和holder.isUpdated()为true,所以会进入if控制中,然后就是添加到一级缓存mAttachedScrap中,总结一下刚才的过程:
detachAndScrapAttachedViews 会将正在显示的View 存入到一级缓存mAttachedScrap。
放入之后在什么地方会用到,回到layoutChunk方法,第一行调用了View view = layoutState.next(recycler);现在看看是怎么取到View的:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
调用了recycler.getViewForPosition(mCurrentPosition);,跟进去最后会调用到tryGetViewHolderForPositionByDeadline:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) 预布局情况从mChangedScrap缓存中取
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);
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;
}
}
//3)mViewCacheExtension 获取
if (holder == null && mViewCacheExtension != null) {
...
}
if (holder == null) { // fallback to pool
...
//4) 从RecycledViewPool中获取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
return holder;
}
1处调用getScrapOrHiddenOrCachedHolderForPosition,这个方法做的事情很简单就是依次从mAttachedScrap、hiddenViews和mCachedViews中获取holder,需要注意的hiddenViews并不算缓存,这个只和动画有关,这个以后有时间在理解,2处会判断是否设置了StableIds,设置了就调用getScrapOrCachedViewForId方法,这个方法会依次从mAttachedScrap和mCachedViews中获取holder,getScrapOrHiddenOrCachedHolderForPosition
和getScrapOrCachedViewForId
调用都会在mAttachedScrap和mCachedViews中获取holder,不同的地方是一个通过position,一个通过Id,也就是说通过position直接拿到的holder不用去判断ItemViewType是否一致,通过Id需要判断ItemViewType的类型是否一致。4处就是在RecycledViewPool中获取了,具体看看这里面如何拿到的:
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
这地方先通过ItemViewType拿到指定类型的缓存,然后在得到指定的holder。
最后如果RecycledViewPool中没有,就调用mAdapter.createViewHolder
问题
- notifyItemChanged 和notifyDataSetChanged区别
我们看各自源码做了什么
@Override
//notifyDataSetChanged
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
//notifyItemChanged
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
markKnownViewsInvalid();
}
void markKnownViewsInvalid() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
}
不一样的地方就是notifyDataSetChanged最后会添加flag值ViewHolder.FLAG_UPDATE和ViewHolder.FLAG_INVALID,这两个的作用是什么,我们回到缓存相关的代码
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
看到会在if判断中执行,也就是会先remove掉view,在放入二级缓存中,这个复用性要比detach的低一些,具体可以参考这篇文章,所以remove掉在add回来可能就会有图片闪动的问题,这也是notifyDataSetChanged效率低的原因。
- 如何提高缓存复用或StableIds 如何提高效率的
前面也说过,notifyDataSetChanged会效率低些,不过在重写mAdapter.hasStableIds()时,可以让缓存放入一级缓存中,另外在复用时通过ViewType方式来获取ViewHolder,会优先到一级或者二级缓存里面去寻找,而不是直接去RecyclerViewPool里面去寻找。
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
参考:
RecyclerView 源码分析(三) - RecyclerView的缓存机制
RecyclerView机制分析: Recycler