lv 和 rv 的緩存比較(初稿)

dim.red

lv的緩存

存儲 View 結構


public void setViewTypeCount(int viewTypeCount) {
    if (viewTypeCount < 1) {
        throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
    }
    //noinspection unchecked
    ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
    for (int i = 0; i < viewTypeCount; i++) {
        scrapViews[i] = new ArrayList<View>();
    }
    mViewTypeCount = viewTypeCount;
    mCurrentScrap = scrapViews[0];
    mScrapViews = scrapViews;
}

存儲View 的是ArrayList<View>[],并且數組大小為viewTypeCount , 這也是為什么我們在多 type 的時候需要指定type的個數了.

屏幕外的緩存



/**
 * Put a view into the ScrapViews list. These views are unordered.
 *
 * @param scrap The view to add
 */
void addScrapView(View scrap, int position) {
    AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    if (lp == null) {
        return;
    }

    lp.scrappedFromPosition = position;

    ...
    if (mViewTypeCount == 1) {
        mCurrentScrap.add(scrap);
    } else {
        mScrapViews[viewType].add(scrap);
    }

    ...
}

/**
 * @return A view from the ScrapViews collection. These are unordered.
 */
View getScrapView(int position) {
    if (mViewTypeCount == 1) {
        return retrieveFromScrap(mCurrentScrap, position);
    } else {
        int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
    }
    return null;
}

static View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    int size = scrapViews.size();
    if (size > 0) {
        // See if we still have a view for this position.
        for (int i=0; i<size; i++) {
            View view = scrapViews.get(i);
            if (((AbsListView.LayoutParams)view.getLayoutParams())
                    .scrappedFromPosition == position) {
                scrapViews.remove(i);
                return view;
            }
        }
        return scrapViews.remove(size - 1);
    } else {
        return null;
    }~~~

####注意:
代碼為api 21的,各個系統版本不同可能代碼有所不同,但是核心的思想是一樣的.

##rv的緩存

###根據 Position 獲取 View 視圖

View getViewForPosition(int position, boolean dryRun) {
...
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
...
}
if (holder == null) {
...
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrap = 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 (holder == null) { // fallback to recycler
// try recycler.
// Head to the shared pool.
if (DEBUG) {
Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "
+ "pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (DEBUG) {
Log.d(TAG, "getViewForPosition created new ViewHolder");
}
}
}

...
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);
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    holder.mOwnerRecyclerView = RecyclerView.this;
    mAdapter.bindViewHolder(holder, offsetPosition);
    attachAccessibilityDelegate(holder.itemView);
    bound = true;
    if (mState.isPreLayout()) {
        holder.mPreLayoutPosition = position;
    }
}

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 = fromScrap && bound;
return holder.itemView;

}

這里可以看出
holder 分別從以下幾個地方被賦值: 
1. 當mState.isPreLayout() 為 true 有也就是動畫的時候.
getChangedScrapViewForPosition
從mChangedScrap 中獲取到配置position ,position 配置不到的話,當mAdapter.hasStableIds() 為 true 的話,匹配getItemId 的值.值得注意當我們的 LayoutManger 支持動畫的時候,他的onLayoutChildren 會被調用兩個,一次為Pre-Layout,一種是 Real-Layout, 而mChangedScrap中的 View 在只會在Pre-Layout.返回的目的是為了 LayoutManager 在Pre-Layout中不會空白了一塊.可以正確布局.
* getScrapViewForPosition() 從mAttachedScrap 中匹配position , 配置不到的話從mCachedViews 去匹配 position 值,
* 當mAdapter.hasStableIds() 為 true 的時候.
getScrapViewForId 從mAttachedScrap 中匹配getItemId 以及 ViewType 值,匹配不到的話,嘗試從mCachedViews 匹配getItemId 和ViewType.
* 當 mViewCacheExtension 不為空的時候 getViewForPositionAndType()從開發者設置ViewCacheExtension  中獲取到 View
* getRecycledViewPool().getRecycledView(type)
從RecycledViewPool 獲取到View
* mAdapter.createViewHolder(RecyclerView.this, type); 創建一個 View .

我們從上面的是地方可以看出我們的緩存 View 存儲在兩種類型:
Scrap 和recycle:

Scrap 
mChangedScrap,mAttachedScrap,mCachedViews.
recycle 
RecycledViewPool.
Scrap 之所以比recycle輕量. 因為recycle 一定會有bindViewHolder 的動作.而Scrap 不一定會有.
####注意:
mAdapter.hasStableIds()  表示數據集合中的每一項是否可以代表有惟一的標識符,這個都作用跟Adapter.hasStableIds一致的效果,具體作用在notifyDataSetChanged 體現. eg:你有適配器hasStableIds為 false, 你的列表中刪除了第2項,那你使用notifyDataSetChanged 那么你的第2項的展示的數據是第三項的,但是你的 View 還是之前的第2的View.而你hasStableIds 為 true, 并且為他們每個項有一個唯一的 id, 那你刪除了第2項,使用notifyDataSetChanged 那么你的第2項的展示的數據是第三項的,你的 View 就是之前第三項.因為 View 跟數據匹配上了.


###屏幕內緩存
RequestLayout 和NotifyXXX 下的回收.
```

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
       ...
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}
```
mChangedScrap: 收集的是界面上被打上UpdateOp.UPDATE的 item,rv 通過notifyItemChanged對 position 所在的ViewHolder 打上flag的.

mAttachedScrap: 界面上所有非mChangedScrap 的 View
###屏幕外的緩存.

private static final int DEFAULT_CACHE_SIZE = 2;
private int mViewCacheMax = DEFAULT_CACHE_SIZE;
/**

  • internal implementation checks if view is scrapped or attached and throws an exception

  • if so.

  • Public version un-scraps before calling recycle.
    */
    void recycleViewHolderInternal(ViewHolder holder) {

    ...
    if (forceRecycle || holder.isRecyclable()) {
    if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
    | ViewHolder.FLAG_UPDATE)) {
    // Retire oldest cached view
    final int cachedViewSize = mCachedViews.size();
    if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
    recycleCachedViewAt(0);
    }
    if (cachedViewSize < mViewCacheMax) {
    mCachedViews.add(holder);
    cached = true;
    }
    }
    if (!cached) {
    addViewHolderToRecycledViewPool(holder);
    recycled = true;
    }
    }

    }

void recycleCachedViewAt(int cachedViewIndex) {

ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);

addViewHolderToRecycledViewPool(viewHolder);
mCachedViews.remove(cachedViewIndex);

}

void addViewHolderToRecycledViewPool(ViewHolder holder) {
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
dispatchViewRecycled(holder);
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}


這里我們可以看出 view 的回收是要經過mCachedViews 然后才是RecycledViewPool
并且這里的判斷條件也是挺有意思:
如果mCachedViews 到達 最大值,講 mCachedViews 第一個壓入RecycledViewPool中,然后要回收的 View也壓到RecycledViewPool中去.如果不沒有到達最大值才壓入 mCachedViews 中去.從代碼中我們可以看出最大值為2,你也可是使用setViewCacheSize方法設置最大值.

RecycledViewPool 是最后一級回收了.我們看一下這個RecycledViewPool 的實現.

public static class RecycledViewPool {
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
private SparseIntArray mMaxScrap = new SparseIntArray();
private int mAttachCount = 0;

private static final int DEFAULT_MAX_SCRAP = 5;

...

public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}

private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
ArrayList<ViewHolder> scrap = mScrap.get(viewType);
if (scrap == null) {
scrap = new ArrayList<>();
mScrap.put(viewType, scrap);
if (mMaxScrap.indexOfKey(viewType) < 0) {
mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
}
}
return scrap;
}
}

我們可以看出這里使用了SparseArray<ArrayList<ViewHolder>>來存儲 View,由于SparseArray 可以動態增加,所以我們并不需要手動寫明 viewTypeCount. 同時我們也可以看到每種類型緩存最大值為5 ,大于5以后 的 view 會被丟棄.


##對比:
1. lv為一 View 為單位. rv 以 ViewHolder 為單位. 設計上 rv 更先進.
* lv對多 type 的緩存機制不太好, 只要被生成 View 都會被緩存起來.
eg:當出現大量 type 1 出現以后,在出現大量的 type2, 此時內存中就還有存在大量的 type1和大量的 type2. 而我們現在只有 type2,多余的 type1 一直占有內存不釋放..而rv 的滑動時候的緩存是RecycledViewPool +mCachedViews , mCachedViews只有2個,而RecycledViewPool相同 type 最多存儲5個.也就像上面的場景, rv 就不會有大量的 type1和 type2 的出現.
* rv 的緩存定制能力更強.你可以自定義一個RecycledViewPool 進去,也能設置mCachedViews 的容量.

##rv 使用的坑:
1. 當你的 多type的是個,相同 type 出現在屏幕的數量差值大于5 的時候,并且經常出現的這種情況.比如說你的 type1 這時候在屏幕中是有13個,然后變成3,然后再變成13,這中情況交替出現的時候,會出現頻繁的 View 的創建.因為你在13 切換到3 的時候,剩下的10要被緩存起來,但是RecycledViewPool只能緩存5個,mCachedViews最多幫助緩存2個,剩下的 View 就被釋放了.當再次切換到13的情況下,就只能創建 View 了,我們可以通過setMaxRecycledViews對RecycledViewPool 緩存最大值的修改.
* 出于動畫的考量.當你的 數據的改變而你調用notifyItemChanged 的時候.因為此時的 View 被 mChangedScrap 儲存.而且mChangedScrap只會在 pre-Layout 中返回,導致你在 real-layout 中得到 View 是一個新的 View, 所以notifyItemChanged 往往導致了一些 View 的創建和界面的圖片的閃爍.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容