ListView的复用大家都应该相当熟悉了,先来看一张图片。
对于一个ListView来说,如果屏幕可以显示的Item是n个,那么使用convertView复用后,整个过程中最多也就创建n+1个Item,其它就都是复用的,当然了,如果ViewType是多种,那么数量肯定会多一些。但是原理都是一样的。
RecylerView 是Google推出的一个更加侧重复用的列表组件,很多人的第一感觉就是它跟ListView的复用应该是一样的,只不过更加方便了。建议大家自己写一个Demo项目来运行一下,会发现 RecylerView 调用 onCreateViewHolder 的次数完全不是我们预想的那样,是n+1。也就是说它创建的Item个数变多了,一般是n+4,这是什么原因呢。又能如何设置修改这个效果呢。
问题的关键就是:
/**
* Set the number of offscreen views to retain before adding them to the potentially shared
* {@link #getRecycledViewPool() recycled view pool}.
*
* The offscreen view cache stays aware of changes in the attached adapter, allowing
* a LayoutManager to reuse those views unmodified without needing to return to the adapter
* to rebind them.
*
* @param size Number of views to cache offscreen before returning them to the general
* recycled view pool
*/
public void setItemViewCacheSize(int size) {
mRecycler.setViewCacheSize(size);
}
但是如果只设置了这个为0,会发现数理只是减少了两个还是不对。我们继续来查看源码。mRecycler 是 RecylerView 的内部类 Recycler对象。
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;
/**
* Set the maximum number of detached, valid views we should retain for later use.
*
* @param viewCount Number of views to keep before sending views to the shared pool
*/
public void setViewCacheSize(int viewCount) {
mRequestedCacheMax = viewCount;
updateViewCacheSize();
}
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);
}
}
mLayout就是我们的LayoutManager。
/**
* Written by {@link GapWorker} when prefetches occur to track largest number of view ever
* requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or
* {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call.
*
* If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)},
* will be reset upon layout to prevent initial prefetches (often large, since they're
* proportional to expected child count) from expanding cache permanently.
*/
int mPrefetchMaxCountObserved;
首先我们确定缓存的数目也受这个变量的影响,但是它的初始值是0,为什么实际效果中多了一个呢。通过注释我们可以看到是 GapWorker 这个类中有方法会修改它。
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
mCount = 0;
if (mPrefetchArray != null) {
Arrays.fill(mPrefetchArray, -1);
}
final RecyclerView.LayoutManager layout = view.mLayout;
if (view.mAdapter != null
&& layout != null
&& layout.isItemPrefetchEnabled()) {
if (nested) {
// nested prefetch, only if no adapter updates pending. Note: we don't query
// view.hasPendingAdapterUpdates(), as first layout may not have occurred
if (!view.mAdapterHelper.hasPendingUpdates()) {
layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
}
} else {
// momentum based prefetch, only if we trust current child/adapter state
if (!view.hasPendingAdapterUpdates()) {
layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
view.mState, this);
}
}
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
view.mRecycler.updateViewCacheSize();
}
}
}
@Override
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
if (getChildCount() == 0 || delta == 0) {
// can't support this scroll, so don't bother prefetching
return;
}
ensureLayoutState();
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(delta);
updateLayoutState(layoutDirection, absDy, true, state);
collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}
void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
final int pos = layoutState.mCurrentPosition;
if (pos >= 0 && pos < state.getItemCount()) {
layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
}
}
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
if (layoutPosition < 0) {
throw new IllegalArgumentException("Layout positions must be non-negative");
}
if (pixelDistance < 0) {
throw new IllegalArgumentException("Pixel distance must be non-negative");
}
// allocate or expand array as needed, doubling when needed
final int storagePosition = mCount * 2;
if (mPrefetchArray == null) {
mPrefetchArray = new int[4];
Arrays.fill(mPrefetchArray, -1);
} else if (storagePosition >= mPrefetchArray.length) {
final int[] oldArray = mPrefetchArray;
mPrefetchArray = new int[storagePosition * 2];
System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
}
// add position
mPrefetchArray[storagePosition] = layoutPosition;
mPrefetchArray[storagePosition + 1] = pixelDistance;
mCount++;
}
最后mCount变成1,也就是说把mPrefetchMaxCountObserved变成1,ViewCacheSize变成3了。
那怎么把 RecylerView 变得和 ListView一样也就很简单了,只需要两行代码。
recylerView.setItemViewCacheSize(0);
layout.setItemPrefetchEnabled(false);
最后谈下我的理解。可以这样来理解 RecylerView 的缓存机制。 首先它支持预加载,像ViewPager一样(ViewPager的那些事——你真了解预加载机制吗?),最少需要一个提前量。然后它的后边不是马上回收复用,而是维护一个缓存池,这个池子的大小可以设置,只有当池子满了之后再加入新的才会开始回收重用。