Android RecyclerView 复用错乱通用解法详解

Android RecyclerView 复用错乱通用解法详解,第1张

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

写在前面:

在上篇文章中说过对于像 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 刷新下列表也是可以的。

好了本文到这里就结束了~希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

总结

以上是内存溢出为你收集整理的Android RecyclerView 复用错乱通用解法详解全部内容,希望文章能够帮你解决Android RecyclerView 复用错乱通用解法详解所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1144878.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存