RecyclerView的缓存分析

作者: CP9 | 来源:发表于2017-12-14 16:24 被阅读601次

    RecyclerView的缓存主要体现在RecyclerView的内部类Recycler

    重要的成员变量

    四级缓存 —— Scrap、Cache、ViewCacheExtension 、RecycledViewPool

    1. mAttachedScrap
      final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    2. mChangedScrap
      ArrayList<ViewHolder> mChangedScrap = null;
    3. mCachedViews
      final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    4. ViewCacheExtension
      private ViewCacheExtension mViewCacheExtension;
    5. RecycledViewPool
      RecycledViewPool mRecyclerPool;

    Cache缓存默认大小

    static final int DEFAULT_CACHE_SIZE = 2;
    int mViewCacheMax = DEFAULT_CACHE_SIZE;
    

    重要的方法

    设置Cache缓存大小 —— setViewCacheSize

    这个方法是公有的,所以可以在自己的RecyclerView中定义Cache缓存大小,例如:

    mRecyclerView.setItemViewCacheSize(5);
    

    获得指定位置的子View —— getViewForPosition,可能来自于缓存,也可能重新创建

    搜索mChangedScrap列表,从对应postion中找,找不到再从对应id找

    只有满足mState.isPreLayout()这个条件才会搜索mChangedScrap列表,这个条件在dispatchLayoutStep1中赋值为mState.mInPreLayout = mState.mRunPredictiveAnimations;,即发生添加、删除、修改要执行动画效果时,mState.mInPreLayout为true;在dispatchLayoutStep2中会赋值为false。显然只有在dispatchLayoutStep1中要执行动画的时候会调用mLayout.onLayoutChildren(mRecycler, mState);方法,预布局时,getViewForPosition才会走这第一步。

    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    
    1. 找是否有和postion相同的holder
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        return holder;
    }
    
    1. 找是否有和id相同的holder
    final ViewHolder holder = mChangedScrap.get(i);
    if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        return holder;
    }
    

    通过postion按顺序搜索mAttachedScrap、ChildHelper中存的mHiddenViews、mCachedViews列表

    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    
    1. 搜索mAttachedScrap
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
            && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        return holder;
    }
    
    1. 搜索mHiddenViews,从mHiddenViews找到相应的holder后,立即将其从mHiddenViews中移除,然后添加到Scrap缓存中
    View view = mChildHelper.findHiddenNonRemovedView(position);
    if (view != null) {
        // This View is good to be used. We just need to unhide, detach and
        // scrap list.
        final ViewHolder vh = getChildViewHolderInt(view);
        mChildHelper.unhide(view);
        int layoutIndex = mChildHelper.indexOfChild(view);
        mChildHelper.detachViewFromParent(layoutIndex);
        scrapView(view);
        vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        return vh;
    }
    
    1. 搜索mCachedViews,从Cache缓存中找到相应的holder后,立即将其从Cache缓存中移除
    if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        return holder;
    }
    

    通过id按顺序搜索mAttachedScrap、mCachedViews列表

    if (mAdapter.hasStableIds()) {
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                type, dryRun);
    }
    
    1. 搜索mAttachedScrap
    if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
        if (type == holder.getItemViewType()) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            if (holder.isRemoved()) {
                // this might be valid in two cases:
                // > item is removed but we are in pre-layout pass
                // >> do nothing. return as is. make sure we don't rebind
                // > item is removed then added to another position and we are in
                // post layout.
                // >> remove removed and invalid flags, add update flag to rebind
                // because item was invisible to us and we don't know what happened in
                // between.
                if (!mState.isPreLayout()) {
                    holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
                            ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                }
            }
            return holder;
        } else if (!dryRun) {
            // if we are running animations, it is actually better to keep it in scrap
            // but this would force layout manager to lay it out which would be bad.
            // Recycle this scrap. Type mismatch.
            mAttachedScrap.remove(i);
            removeDetachedView(holder.itemView, false);
            quickRecycleScrapView(holder.itemView);
        }
    }
    
    1. 搜索mCachedViews
    if (holder.getItemId() == id) {
        if (type == holder.getItemViewType()) {
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            return holder;
        } else if (!dryRun) {
            recycleCachedViewAt(i);
            return null;
        }
    }
    

    用户通过ViewCacheExtension可以自定义缓存策略

    final View view = mViewCacheExtension
            .getViewForPositionAndType(this, position, type);
    

    通过RecycledViewPool获取缓存

    需要注意的是,如果从RecycledViewPool中获取到了相应的holder,要将holder的一些状态重置,因为从这取的holder只是根据type匹配的,不是position对应的holder,所以需要重置holder的状态

    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal();
        if (FORCE_INVALIDATE_DISPLAY_LIST) {
            invalidateDisplayListInt(holder);
        }
    }
    

    如果上述步骤都没获取到值,则通过Adapter的createViewHolder方法创建一个holder

    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    

    调用Adapter的bindViewHolder方法,会调用Adapter的onBindViewHolder空方法

    只有满足这三个条件之一,才会调用Adapter的bindViewHolder方法

    1. holder还没绑定,即还没调用bindViewHolder方法,这个是唯一能将holder的标记设为绑定的方法
    2. holder需要更新
    3. holder已经无效
    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);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    

    总的来说分为这四类缓存:


    Scrap缓存

    scrap缓存主要用在布局前后,主要包括mAttachedScrap和mChangedScrap这两个缓存列表

    添加缓存 —— scrapView

    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);
    }
    

    添加Scrap缓存的时机

    每当RecyclerView调用dispatchLayoutStep2方法,内部都会调用onLayoutChildren方法,虽然不同的LayoutManager的实现不同,但是其中都会调用detachAndScrapAttachedViews方法,在这个方法中会对RecyclerView中已经添加的子View遍历调用scrapOrRecycleView方法,scrapOrRecycleView方法会根据holder的状态来判断是要添加到cache缓存中还是scrap缓存中,如果添加到Scrap缓存,最终会调用scrapView方法

    大多数情况会添加到mAttachedScrap这个Scrap缓存中,什么时候会添加到mChangedScrap缓存中呢?举个例子:

    update_C.png
    如上图的列表,我现在要将字母C修改为Z,当调用getAdapter().notifyItemChanged(2);方法,流程如下 Recycler_Update_Flow.png

    也就是说要修改的item会添加到mChangedScrap缓存中去,其余的会添加到mAttachedScrap缓存中

    移除缓存 —— unscrapView

    根据添加缓存方法中holder.setScrapContainer(this, boolean);这行代码设置的boolean值来判断,true则移除mChangedScrap中的holder,false则移除mAttachedScrap中的holder

    移除Scrap缓存的时机:

    1. RecyclerView的addView方法,内部会根据holder.wasReturnedFromScrap() || holder.isScrap()此条件判断是否需要移除scrap缓存,相应的会attach之前添加scrap缓存时detach的viewmChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
    2. onLayout中最终会调用dispatchLayoutStep3方法,内部调用了removeAndRecycleScrapInt方法回收所有的scrap缓存

    显然,Scrap缓存只是用在布局期间,布局后就清空了Scrap缓存

    Cache缓存

    添加缓存 —— recycleViewHolderInternal

    在 RecyclerView中通过recycleViewHolderInternal方法添加缓存

    1. 满足以下两个条件,才能添加到Cache缓存中:
      a. mViewCacheMax > 0,即Cache缓存设置的大小要大于0
      b. !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN),一般指的是该item不会执行动画,例如滑动中等
    2. 如果超过Cache缓存的最大,则移除第0个缓存
    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
        recycleCachedViewAt(0);
        cachedViewSize--;
    }
    
    1. 添加到Cache缓存的最后
    int targetCacheIndex = cachedViewSize;
    mCachedViews.add(targetCacheIndex, holder);
    cached = true;
    

    显然,Cache缓存的数据结构是后入先出的队列结构

    添加Cache缓存时机

    dispatchLayoutStep2

    1. 在通过fill方法填充布局时,会遍历每一个ChildHelper中的子类,如果满足viewHolder.isInvalid() && !viewHolder.isRemoved() &&!mRecyclerView.mAdapter.hasStableIds()这个条件,则会添加到cache缓存中去
    2. 在通过getViewForPosition方法获得给定位置的itemview时,假如通过第二步getScrapOrHiddenOrCachedHolderForPosition方法获得了一个holder,满足!validateViewHolderForOffsetPosition(holder)条件,则会添加到cache缓存中去
    3. 调用recycleView方法,将holder回收到cache缓存中

    dispatchLayoutStep3

    1. removeAnimatingView中,如果是从ChildHelper的mHiddenViews中找到并移除了这个View,则将这个View添加到cache缓存中去,举个例子,当调用notifyItemRangeRemoved方法删除item,则被删除的item在执行完删除动画,会将这个item的holder添加到cache缓存中
    2. removeAndRecycleScrapInt中,清空Scrap缓存,并将其添加到Cache缓存中

    除了上述几种情况,对于不同的LayoutManager还有不同的区别,例如LinearLayoutManager调用fill方法时,在方法开头会调用recycleByLayoutState方法,该方法会回收看不到的item

    注意:子View回收之前必须已经从父布局中detached或removed

    移除缓存 —— mCachedViews.remove

    1. 在getViewForPosition步骤2时,通过getScrapOrHiddenOrCachedHolderForPosition方法,从cache缓存中获取到了holder,则移除
    2. 在getViewForPosition步骤3时,通过getScrapOrCachedViewForId方法,从cache缓存中获取到了holder,则移除
    3. 需要通过recycleCachedViewAt方法移除cache缓存时

    ViewCacheExtension

    用户可以自定义的缓存

    RecyclerViewPool

    添加到RecyclerViewPool中 —— putRecycledView

    scrapHeap.add(scrap);
    

    从RecyclerViewPool中获取 —— getRecycledView

    return scrapHeap.remove(scrapHeap.size() - 1);
    

    从添加和获取缓存来看,RecyclerViewPool的数据结构是后进先出的栈结构,这能保证每次获取到的holder都是池中最新的

    添加RecyclerViewPool缓存 —— recycleViewHolderInternal

    recycleViewHolderInternal内部,如果没有将item添加到Cache缓存中,则会添加到RecyclerViewPool缓存中

    // cache的值在添加Cache缓存的步骤中赋值
    if (!cached) {
        addViewHolderToRecycledViewPool(holder, true);
        recycled = true;
    }
    

    添加RecyclerViewPool缓存时机

    由于在RecyclerView中添加Cache缓存和RecyclerViewPool缓存用的是同一个方法recycleViewHolderInternal,所以两个缓存的添加时机是一样的

    移除缓存 —— getViewForPosition

    return scrapHeap.remove(scrapHeap.size() - 1);
    

    只在getViewForPosition时,从RecyclerViewPool缓存中获取到holder,同时从RecyclerViewPool中移除

    RecyclerView各种状态下的缓存分析

    加载RecyclerView显示到屏幕上

    在dispatchLayoutStep2中的缓存变动

    所在方法 缓存类型 列表中的数据
    ChildHelper,即ReclyerView中的子View ChildHelper_List.png
    detachAndScrapAttachedViews Scrap缓存的mAttachedScrap列表 mAttachedScrap.png
    getViewForPosition 从Scrap缓存的mAttachedScrap列表中取
    addView 从Scrap缓存的mAttachedScrap列表中移除缓存

    滑动RecyclerView

    可以打印Cache缓存列表和RecyclerViewPool缓存列表来看滑动RecyclerView时的缓存变化,如下:

    mRvTest.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
        }
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            mRvTest.printAllValue();
        }
    });
    
    public void printAllValue() {
        try {
            Field field = rvClz.getDeclaredField("mRecycler");
            field.setAccessible(true);
            recycler = (RecyclerView.Recycler) field.get(this);
            recycler.getScrapList();
            getCacheField("mCachedViews");
            Field poolField = recyclerPoolClz.getDeclaredField("mScrap");
            poolField.setAccessible(true);
            SparseArray<Object> sa = (SparseArray<Object>) poolField.get(this.getRecycledViewPool());
            for (int i = 0; i < sa.size(); i++) {
                Field fd = scrapDataClz.getDeclaredField("mScrapHeap");
                fd.setAccessible(true);
                ArrayList<ViewHolder> mScrapHeap = (ArrayList<RecyclerView.ViewHolder>) fd.get(sa.get(sa.keyAt(i)));
                for (int j = 0; j < mScrapHeap.size(); j++) {
                    Log.d(TAG, "RecycledViewPool position: " + ((TextView) mScrapHeap.get(i).itemView.findViewById(R.id.tv)).getText());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public void getCacheField(String fieldName) throws Exception {
        Field field = recyclerClz.getDeclaredField(fieldName);
        field.setAccessible(true);
        List<ViewHolder> viewHolders = (List<RecyclerView.ViewHolder>) field.get(recycler);
        if (viewHolders == null)
            return;
        for (int i = 0; i < viewHolders.size(); i++) {
            Log.d(TAG, fieldName + " position : " + ((TextView) viewHolders.get(i).itemView.findViewById(R.id.tv)).getText());
        }
    }
    

    如果RecyclerView滑动到F开始有一部分显示到屏幕中,则会

    所在方法 缓存类型 列表中的数据
    GapWorker.prefetchPositionWithDeadline Cache缓存 Cache缓存列表1.png
    getViewForPosition 从Cache缓存的列表中取

    如果RecyclerView滑动到A开始消失在屏幕中,则会

    所在方法 缓存类型 列表中的数据
    GapWorker.prefetchPositionWithDeadline Cache缓存 Cache缓存列表2.png
    getViewForPosition 从Cache缓存的列表中取

    总结

    1. Scrap缓存用在RecyclerView布局时,布局完成之后就会清空

    2. 添加到Cache缓存和RecyclerViewPool缓存的item,他们的View必须已经从RecyclerView中detached或removed

    相关文章

      网友评论

        本文标题:RecyclerView的缓存分析

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