RecyclerView 复用错乱通用解法

RecyclerView 复用错乱通用解法

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

写在前面:
在上篇文章中说过对于像 RecyclerView 或者 ListView 等等此类在有限屏幕中展示大量内容的控件,复用的逻辑就是其核心的逻辑,而关于复用导致最常见的 bug 就是复用错乱。在大上周我就遇到了一个很奇怪的问题,这也是我下决心研究 RecyclerView 的原因。

RecyclerView 源码分析

而这篇文章的目的首先是讨论在 RecyclerView 复用错乱时,一些通用的解决思路,其次就是探究我遇到的那个奇怪的问题,帮助未来同样遇到的朋友们。

复用错乱的解决办法

本文的前半部分很简单的,以为关于复用错乱,RecyclerView 已经有他的前辈 ListView 替它踩了很多坑了。虽然他们的复用逻辑是有差异的,例如 ListView 只有两层缓存,但是 RecyclerView 可以理解为有四层;ListView 缓存的单位是 view,而 RecyclerView 缓存的单位是 ViewHolder。但是不管他们复用逻辑的差异如何,终归都是把那个缓存起来的 view 拿过来接着用,所以解决复用错乱的方法是一样的。

RecyclerView 复用导致错乱的原因其实就是拿出来之前的 View 来添加到新 item 上,之前 View 的状态一直保留着,所以也就错乱了。不过解决起来很简单:

首先我们以 adapter 数据的来源分为两大类:

1.当数据来源是同步的

这种情况是最简单的,你就保证当 onBindViewHolder 方法调用的时候,你的 itemview 中每个 view 的状态都有一个默认值。这是什么意思呢?

        if ("<unknown>".equals(artists)) {
            holder.cbMusicState.setChecked(true);
        } else {
            holder.cbMusicState.setChecked(false);
        }

假设我们的 holder 里面有个 Checkbox 控件,当歌手名为 unknown 时,Checkbox 勾选。注意个时候你一定要加上这个 else 条件,才能保证复用这个 ViewHolder 的时候,Checkbox 的状态不出错。任何控件都一样,总结起来就是你要给每个控件的状态赋一个新的值,替换掉之前的,这样自然不会出现什么复用错乱的问题。

2.当数据的来源是异步的

这种情况也很常见,我们举个栗子,比如你的 ItemView 里面有个 ImageView,每次 onBindViewHolder 的时候,你传入一个 url,等待服务器返回的结果,然后展示在 ImageView 上。这种情况会怎样导致错乱呢?

是这样的,假设我进入了页面,开始为第一个 ImageView 请求图片,但是此刻我下划屏幕,划到了第四个 item,此时第一个 item 已经不可见了,第四个 item 复用了第一个 item 的 imageview,恰好此刻第一个 imageview 的图片结果返回了,就正好展示在了第四个 itemview 上。 这样就发生了图片的错乱。

出现这个问题的原因就是这个 ImageView 和请求的 url 没一一绑定,所以按照这个思路来解决吧:

    holder.ivCameraImages.setBackground(R.drawable.place_holder);
    
    holder.ivCameraImages.setTag(imageURL);

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == MSG_IMAGE) {
                Bitmap bm = (Bitmap) msg.obj;
                if (bm != null) {
                    if (TextUtils.equals((String) imageView.getTag(), imageURL)) {
                        imageView.setBackground(new BitmapDrawable(bm));
                    }
                }
            }
        }

首先在没加载图片之前,给 ImageView 设置一个默认图片,然后通过 setTag 方法,将 ImageView 和 图片的 url 一一对应起来,设置的时候再判断一下,这个 imageview 的 tag 和当时请求的 url,是不是一致的,如果是一致的,再保存。

以上就是复用错乱时两种比较通用的解法,基本上可以覆盖大部分情况。

一个奇怪的问题

这个问题的现象是这样子的:

当 RecyclerView 的条目很少的时候,比如只有六个,将 RecyclerView 从上滑动到下,这个时候是正常的,onBindViewHolder 会调用,不过此时从底部上划的时候,上方的 item 从不可见到可见的这个过程中,onBindViewHolder 并没有调用,这个时候我也就没办法进行一些刷新 item 的操作了。

这个问题的原因是 onBindViewHolder 方法不调用导致的,我在 StackOverflow 上搜索了很多答案,终于找到了一个可以解决我的问题的:

recyclerview-not-recycling-views-if-the-view-count-is-small

(中文资料压根就没有,所以掌握英文搜索是多么的重要)

你可以调用

recyclerView.setItemViewCacheSize(int);

这个 api,去调整 RecyclerView 的复用逻辑和方式来解决 onBindViewHolder 没有调用的这个问题。

但是原理是怎样的呢?作为一名好奇心颇重的程序员,一步步 debug RecyclerView 的源代码,发现了导致这个问题的原因,一起来看看吧。

在上一篇文章中,我们分析了 RecyclerView 的源码,其中复用逻辑的模块,有一个非常重要的核心方法 tryBindViewHolderByDeadline,这个方法目的就是在 RecyclerView 的层层缓存结构中,取出 ViewHolder。

这里就不再次研究它了,想了解的去看之前的文章,我来描述一下对于这个场景,简化之后的逻辑:

当 RecyclerView 从底部向上滑动的时候,会先后从 mCachedViews 和 mRecyclerPool 中寻找缓存的 ViewHolder。

mCachedViews 和 mRecyclerPool 之间又有什么关系呢?

        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

        void updateViewCacheSize() {
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                recycleCachedViewAt(i);
            }
        }

当调用 setViewCacheSize 这个方法时,相当于是给 mViewCacheMax 这个变量赋值了, for 循环调用 recycleCachedViewAt 的作用是将 mCachedViews 中缓存的 ViewHolder 放进 RecyclerPool 中。可以看到 for 循环的周期是从 mCachedViews 的最后一个对象直到 mCachedViews.size == mViewCacheMax 这个值时。

也就是可以这么理解, setViewCacheSize 这个方法其实就是为 mCachedViews 集合设置所能持有 ViewHolder 的最大数量。

setViewCacheSize(0)时,RecyclerView 想去复用 ViewHolder 时,只能去 RecyclerPool 中去取了,这里就有问题来了,从 RecyclerPool 中取和从 mCachedViews 中取 ViewHolder 中又有什么区别呢?

                if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

当从 RecyclerPool 取出 ViewHolder 时,调用了 resetInternal 这个函数的作用是清空一些记录的参数,包括之前记录 ViewHolder 状态的 mFlags。

 else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder);
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

代码再往下走的时候,刚刚清空的 flag 参数这个时候就用到了,holder.isBound() 返回 flase,进入 if 判断,调用 tryBindViewHolderByDeadline 进而调用了 onBindViewHolder

到这里这个逻辑就描述清楚了,所以设置 setViewCacheSize 来调整 mCachedViews 保存 ViewHolder 的大小,就能解决问题。

当然有些特殊的情况,某些位置就不能调用 onBindViewHolder,没关系,可以监听 RecyclerView 的滑动,当滑动停止的时候,再调用 notify 刷新下列表也是可以的。

好了本文到这里就结束了~

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

推荐阅读更多精彩内容