深入了解RecyclerView
RecyclerView缓存机制
说到Android的布局,ListView肯定是最常用的控件之一了,而提到ListView就不得不说起RecyclerView了,强大的RecyclerView不仅可以实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。Android官方更加推荐使用RecyclerView。官方给RecyclerView的解释:
A flexible view for providing a limited window into a large data set.
翻译过来的意思就是一个为大数据集提供有限窗口的灵活视图。如果我们想用好这个RecyclerView,那么对于他的缓存机制我们就有了解的必要了,下面我们就来看下RecyclerView 的缓存机制
RecyclerView的缓存类
RecyclerView 有几个比较重要的缓存类:Recycler,RecycledViewPool,ViewCacheExtension
Recycler
我们先来看下源码( 部分)
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
...省略...
}
这个Recycler 类里就基本包含了所有的缓存类,首先说下他的成员变量
- mAttachedScrap :这个变量用一个ArrayList存放了ViewHolder,这里的ViewHolder的数据是不做修改的,当使用到这里的缓存的时候是不用走adapter的绑定方法的。
- mChangedScrap :这个变量和上面的mAttachedScrap 一样,不过有区别的是这里存放的是已经变了的ViewHolder,使用到了这个缓存的话是需要重新走adapter的绑定方法的。
- mCachedViews :这个存放的是已经与RecyclerView分离了的ViewHolder,但是他依然保存了ViewHolder的信息,比如position,数据等。这里的默认的大小为2,就是mViewCacheMax这个变量,不过我们可以人为的改变他的大小,用到了RecyclerView.setItemViewCacheSize()方法。
- mRecyclerPool:这个就是一个缓存类,从名字中我们也可以看到,是一个缓存池。进入这个类我们可以看到:
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
...省略...
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
private int mAttachCount = 0;
...省略...
}
- 首先看到的就是这个常量DEFAULT_MAX_SCRAP ,这个指的就是这个缓存池默认的缓存数,这个数量也是可以修改的,而且这个数量指的并不是这个池子可以缓存这么多,而指的是每个itemType可以缓存的ViewHolder数量。
- 接下来就是静态内部类ScrapData ,这里面也创建了一个存储ViewHoler的ArrayList,和一个mMaxScrap ,表示了这个默认的缓存数为5。
- 最后就是SparseArray ,它存储了我们上面的ScrapData ,这样的话就可以通过区分itemType来缓存ViewHolder了。
- mViewCacheExtension:是一个抽象类,开发者可以自己去定义这个缓存类,我们可以根据实际场景来制定。
RecyclerView 的加载过程
上面介绍了RecyclerView 的几个比较重要的缓存类,那他们之间是怎么样工作的呢?下面我就就来看一下RecyclerView 的加载过程,一个view从创建到显示出来大概要经过measure,layout,draw这么几个方法,我们来看measure这个过程,定位到RecyclerView 的onMeasure()方法:
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
}else{
...
}
}
省略了一些无关的代码,其中比较重要的方法dispatchLayoutStep1()和dispatchLayoutStep2(),我们可以看到影响dispatchLayoutStep1()方法运行的条件是mState.mLayoutStep == State.STEP_START,我们进入源码可以看到:State.STEP_START是一个常量,值为1,mState.mLayoutStep指向了一个变量
int mLayoutStep = STEP_START;
条件为true所以首先进入到dispatchLayoutStep1()
private void dispatchLayoutStep1() {
if (mState.mRunSimpleAnimations) {
......
}
if (mState.mRunPredictiveAnimations) {
......
} else {
clearOldPositions();
}
mState.mLayoutStep = State.STEP_LAYOUT;
}
有两个比较重要的if语句,条件为mState.mRunSimpleAnimations和mState.mRunPredictiveAnimations,点击进去我们可以发现
boolean mRunSimpleAnimations = false;
boolean mRunPredictiveAnimations = false;
这两个变量默认都为false,第二个判断走了一个clearOldPositions()方法,从字面意思上来看就是清除之前的position操作,由于我们是第一次加载,这个方法暂且与我们无关,接下来就是mState.mLayoutStep = State.STEP_LAYOUT,我们可以看到这里的值改变了,方法结束,返回到onMeasure我们来往下看dispatchLayoutStep2()方法:
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
在这个方法会调用到mLayout.onLayoutChildren(mRecycler, mState)方法,mLayout也就是LayoutManager,我们去LinearLayoutManager里看看onLayoutChildren到底都做了什么
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
detachAndScrapAttachedViews(recycler);
...
if (mAnchorInfo.mLayoutFromEnd) {
...
// fill towards start
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
...
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
...
} else {
...
// fill towards end
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
...
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
...
}
...
}
这个方法代码比较多,我只拿出了对我们比较重要的代码,首先来看下detachAndScrapAttachedViews(recycler)方法
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);
}
}
detachAndScrapAttachedViews方法里调用了scrapOrRecycleView(recycler, i, v)方法,在这个方法里正常第一次布局的时候是会走else分支,在else分支里我们看到了recycler这个成员变量,也就是Recycler ,熟悉的东西来了,我们走进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);
}
}
进到这个方法里,我们看到了熟悉的mAttachedScrap和mChangedScrap,到这里我们基本可以知道,在首次布局onLayoutChildren()调用了detachAndScrapAttachedViews(recycler)方法中把我们的view添加到了mAttachedScrap缓存中。接下来我们返回到onLayoutChildren方法,下面有个if语句,不过最终还是会走fill方法,fill方法里又调用了layoutChunk()方法,我们来看一看这个layoutChunk()里做了什么
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);
}
}
...
}
首先通过next方法把view拿到,然后经过一系列的判断将view添加到布局里去,我们着重看下next方法,看一看是怎么获取到view的
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
如果mScrapList 不为空就走nextViewFromScrapList()方法,我们暂时忽略这个方法,然后走进recycler.getViewForPosition(mCurrentPosition)这个方法,
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
经过层层调用,最终走到了tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS)方法
@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;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
// 2) Find from scrap/cache via stable ids, if exists
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 (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
return holder;
}
这里面做了很多的事情,view 的获取和绑定都是在这里实现的,我们从顺序往下看
- 首先经过第一个if语句,当mState.isPreLayout()为true时就会调用getChangedScrapViewForPosition(position)来获取view;我们在调用adapter的notifyItemChanged等方法时isPreLayout()就为true,意思就是列表的item数据改变了,就会把这个viewHoler放入到mChangedScrap里
- 接下来如果没有找到holder 就会走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 (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
}
// 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);
}
...
}
这里面有三个步骤,首先从mAttachedScrap里找,然后再从mChildHelper里的mHiddenViews里找,最后再从mCachedViews里找
- 假如上面的方法依然没有找到holder,就从mViewCacheExtension里找,我们之前说过这个类是给开发者自定义的一个类,如果我们自己没有重写,那么这里是找不到的
- 接下来就是在pool中找了,这里调用了getRecycledViewPool().getRecycledView(type),这个参数type就是我们之前说的viewHoler的类型了
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
- 最后如果上面的方法中都没有找到holder,那么就会调用mAdapter.createViewHolder(RecyclerView.this, type)了,是不是很熟悉?没错,就是我们在Adapter里重写的createViewHolder()方法了。
到此为止一个RecyclerView从无到有的加载过程我们就分析完了。
RecyclerView 的滑动缓存过程
我们知道RecyclerView 具有强大的缓存机制,内部也实现了可滑动的机制,那么当滑动时被滑出屏幕外的View和滑动进屏幕内的View他们都是从哪里来的呢,我们一起来看源码
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
我们知道RecyclerView 是可以横向滑动的,那么横向和纵向滑动分别调用了上面的两个方法,他们也最终调用了同一个方法scrollBy(dx, recycler, state);而这个方法里也调用了fill()方法
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
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);
}
...
}
进入fill方法,这次我们不看layoutChunk()方法,我们来看一下recycleByLayoutState(recycler, layoutState)方法,方法中判断条件分别会走recycleViewsFromEnd()和recycleViewsFromStart()方法,但是这两个方法最终都会调用recycleChildren(recycler, childCount - 1, i)方法,而该方法最后调用了removeAndRecycleViewAt(i, recycler)方法
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
这里把index位的view给remove掉了,然后调用了recycler.recycleView(view),紧接着这个方法又调用了recycleViewHolderInternal(holder)方法。
void recycleViewHolderInternal(ViewHolder holder) {
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--;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
这里面的逻辑就是先判断mCachedViews的容量是否超过默认值,如果超过了就要走recycleCachedViewAt(0)方法,这个方法里的逻辑就是把最老的缓存放到RecyclePool里,然后把mCachedViews里的remove掉,执行了该操作之后再把新的缓存放到mCachedViews里,如果这个视图不符合条件会直接被放进RecyclerPool中,注意这里有个cached的boolean类型的变量,就是来判断是否符合条件的,RecyclePool里的操作addViewHolderToRecycledViewPool(holder, true)方法最终会调用getRecycledViewPool().putRecycledView(holder);
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
这个方法里的逻辑为:首先判断池子里的容量是否满了,如果满了的话就直接返回,不再缓存了,否则的话会调用resetInternal()方法,从reset我们也可以看出是一个重置方法,缓存之前全部清空,这里也就可以说明为什么满了之后就不再继续缓存了,到此为止,滑出屏幕外的缓存过程也就讲完了,那么滑入屏幕内的过程呢,其实是和RecyclerView加载过程是一样的。
总结
我们从RecyclerView的几个缓存类整体入手,分析成员变量,到RecyclerView的加载过程,再到他的缓存过程,这几个方面了解了RecyclerView缓存层级与缓存机制。
参考:
https://blog.csdn.net/singwhatiwanna/article/details/100166495