RecyclerView#Adapter#notifyDataSetChanged方法后,为何还会新建ViewHolder?

环境

android sdk版本: 30

依赖:

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.recyclerview:recyclerview:1.2.1"

案例分析:

RecyclerView宽高固定;LayoutManagerLienarLayoutManager,vertical方向;数据20条,足以铺满整个屏幕。

现象:
①先创建Adapter,设置20条数据。
②调用RecyclerView#Adapter#notifyDataSetChanged方法后,当前页面中只有5个ViewHolder复用,其余的ViewHolder会走Adapter#createViewHolder方法创建新的ViewHolder

原理:

为了搞清楚原理,我们先看一下,刚进入页面时,RecyclerView#Adapter#onCreateViewHolder方法的调用栈。

RecyclerView#Adapter#onCreateViewHolder方法的调用栈

RecyclerView#onLayout(): 4578行
RecyclerView#dispatchLayout(): 4012行
RecyclerView#dispatchLayoutStep2():4309行
LinearLayoutManager#onLayoutChildren(): 668行
LinearLayoutManager#fill(): 1591行
LinearLayoutManager#layoutChunk(): 1631行
LinearLayoutManager#LayoutState#next(): 2330行
RecyclerView#Recycler#getViewForPosition(int position): 6296行
RecyclerView#Recycler#getViewForPosition(position, boolean dryRun): 6300行
RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs): 6416行
RecyclerView#Adapter#createViewHolder(@NonNull ViewGroup parent, int viewType): 7295行
RecyclerView#Adapter#onCreateViewHolder(@NonNull ViewGroup parent, int viewType)

其核心是RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline方法,它主要有两个作用,一个是获取ViewHolder;另一个给ViewHolder绑定数据。

获取ViewHolder是有顺序的,会先尝试从各级缓存里面去获取,会依次从Recycler scrapcacheRecycledViewPool中获取,如果都获取不到,就直接创建一个ViewHolder

RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline

Attempts to get the ViewHolder for the given position, either from the Recycler scrap, cache, the RecycledViewPool, or creating it directly.
获取给定位置的ViewHolder。会依次从Recycler scrap、cache、RecycledViewPool中获取,如果都获取不到,就直接创建一个ViewHolder

核心:获取viewHolder;给viewHolder绑定数据。
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ...
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 如果需要预先布局,就尝试从mChangedScrap中去获取。
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        ...
    }
    // 1) Find by position from scrap/hidden list/cache
    // 尝试依次从mAttachedScrap、mChildHelper的mHiddenViews、mCachedViews中去获取可复用的ViewHolder
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        // 校验holder是否有效,无效就清除vh
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                ...
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ...
        // 获取这个位置对应的数据类型,通过重写的Adapter#getItemViewType方法。
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        // 如果设置了stable ids,就根据id依次从mAttachedScrap、mCachedViews中查找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            ...
        }
        // 尝试从mViewCacheExtension中获取VH
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        // 尝试从 RecycledViewPool 中获取
        if (holder == null) { // fallback to pool
            ...
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        // 调用RecyclerView#Adapter#onCreateViewHolder生成ViewHolder
        if (holder == null) {
            ...
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...           
        }
    }
    ...
    // 给viewholder绑定数据
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        ...
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {       
        ...
        // 给viewholder绑定数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    // 给holder.itemView设置RecyclerView#LayoutParams。并将其相互绑定。
    ...
    return holder;
}

实例分析。

我们这里调用RecyclerView#Adapter#notifyDataSetChanged方法后,既有复用的ViewHolder,也有新建的ViewHolder。复用的ViewHolder来自于哪里?为什么是5个?为什么还要新建ViewHolder

带着这些问题,我们debug下我们的场景,看下ViewHolder的来源。

核心在于调用RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline方法,关键在于下面这段代码:

holder = getRecycledViewPool().getRecycledView(type);

我们知道,RecycledViewPool中是以viewType来存放不同的ViewHolder的,每个type最多存放五个。

所以我们在RecyclerView#Recycler#tryGetViewHolderForPositionByDeadline中,从RecycledViewPool中最多能找到五个可复用的ViewHolder,其余的只能走新建ViewHolder流程了。

RecycledViewPool

先来看下RecycledViewPool的说明:

RecycledViewPool lets you share Views between multiple RecyclerViews.
If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool and use setRecycledViewPool(RecyclerView.RecycledViewPool).
RecyclerView automatically creates a pool for itself if you don't provide one.

大意是RecycledViewPool可以让你在多个recyclerview之间共享视图。
如果你想在RecyclerViews中回收视图,可以创建一个RecycledViewPool的实例并使用setRecycledViewPool(RecyclerView.RecycledViewPool)
如果你不提供一个RecycledViewPool实例,那么RecyclerView会自动为自己创建一个。

我们看下RecyclerView#Recycler#getRecycledViewPool方法:确实是自动创建了一个。

RecycledViewPool getRecycledViewPool() {
    if (mRecyclerPool == null) {
        mRecyclerPool = new RecycledViewPool();
    }
    return mRecyclerPool;
}

再看下RecycledViewPool的结构:

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }

    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ...
}

如上所示,里面有一个SparseArray<ScrapData>类型的变量mScrap,用来存储不同类型的ViewHolderScrapData数据中包含ArrayList<ViewHolder>类型变量mScrapHeap,用来存放具体的ViewHolder,它的最大容量是5(DEFAULT_MAX_SCRAP)。

RecycledViewPool中的数据何时添加的

本例中RecycledViewPool中的数据是从哪里添加的呢?

本例中,向ScrapData#mScrapHeap添加ViewHolder数据的调用链如下:

RecyclerView#onLayout(): 4578行
RecyclerView#dispatchLayout(): 4012行
RecyclerView#dispatchLayoutStep2():4309行
LinearLayoutManager#onLayoutChildren(): 633行
RecyclerView#LayoutManager#detachAndScrapAttachedViews(): 9493行
RecyclerView#LayoutManager#scrapOrRecycleView(): 9508行
RecyclerView#Recycler#recycleViewHolderInternal(): 6671行
RecyclerView#Recycler#addViewHolderToRecycledViewPool(): 6723行
RecyclerView#RecyclerViewPool#putRecycledView(ViewHolder scrap): 5931行
scrapHeap.add(scrap);

核心是LinearLayoutManager#onLayoutChildren()方法中,如下的这段代码:

detachAndScrapAttachedViews(recycler);

也就是说,在调用RecyclerView#Adapter#notifyDataSetChanged方法后,会触发绘制流程。在Linearlayout#layoutChildren方法中,会先对ViewHolder进行缓存,然后会对ViewHolder进行复用。

相关资料

demo-AdapterOnCreateViewHolderTestActivity

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

推荐阅读更多精彩内容