RecyclerView回收机制分析


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

任重而道远

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,367评论 0 27
  • 简介: 提供一个让有限的窗口变成一个大数据集的灵活视图。 术语表: Adapter:RecyclerView的子类...
    酷泡泡阅读 5,128评论 0 16
  • 英文注释 大意如下: 一个Recycler 负责管理 废弃的和独立的 itemview 用于重用。 一个"scra...
    秋兰兮青青阅读 6,785评论 5 17
  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 从Android 5.0...
    Rtia阅读 307,254评论 27 439
  • 文/小叮当 房间狭小 竟容不下三两个转身 走进院子 任尔奔腾跳跃 天际的浮云总是那一块 事实是逃不过狭隘的景色 走...
    罗布泊的小哆啦阅读 221评论 0 0