RecyclerView回收机制分析

2018-05-11  本文已影响95人  gczxbb

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

任重而道远

上一篇下一篇

猜你喜欢

热点阅读