Android RecyclerView 复用错乱通用解法详解
写在前面:
在上篇文章中说过对于像 recyclerview 或者 listview 等等此类在有限屏幕中展示大量内容的控件,复用的逻辑就是其核心的逻辑,而关于复用导致最常见的 bug 就是复用错乱。在大上周我就遇到了一个很奇怪的问题,这也是我下决心研究 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
方法不调用导致的,我在 * 上搜索了很多答案,终于找到了一个可以解决我的问题的:
(中文资料压根就没有,所以掌握英文搜索是多么的重要)
你可以调用
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 刷新下列表也是可以的。
好了本文到这里就结束了~希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Angular5.0.0新特性
下一篇: 广告渠道有哪些,线下广告投放平台一览