RecyclerView是Android開發中一個相當重要的UI控件,在平常項目的業務開發中無處不在,功能也極其強大。子View不一樣邏輯解耦,view回收複用高性能,易用性體如今局部刷新、item動畫,拖拽測滑等,基本能替代ListView全部功能(但也並不能徹底替代ListView,ListView並無被標記爲@Deprecated,關於替換的必要性能夠參考【騰訊Bugly乾貨分享】Android ListView與RecyclerView對比淺析--緩存機制)。RecyclerView核心優點是緩存機制的設計,本文以RecyclerView緩存原理爲主線,部分源碼進行分析,從RecyclerView的緩存結構,緩存管理以及緩存使用等方面進行展開。java
RecylerView緩存的簡單梳理,RecylerView中一共有五種緩存,分別是:android
其中前兩種mScrapView、mAttachedScrap並不對外暴露,真正開發中能控制或自定義的是後三種mCachedViews、mViewCacheExtension和mRecyclerPool,因此在學習RecyclerView緩存原理的過程當中,建議的方向是:理解前兩種的做用以及相關源碼,理解後三者的做用、源碼並掌握實際用法。在閱讀理解過程當中結合實踐對關鍵方法和變量進行跟蹤debug,會更快的掌握整個知識體系。segmentfault
注:本文引用的RecyclerView相關源碼爲最新api 29(Android Q),recyclerView-v7版本29.0.0(即最新sdk版本29.0.0)包下的源代碼,查看最新源碼須要最新測試版編譯器Android Studio 3.5 Beta 4,具體在as配置文件中引用爲:api
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. * <p> * 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}. * <p> * 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. * <p> * This method should <b>not</b> 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. * <p> * 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. * <p> * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}. * <p> * 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結構爲:app
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
複製代碼
ScrapData類須要注意兩個維護的變量:less
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
複製代碼
int mMaxScrap = DEFAULT_MAX_SCRAP;
複製代碼
看到了熟悉的ViewHolder集合列表,以及列表緩存默認最大個數DEFAULT_MAX_SCRAP = 5
,再看看ScrapData對象被初始化建立時機:ide
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. * * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but * that has been marked for removal or reuse.</p> * * <p>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.</p> */
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. * <p> * 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;
}
複製代碼
一、mChangedScrap和mAttachedScrap
mChangedScrap和mAttachedScrap集合列表在RecyclerView內部使用,不對外暴露(即便用層無可用方法控制),主要在RecyclerView內部佈局(onLayout)子視圖時會使用到,用做臨時存儲。因爲篇幅關係,mChangedScrap和mAttachedScrap在本篇中不做深刻分析。
二、mCachedViews
mCachedViews中緩存的ViewHolder在使用時無需調用onBindViewHolder方法進行視圖數據綁定,可徹底複用,但mCachedViews中獲取的ViewHolder也只能用於固定position位置的複用(mCachedViews中的ViewHolder都會有固定綁定好的position)。默認最大緩存個數mViewCacheMax = DEFAULT_CACHE_SIZE =2,但實際在RecyclerView列表數據填充以後進行上下(或左右)滑動時,mCachedViews數量會有3個,緣由是RecyclerView的prefech機制會致使在mCachedViews中會額外增長一個ViewHolder的緩存。
三、mViewCacheExtension
上文的緩存結構分析可知,ViewCacheExtension是暴露給應用層實現的自定義緩存,使用場景是某一類相同viewType不一樣位置的子View,要保證在滑動中始終存在於內存中而且不會出現從新綁定視圖數據(即重複調用onBindViewHolder)的狀況。沒法使用mCachedViews的緣由是,儘管mCachedViews也不須要從新綁定視圖數據,但mCachedViews的緩存複用和移除不固定viewType類型,而且和position強綁定,mCachedViews緩存的是最近滑出屏幕的子視圖。
四、mRecyclerPool
mRecyclerPool與mCachedViews不一樣的是內部緩存的ViewHolder在使用時須要調用onBindViewHolder方法從新進行視圖數據綁定,mRecyclerPool中緩存的全部ViewHolder都是被清除狀態無綁定postisin。由於從新調用onBindViewHolder方法進行視圖數據綁定,因此使用mRecyclerPool中的ViewHolder緩存是必然會從新綁定視圖數據,再次調用onBindViewHolder方法。mRecyclerPool緩存主要左右是減小ViewHolder建立即減小onCreateViewHolder方法的調用
一、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}. * * <p>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.</p> * * @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);
}
複製代碼
二、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);
}
}
複製代碼
三、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的整個緩存體系有更加深刻的理解。大道至簡,精益求精,共勉!