lv 和 rv 的缓存比较(初稿)

作者: 轻微 | 来源:发表于2016-03-07 01:52 被阅读1177次

    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 的创建和界面的图片的闪烁.

    相关文章

      网友评论

        本文标题:lv 和 rv 的缓存比较(初稿)

        本文链接:https://www.haomeiwen.com/subject/dxdwkttx.html