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阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容