RecyclerView源码解析

复用和回收

复用的好处:
避免为表项视图绑定数据,创建表项视图。

子item的绘制交给LayoutManager去处理。

fill

LinearLayoutManager#fill
作用:回收和复用。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ...
    // 当前的方向上是否还有多余的空间填充item
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // 当剩余空间> 0时,继续填充更多表项
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // 通过View循环,来对条目进行一条条复用,填充剩余空间
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
    
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // 从剩余空间中扣除新表项占用像素值
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                // 在limit上追加新表项所占像素值
                // 回收哪些项目是根据limit线走的,手指向上滑,底部填充元素,limit线会下移,在这根线上面的条目会被回收。
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 回收
            recycleByLayoutState(recycler, layoutState);
        }
    }
    return start - layoutState.mAvailable;
}

回收

LinearLayoutManager#recycleByLayoutState

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
         // 从列表头回收
        recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
    } else {
        // 从列表尾回收
        recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
    }
}

LinearLayoutManager#recycleViewsFromStart

private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
    //从头开始遍历 LinearLayoutManager,以找出应该会回收的表项
    final int childCount = getChildCount();
    // 是否反转布局,就是布局上从上往下填充还是从下往上填充
    if (mShouldReverseLayout) {
        for (int i = childCount - 1; i >= 0; i--) {
            View child = getChildAt(i);
            // 当某表项底部位于limit隐形线之后时,回收它以上的所有表项
            // limit是列表中隐形的线
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                //回收索引为末尾到i-1的表项
                recycleChildren(recycler, childCount - 1, i);
                return;
            }
        }
    } else {
         //回收索引为0到i-1的表项
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}

“从列表头回收表项”所对应的场景是:手指上滑,列表向上滚动,新的表项逐个插入到列表尾部,列表头部的表项逐个被回收。

复用

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 1. 通过缓存池中获取下个条目
    View view = layoutState.next(recycler);
   
    // 2. 将列表中的一项添加进RecyclerView
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    // 3. 测量该视图
    measureChildWithMargins(view, 0, 0);
    // 4. 获取填充视图需要消耗的像素值
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    // 5. 布局表项
    // 确定表项上下左右四个点相对于RecyclerView的位置
    layoutDecoratedWithMargins(view, left, top, right, bottom);
}

LinearLayoutManager#next

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

RecyclerView#tryGetViewHolderForPositionByDeadline

复用机制代码

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 复用的对象是ViewHolder
    // 在布局之前
    if (mState.isPreLayout()) {
        // 1. 通过id或者position从mChangedScrap缓存找到对应的缓存
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        // 2. 通过position从mAttachedScrap或二级回收缓存中获取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                if (!dryRun) {
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);

        final int type = mAdapter.getItemViewType(offsetPosition);
        // 3. 通过id从mAttachedScrap或二级回收缓存中获取ViewHolder
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        if (holder == null && mViewCacheExtension != null) {
            // 4. 从自定义缓存中获取ViewHolder
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            // 5. 从缓存池中取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            long start = getNanoTime();
            // 6.所有缓存都没命中,就需要创建ViewHolder
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }

            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
            }
        }
    }

    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //获得ViewHolder后,绑定视图数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ...
    return holder;
}

总结:
RecyclerView在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。在填充表项的同时,也会回收表项,回收的依据是limit隐形线。
limit隐形线是RecyclerView在滚动发生之前根据滚动位移计算出来的一条线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠。
limit隐形线的初始值=列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到limit值之上,即limit隐形线会随着新表项的填充而不断地下移。
触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于limit隐形线下方,则该表项上方的所有表项都会被回收。

四级缓存

// detach调用
// 复用的时候不需要调用bindViewHolder重新绑定数据,状态和数据不会被重置的
// 保存原封不动的ViewHolder
// 生命周期两次布局
// 位置一致才能复用
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// 发生变化的ViewHolder
// 生命周期只有预布局
ArrayList<ViewHolder> mChangedScrap = null;

// remove调用
// 可通过setItemCacheSize调整,默认大小为2
// 上下滑动,被滑出去的ViewHolder缓存
// 如果超过限制,会把最老的item移除到RecycledViewPool中。

// mCachedViews中缓存的ViewHolder只能复用于指定位置,不需要调用bindViewHolder重新绑定数据
// 应用场景列表回滚
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

// 自定义拓展View缓存
private ViewCacheExtension mViewCacheExtension;

// RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储
// 同一类型的ViewHolder存放在ArrayList 中,且默认最多存储5个。
// mCachedViews缓存放不下的时候,才会把缓存放进mRecyclerPool,里面的缓存都是需要重新绑定数据的。
// 从mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项。
RecycledViewPool mRecyclerPool;

最差情况:重新创建ViewHolder绑定数据
次好情况:复用ViewHolder需要重新绑定数据
最好情况:复用ViewHolder不需要重新绑定数据

谈谈mChangedScrap

生命周期只有预布局的时候。
mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到mChangedScrap中。
mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。

浅谈几种更新RecyclerView的区别

notifyItemInserted


需要重新布局,A、B、C都可以从mAttachedScrap缓存拿出来直接使用,不需要绑定,a需要创建对应的ViewHolder重新绑定,添加进一级缓存。

注意如果是A,B,C,D移除B,B还是在mAttachedScrap缓存,只不过FLAG是REMOVE。

notifyDataSetChanged


代表数据全面发生变化,屏幕上的内容标为无效,屏幕上的元素全部缓存到四级缓存RecycledViewPool,屏幕上的元素都需要重新绑定。

notifyItemChanged


A被添加了FLAG_UPDATE,在scrapView(View view)中不满足!holder.isUpdated()所以会被放入到mChangedScrap,然后在缓存复用时B、C、D都可以直接使用,A因为被修改了所以需要重新绑定一下。
也就是说:notifyItemChanged将屏幕上的元素保存到一级缓存中,有更改的保存到mChangedScrap中并且需要重新绑定,没有变化的保存到mAttachedScrap中。

重点类

Recycler:管理复用
LayoutManager:管理布局

detach和remove

一个View只是暂时被清除掉,稍后立刻就要用到,使用detach。它会被缓存进scrapCache的区域。
一个View不再显示在屏幕上,需要被清除掉,并且下次再显示它的时机目前未知,使用remove。它会被以viewType分组,缓存进RecyclerViewPool里。

scrap view的生命周期

在将表项一个个填充到列表之前会先将其先回收到mAttachedScrap中,回收数据的来源是LayoutManager的孩子,而LayoutManager的孩子都是屏幕上可见的或即将可见的表项。
RecyclerView布局的最后一步,清除scrap view。
mAttachedScrap生命周期起始于RecyclerView布局开始,终止于RecyclerView布局结束。

RecyclerView的动画

列表中有两个表项(1、2),删除2,此时3会从屏幕底部平滑地移入并占据原来2的位置。
为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项3也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项3的位置,就知道表项3该如何做动画了,表项2做消失动画,当动画结束后,item2的ViewHolder会被回收。

RecyclerView为了实现表项动画,进行了2次布局(预布局+后布局),在源码上表现为LayoutManager.onLayoutChildren()被调用2次。
预布局的过程始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()。

在每次向RecyclerView填充表项之前都会先清空LayoutManager中现存表项,将它们detach并同时缓存入mAttachedScrap列表中。在紧接着的填充表项阶段,就立马从mAttachedScrap中取出刚被 detach的表项并重新attach它们。

pre-layout
LinearLayoutManager#onLayoutChildren

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    // 在填充表项之前会遍历所有子表项,并逐个回收
    detachAndScrapAttachedViews(recycler);
    ...
    // 填充表项
    fill()
}

post-layout
因为LayoutManager中现有表项1、2、3,所以scrap完成后,mAttachedScrap中存有表项1、2、3的ViewHolder实例(position依次为0、0、1,被移除表项的position会被置0)。分别填充position位置为0和1的表项。为2的位置缓存就不会命中。
缓存命中规则:position相同,并且表项没被移除。

为什么这么设计:
为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”,不然只知道最终位置不知道起始位置。
为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),
为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),
但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),
RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在scrap结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。

整体总结

  • Recycler有4个层次用于缓存ViewHolder对象,优先级从高到底依次为ArrayList<ViewHolder> mAttachedScrapArrayList<ViewHolder> mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPool。如果四层缓存都未命中,则重新创建并绑定ViewHolder对象。
  • 缓存性能:
    都不需要重新创建ViewHolder,只有RecycledViewPool,mChangedScrap需要重新绑定数据。
  • 缓存容量:
    mAttachedScrap:没有大小限制,但最多包含屏幕可见表项。
    mCachedViews:默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
    mRecyclerPool:对ViewHolder按viewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。
  • 缓存用途:
    mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用。
    mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。
    mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项
  • 缓存结构:
    mAttachedScrap:ArrayList<ViewHolder>
    mCachedViews:ArrayList<ViewHolder>
    mRecyclerPool:对ViewHolder按viewType分类存储在SparseArray<ScrapData>中,同类ViewHolder存储在ScrapData中的ArrayList中

参考
RecyclerView 源码分析2-缓存机制图解
RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?

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

推荐阅读更多精彩内容