美文网首页
RecyclerView回收机制分析

RecyclerView回收机制分析

作者: gczxbb | 来源:发表于2018-05-11 17:45 被阅读95次

    Recycler类是RecyclerView内部final类,它管理scrapped(废弃)或detached(独立)的Item视图,使它们可以重用。我们都知道,在ListView中,也有一个类似的RecycleBin类,管理Item的重用。本文的重点是Recycler类,分析一下视图在消失与出现时,如何利用Recycler实现重用。
    ViewHolder类RecyclerView的内部抽象类,我们自己定义的Adapter中实现,封装子视图的一些视图。


    Scrapped视图

    先看一下Recycler内部的几个引用。

    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    private RecycledViewPool mRecyclerPool;
    private ViewCacheExtension mViewCacheExtension;
    

    mAttachedScrap列表:用来存储Scrapped(废弃)的ViewHolder,它对应的视图是detached的,即ItemView调用了ViewGroup的detachViewFromParent方法,从容器的子视图数组中移除,它其实并没有被废弃。它正是存放从RecyclerView中detached的ItemView的ViewHolder列表。

    当RecyclerView初始加载Item,第一次触发onLayoutChildren时,fill创建满足RecyclerView高度的子ItemView,ViewHolder绑定ItemView,并ViewGroup#addView加入RecyclerView视图。第二次onLayoutChildren时,通过detachAndScrapAttachedViews方法将全部ItemView从mChildren数组删除,触发的是ViewGroup#detachViewFromParent方法,ItemView变为detached,ViewHolder放入mAttachedScrap,fill继续触发从mAttachedScrap中获取ViewHolder,将ViewHolder加入子View数组,触发的是ViewGroup#attachViewToParent方法。

    综上所述:mAttachedScrap列表只是暂存从RecyclerView容器中detached下来的ItemView,也可以说从mChildren数组移除的ItemView,这些ItemView属于Scrapped,但是立马又会被attach到RecyclerView。

    mCachedViews列表:从RecyclerView区域移除,从ViewGroup中删除的ItemView,存储在列表中,最大值max,大于max时,删除最早进入的第0个元素,该元素放入RecycledViewPool中,如果还是放不下,直接放入RecycledViewPool。永远存储最新从RecyclerView删除的视图ViewHolder。
    ViewGroup已经执行过removeViewAt删除了View
    RecycledViewPool:视图缓存池,当mCachedViews存储不下时,将ViewHolder放入,根据类型存储。ViewGroup已经执行过removeViewAt删除了View。
    ViewCacheExtension:扩展使用,开发者自己控制缓存。

    RecycView.png

    图中的数据源一共有17项,显示区域中,可容纳的子视图大约在12个左右。


    ItemView视图消失逻辑

    RecyclerView视图显示出来以后,手指触屏,向上滑动。此时,position是0,1,2,3...的ItemView依次滚动出视图可见范围。
    通过源码调试,发现在LinearLayoutManager的recycleChildren方法处,触发了下面的方法,定义在LayoutManager类。

    public void removeAndRecycleViewAt(int index, Recycler recycler) {
        final View view = getChildAt(index);
        removeViewAt(index);//从父容器中删除。
        recycler.recycleView(view);//存入Recycler
    }
    

    首先,LayoutManager的removeViewAt方法,从RecyclerView中删除索引index的子视图,它与position无关。调用辅助类ChildHelper的removeViewAt方法。

    public void removeViewAt(int index) {
        final View child = getChildAt(index);
        if (child != null) {
            mChildHelper.removeViewAt(index);
        }
    }
    

    RecyclerView类的初始化initChildrenHelper方法,定义Callback对象,在辅助类的方法中,调用内部Callback的对应方法。

    private void initChildrenHelper() {
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            ...
            @Override
            public void removeViewAt(int index) {
                final View child = RecyclerView.this.getChildAt(index);
                if (child != null) {
                    dispatchChildDetached(child);
                }
                RecyclerView.this.removeViewAt(index);
            }
            ...
              
        });
    }
    

    dispatchChildDetached方法,通知子视图detached,将调用Adapter的onViewDetachedFromWindow方法,可以在自己的Adapter中重写。注意,这里并没有触发ViewGroup的detachViewFromParent方法。
    RecyclerView的removeViewAt方法,调用父类ViewGroup的removeViewAt方法,删除该ItemView子视图。
    手指上滑,每次最顶部Item视图滑出屏幕时,删除的都是index是0的子视图,手指下移,每次底部Item视图滑出可视范围,删除的都是index是12左右的子视图,与position无关。
    其次,调用Recycler的recycleView方法,将ViewHolder加入缓存mCachedViews或RecycledViewPool池。

     public void recycleView(View view) {
        ViewHolder holder = getChildViewHolderInt(view);
        if (holder.isTmpDetached()) {
            removeDetachedView(view, false);
        }
        if (holder.isScrap()) {
            holder.unScrap();
        } else if (holder.wasReturnedFromScrap()){
            holder.clearReturnedFromScrapFlag();
        }
        recycleViewHolderInternal(holder);
    }
    

    根据View获取它绑定的ViewHolder对象,从View的LayoutParams中获取。ViewHolder的内部mScrapContainer(即Recycler)是空,isScrap方法返回false。只有执行过Recycler的scrapView(View)方法,将ViewHolder加入到mAttachedScrap列表时,才会设置内部mScrapContainer值,当isScrap返回true时,调用unScrap方法,调用内部Recycler的unscrapView方法。

    void unscrapView(ViewHolder holder) {
        if (holder.mInChangeScrap) {
            mChangedScrap.remove(holder);
        } else {
            mAttachedScrap.remove(holder);
        }
        holder.mScrapContainer = null;
        holder.mInChangeScrap = false;
        holder.clearReturnedFromScrapFlag();
    }
    

    从mAttachedScrap列表中删除,置空ViewHolder内部Recycler。
    Recycler的recycleViewHolderInternal方法,将ViewHolder加入缓存mCachedViews或RecycledViewPool池。

    void recycleViewHolderInternal(ViewHolder holder) {
        ...
        final boolean transientStatePreventsRecycling = holder
                        .doesTransientStatePreventRecycling();
        final boolean forceRecycle = mAdapter != null
                        && transientStatePreventsRecycling
                        && mAdapter.onFailedToRecycleView(holder);
        boolean cached = false;
        boolean recycled = false;
        if (forceRecycle || holder.isRecyclable()) {
            if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_REMOVED
                            | ViewHolder.FLAG_UPDATE)) {
                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    //删除mCachedViews第0个元素,并触发
                    //addViewHolderToRecycledViewPool方法加入RecycledViewPool
                    recycleCachedViewAt(0);
                    cachedViewSize --;
                }
                if (cachedViewSize < mViewCacheMax) {
                    mCachedViews.add(holder);
                    cached = true;
                }
            }
            if (!cached) {//未加入mCachedViews时
                addViewHolderToRecycledViewPool(holder);
                recycled = true;
            }
        } else if (DEBUG) {
        }
        mViewInfoStore.removeViewHolder(holder);
        if (!cached && !recycled && transientStatePreventsRecycling) {
            holder.mOwnerRecyclerView = null;
        }
    }
    

    待加入的ViewHolder不能是Scrap,前面经过unScrap方法处理过。缓存mCachedViews最大值是mViewCacheMax,当达到最大时,删除第一个,被删除元素加入RecycledViewPool。如果数量已经小于最大值,将新ViewHolder放入mCachedViews缓存,如果仍然大于,将其放入RecycledViewPool。

    void addViewHolderToRecycledViewPool(ViewHolder holder) {
        ViewCompat.setAccessibilityDelegate(holder.itemView, null);
        dispatchViewRecycled(holder);//派发回调
        holder.mOwnerRecyclerView = null;
        getRecycledViewPool().putRecycledView(holder);//入池
    }
    

    将ViewHolder所属的RecyclerView置空,执行dispatchViewRecycled回调,该方法将调用Adapter的onViewRecycled方法,可重写。ViewHolder放置到RecycledViewPool缓存池。

    综上所述

    当position是0的视图移除屏幕,将ViewHolder存入mCachedViews缓存,最大缓存默认是2,当position是1的视图移除屏幕,也会存入mCachedViews缓存。当position是2的视图移除屏幕,将缓存中的第一个ViewHolder元素删除,加入RecycledViewPool池。position是2的视图ViewHolder存入缓存。这是视图消失的基本逻辑。


    ItemView视图出现的逻辑

    手指触屏,向上滑动,position是12,13,14,15...的ItemView依次从底部冒出,通过调试源码,调用Recycler的getViewForPosition方法。该方法根据position获取ItemView视图,position是RecyclerView的数据源索引,当视图完全展示后,子视图有12个,那么,最后一个的索引是11,position是12索引对应视图不可见,上滑时,12索引首先出现。

    View getViewForPosition(int position, boolean dryRun) {
        /**position边界判断**/
        boolean fromScrap = false;
        ViewHolder holder = null;
        if (holder == null) {
            //根据position从ScrapView中获取holder
            holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
            if (holder != null) {
                //验证holder是否可用于position位置
                if (!validateViewHolderForOffsetPosition(holder)) {
                    ...
                    holder = null;
                } else {
                    fromScrap = true;
                }
            }
        }
        if (holder == null) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                //抛出边界溢出异常IndexOutOfBoundsException
            }
            final int type = mAdapter.getItemViewType(offsetPosition);
            //通过stable ids查找Scrap
            ...
            if (holder == null) { 
                //从RecycledViewPool获取
                holder = getRecycledViewPool().getRecycledView(type);
                if (holder != null) {
                    holder.resetInternal();//这里会设置mPosition=-1
                }
            }
            if (holder == null) {
                //Adapter创建holder 
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
            }
        }
        ...
        boolean bound = false;
        if (mState.isPreLayout() && holder.isBound()) {
        } else if (!holder.isBound() || holder.needsUpdate() || 
                            holder.isInvalid()) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            holder.mOwnerRecyclerView = RecyclerView.this;
            mAdapter.bindViewHolder(holder, offsetPosition);
            bound = true;
            ...
        }
        ...
        return holder.itemView;
    }
    

    首先,从mAttachedScrap与mCachedViews中查找ViewHolder,在视图滚动时,mAttachedScrap是空的,因此,一般情况从mCachedViews缓存查找。
    validateViewHolderForOffsetPosition方法,验证holder是否可用于对应position索引。如果验证通过,设置fromScrap标志,返回holder的itemView视图。如果验证失败,将增加无效标志,holder内部mScrapContainer(即Recycler)存在,说明holder是isScrap的 ,Scrap的holder无法被回收,unScrap方法提前去除其标志,最后会加入缓存,recycleViewHolderInternal方法。
    其次,从RecycledViewPool缓存池中查找。从这里获取的ViewHolder,设置mPosition是NO_POSITION(-1)。如果都未找到,通过Adapter的createViewHolder方法创建,调用Adapter的onCreateViewHolder抽象方法,开发者重写此方法,初始化ItemView,创建ViewHolder对象。最后,通过Adapter的bindViewHolder方法,调用Adapter的onBindViewHolder抽象方法,开发者重写此方法。初始化ViewHolder的View中数据。
    Recycler的getScrapViewForPosition方法。

    ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
        final int scrapCount = mAttachedScrap.size();
        //从mAttachedScrap查找,视图初始显示时走这一步
        //滚动时不会走这里。
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                    && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                if (type != INVALID_TYPE && holder.getItemViewType() != type) {
                    //ViewType不同
                    break;
                }
                holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                return holder;
            }
        }
        ...
        //从mCachedViews列表查找
        final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {
            final ViewHolder holder = mCachedViews.get(i);
            //无效标志FLAG_INVALID的holder可能存在与cache中。
            if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                if (!dryRun) {
                    mCachedViews.remove(i);
                }
                return holder;
            }
        }
        return null;
    }
    

    当视图滚动时,该方法从缓存mCachedViews查找ViewHolder,并且它的mPosition要和position一致。
    举个例子说明一下。假如position是12完全不可见,当向上滑动时,position是12的视图出现,此时,ViewHolder不是getScrapViewForPosition获取。因为mCachedViews还是空,或者position是0的视图已在mCachedViews缓存,但它的mPosition是0,与12不相等,也不会使用它。因此,position是12的要新建ViewHolder。当position是13和14...视图出现,对应position是1,2..的视图要进入mCachedViews缓存,如果mCachedViews缓存未达到最大值,将会一直新建ViewHolder,原因也一样,mPosition不符合。如果到达最大值,缓存的最大值默认是2,此时,已经存储position是0和1的值,继续上滑,position是2的视图要进缓存,删掉最早position是0的值,将它放入RecycledViewPool池。继续,position是14的出现,从缓存未找到符合的position,因为此刻缓存里还都是头部position较小的值,RecycledViewPool已经有值,就从RecycledViewPool获取。这里获取的与positon无关,ViewHoder的mPosition都是-1,只要type类型一样,在Adapter的bindViewHolder方法,会为mPosition赋值,这个ViewHolder内部mPosition就属于14啦。
    改变方向手指下滑,position是2的视图出屏幕,对应的ViewHolder在缓存,直接使用。position是14的消失了,将position是14的ViewHolder加入缓存。

    综上所述

    缓存mCachedViews,存储的总是最新消失Item视图对应的ViewHolder,ype != INVALID_TYPE && holder.getItemVie不管它是在顶部消失,还是在底部消失。它的最大值也不宜过大,设计过大的话会就可以一直装入,未出现过的position都要新建ViewHolder。比如,缓存无限大,一屏显示11个,上滑,这11个都可以进入缓存,那么后面出来11个左右都因position不符而新建。再下滑,后面出来的这些也可以进入缓存,从缓存取出上面的一批显示,这就用不到RecycledViewPool了,失去了它原有的功能。
    那么,为什么会有mCachedViews呢?
    如果直接在RecycledViewPool池存储,当底部视图出来就可以重用第一个消失的视图。对于在一个位置不停上下滑动时,个人感觉,从mCachedViews查找更快一些。

    到这里,我们已经获取了屏幕下一个将要显示的ItemView,接下来就要将它加入到RecyclerView视图中,调用LayoutManager#addViewInt方法。

    private void addViewInt(View child, int index, boolean disappearing) {
        final ViewHolder holder = getChildViewHolderInt(child);
        ...
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (holder.wasReturnedFromScrap() || holder.isScrap()) {
            //视图刚展现时,从mAttachedScrap获取数据时触发这里。
            if (holder.isScrap()) {
                holder.unScrap();
            } else {
                holder.clearReturnedFromScrapFlag();
            }
            mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
            if (DISPATCH_TEMP_DETACH) {
                ViewCompat.dispatchFinishTemporaryDetach(child);
            }
        } else if (child.getParent() == mRecyclerView) { 
            int currentIndex = mChildHelper.indexOfChild(child);
            if (index == -1) {
                index = mChildHelper.getChildCount();
            }
            if (currentIndex == -1) {
                //抛出异常
            }
            if (currentIndex != index) {
                mRecyclerView.mLayout.moveView(currentIndex, index);
            }
        } else {
            mChildHelper.addView(child, index, false);
            lp.mInsetsDirty = true;
            if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
                mSmoothScroller.onChildAttachedToWindow(child);
            }
        }
        ....
    }
    

    如果发现ViewHolder的FLAG_RETURNED_FROM_SCRAP标志或isScrap,先unScrap处理,再调用ViewGroup的attachViewToParent方法。在滚动时,获取的isScrap是false。
    借助ChildHelper的addView方法,调用CallBack的addView方法,最终,调用的是ViewGroup的addView,ItemView加入父容器,dispatchChildAttached方法,会触发Adapter的onViewAttachedToWindow方法。

    @Override
    public void addView(View child, int index) {
        RecyclerView.this.addView(child, index);
        dispatchChildAttached(child);
    }
    
    综上所述

    当视图进入可视范围,从缓存mCachedViews或RecycledViewPool获取ViewHolder,获取内部ItemView,ViewGroup的addView方法将视图加入父视图。
    这是视图可视加/取的逻辑。


    ChildHelper辅助类

    ItemView帮助类,它通过内部Callback接口暴露出来,在RecyclerView类初始化ChildHelper时实现接口方法,调用RecyclerView的对应方法。处理子视图会借助父类ViewGroup。

    RecyclerView的ChildHelper辅助类.jpg

    任重而道远

    相关文章

      网友评论

          本文标题:RecyclerView回收机制分析

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