RecyclerView是Android开发中一个至关重要的UI控件,在日常项目的业务开发中无处不在,功能也极其强大。子View不同逻辑解耦,view回收复用高性能,易用性体现在局部刷新、item动画,拖拽测滑等,基本能替代ListView所有功能(但也并不能完全替代ListView,ListView并没有被标记为@Deprecated,关于替换的必要性可以参考【腾讯Bugly干货分享】Android ListView与RecyclerView对比浅析–缓存机制)。RecyclerView核心优势是缓存机制的设计,本文以RecyclerView缓存原理为主线,部分源码进行分析,从RecyclerView的缓存结构,缓存管理以及缓存使用等方面进行展开。
RecylerView缓存的简单梳理,RecylerView中一共有五种缓存,分别是:
其中前两种mScrapView、mAttachedScrap并不对外暴露,真正开发中能控制或自定义的是后三种mCachedViews、mViewCacheExtension和mRecyclerPool,所以在学习RecyclerView缓存原理的过程中,建议的方向是:理解前两种的作用以及相关源码,理解后三者的作用、源码并掌握实际用法。在阅读理解过程中结合实践对关键方法和变量进行跟踪debug,会更快的掌握整个知识体系。
注:本文引用的RecyclerView相关源码为最新api 29(Android Q),recyclerView-v7版本29.0.0(即最新sdk版本29.0.0)包下的源代码,查看最新源码需要最新测试版编译器Android Studio 3.5 Beta 4,具体在as配置文件中引用为:
dependencies {
...
implementation 'com.android.support:recyclerview-v7:29.0.0'
...
}
RecyclerView缓存本质上指的ViewHolder缓存,下面是源码中五种缓存变量的数据结构:
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
ViewCacheExtension是一个abstranct类,暴露给应用层实现,只有一个abstract的getViewForPositionAndType方法需要覆写。
private ViewCacheExtension mViewCacheExtension;
/**
* ViewCacheExtension is a helper class to provide an additional layer of view caching that can
* be controlled by the developer.
*
* When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
* first level cache to find a matching View. If it cannot find a suitable View, Recycler will
* call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
* {@link RecycledViewPool}.
*
* Note that, Recycler never sends Views to this method to be cached. It is developers
* responsibility to decide whether they want to keep their Views in this custom cache or let
* the default recycling policy handle it.
*/
public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
*
* This method should not create a new View. Instead, it is expected to return
* an already created View that can be re-used for the given type and position.
* If the View is marked as ignored, it should first call
* {@link LayoutManager#stopIgnoringView(View)} before returning the View.
*
* RecyclerView will re-bind the returned View to the position if necessary.
*
* @param recycler The Recycler that can be used to bind the View
* @param position The adapter position
* @param type The type of the View, defined by adapter
* @return A View that is bound to the given position or NULL if there is no View to re-use
* @see LayoutManager#ignoreView(View)
*/
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
int type);
}
/**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
*
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
* and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
*
* RecyclerView automatically creates a pool for itself if you don't provide one.
*/
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
/**
* Tracks both pooled holders, as well as create/bind timing metadata for the given type.
*
* Note that this tracks running averages of create/bind time across all RecyclerViews
* (and, indirectly, Adapters) that use this pool.
*
* 1) This enables us to track average create and bind times across multiple adapters. Even
* though create (and especially bind) may behave differently for different Adapter
* subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
*
* 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
* false for all other views of its type for the same deadline. This prevents items
* constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
*/
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
...
/**
* Sets the maximum number of ViewHolders to hold in the pool before discarding.
*
* @param viewType ViewHolder Type
* @param max Maximum number
*/
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
/**
* Returns the current number of Views held by the RecycledViewPool of the given view type.
*/
public int getRecycledViewCount(int viewType) {
return getScrapDataForType(viewType).mScrapHeap.size();
}
/**
* Acquire a ViewHolder of the specified type from the pool, or {@code null} if none are
* present.
*
* @param viewType ViewHolder type.
* @return ViewHolder of the specified type acquired from the pool, or {@code null} if none
* are present.
*/
@Nullable
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;
}
...
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
}
简单分析RecyclerPool类的结构,里面核心数据结构:
SparseArray<ScrapData> mScrap = new SparseArray<>();
SparseArray仅用于存储key(int),value(object)的结构,比HashMap更省内存,在某些条件下性能更好(具体本文不展开)。再看ScrapData结构为:
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
ScrapData类需要注意两个维护的变量:
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
看到了熟悉的ViewHolder集合列表,以及列表缓存默认最大个数DEFAULT_MAX_SCRAP = 5
,再看看ScrapData对象被初始化创建时机:
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
显然是在getScrapDataForType方法传入viewType时创建,再找找getScrapDataForType被调用的地方会发现有一个对外暴露的方法setMaxRecycledViews里面调用,从而说明可以根据viewType设置不同类别ViewHolder的最大缓存个数:
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
RecycledViewPool里还有一个需要注意的方法getRecycledView:
@Nullable
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;
}
这个方法是RecyclerView使用时比较熟悉的方法,根据viewType获取ViewHolder,可以看出本质是从RecycledViewPool的mScrap缓存结构中获取ViewHolder缓存。至此,整个RecyclerView的缓存结构大致梳理清楚。
从上面描述的缓存结构源码中不难发现,5种缓存结构变量均存在Recycler类中,所有ViewHolder缓存的增删改查方法也都在Recycler类中实现。
A Recycler is responsible for managing scrapped or detached item views for reuse
正如Recycler类注释描述,Recycler是RecyclerView缓存核心工具类。典型的应用场景就是给LayoutManger提供可复用的视图:
Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for an adapter's data set representing the data at a given position or item ID
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*
* A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.
*
* Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
* an adapter's data set representing the data at a given position or item ID.
* If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
* If not, the view can be quickly reused by the LayoutManager with no further work.
* Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
* may be repositioned by a LayoutManager without remeasurement.
*/
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
...
LayoutManager作用是测量和定位RecyclerView中的子视图,也提供策略用于回收不再可见的子视图
关于五种缓存的使用,在tryGetViewHolderForPositionByDeadline方法中,依次从五种缓存数据结构中获取可用缓存:
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
*
* If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return
* rather than constructing or binding a ViewHolder if it doesn't think it has time.
* If a ViewHolder must be constructed and not enough time remains, null is returned. If a
* ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is
* returned. Use {@link ViewHolder#isBound()} on the returned object to check for this.
*
* @param position Position of ViewHolder to be returned.
* @param dryRun True if the ViewHolder should not be removed from scrap/cache/
* @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
* complete. If FOREVER_NS is passed, this method will not fail to
* create/bind the holder if needed.
*
* @return ViewHolder for requested position
*/
@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;
}
1、mChangedScrap和mAttachedScrap
mChangedScrap和mAttachedScrap集合列表在RecyclerView内部使用,不对外暴露(即使用层无可用方法控制),主要在RecyclerView内部布局(onLayout)子视图时会使用到,用作临时存储。由于篇幅关系,mChangedScrap和mAttachedScrap在本篇中不作深入分析。
2、mCachedViews
mCachedViews中缓存的ViewHolder在使用时无需调用onBindViewHolder方法进行视图数据绑定,可完全复用,但mCachedViews中获取的ViewHolder也只能用于固定position位置的复用(mCachedViews中的ViewHolder都会有固定绑定好的position)。默认最大缓存个数mViewCacheMax = DEFAULT_CACHE_SIZE =2,但实际在RecyclerView列表数据填充之后进行上下(或左右)滑动时,mCachedViews数量会有3个,原因是RecyclerView的prefech机制会导致在mCachedViews中会额外增加一个ViewHolder的缓存。
3、mViewCacheExtension
上文的缓存结构分析可知,ViewCacheExtension是暴露给应用层实现的自定义缓存,使用场景是某一类相同viewType不同位置的子View,要保证在滑动中始终存在于内存中并且不会出现重新绑定视图数据(即重复调用onBindViewHolder)的情况。无法使用mCachedViews的原因是,尽管mCachedViews也不需要重新绑定视图数据,但mCachedViews的缓存复用和移除不固定viewType类型,并且和position强绑定,mCachedViews缓存的是最近滑出屏幕的子视图。
4、mRecyclerPool
mRecyclerPool与mCachedViews不同的是内部缓存的ViewHolder在使用时需要调用onBindViewHolder方法重新进行视图数据绑定,mRecyclerPool中缓存的所有ViewHolder都是被清除状态无绑定postisin。因为重新调用onBindViewHolder方法进行视图数据绑定,所以使用mRecyclerPool中的ViewHolder缓存是必然会重新绑定视图数据,再次调用onBindViewHolder方法。mRecyclerPool缓存主要左右是减少ViewHolder创建即减少onCreateViewHolder方法的调用
1、mCachedViews
mCachedViews的使用相对简单,使用层直接控制的方法只有setItemViewCacheSize即设置mCachedViews的最大缓存个数,
mRecyclerView. setItemViewCacheSize(maxCacheSize);
/**
* 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);
}
2、ViewCacheExtension
ViewCacheExtension自定义缓存使用核心是理解作用和使用时机,使用demo:
SparseArray<View> specials = new SparseArray<>();
...
recyclerView.getRecycledViewPool().setMaxRecycledViews(SPECIAL, 0);
recyclerView.setViewCacheExtension(new RecyclerView.ViewCacheExtension() {
@Override
public View getViewForPositionAndType(RecyclerView.Recycler recycler,
int position, int type) {
return type == SPECIAL ? specials.get(position) : null;
}
});
...
class SpecialViewHolder extends RecyclerView.ViewHolder {
...
public void bindTo(int position) {
...
specials.put(position, itemView);
}
}
3、RecycledViewPool
RecyclerdViewPool具体使用方式:
RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
//或 RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool();
recycledViewPool.setMaxRecycledViews(type1, cacheSize1);
recycledViewPool.setMaxRecycledViews(type2, cacheSize2);
recycledViewPool.setMaxRecycledViews(type3, cacheSize3);
...
mRecyclerView.setRecycledViewPool(recycledViewPool);
对RecyclerView缓存体系的梳理,会对日常项目开发列表相关业务有更深入的理解。本文由于篇幅关系仅做了相对简单的说明,要系统深入理解RecyclerView缓存,建议用最简单的RecyclerView列表展示demo,在RecyclerView适配器的核心方法如onCreateViewHolder、onBindViewHolder等加log,观察创建、滑动中的具体日志输出规律。并结合RecyclerView缓存的5大变量进行debug才会对RecyclerView的整个缓存体系有更加深入的理解。大道至简,精益求精,共勉!