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

写在前面:

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

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

(0)

相关推荐

  • Android RecyclerView 实现快速滚动的示例代码

    简评:Android Support Library 26 中终于实现了一个等待已久的功能: RecyclerView 的快速滚动 . Android 官方早就在建议开发者使用 RecyclerView 替代 ListView,RecyclerView 也确实表现要好于 ListView,除了没有快速滚动,就像下面这样: 因此,之前要想在 RecyclerView 上实现快速滚动,往往是依赖第三方库,比如:FutureMind/recycler-fast-scroll或 timusus/Recy

  • Android中RecyclerView实现Item添加和删除的代码示例

    本文介绍了Android中RecyclerView实现Item添加和删除的代码示例,分享给大家,具体如下: 先上效果图: RecyclerView简介: RecyclerView用以下两种方式简化了数据的展示和处理: 1. 使用LayoutManager来确定每一个item的排列方式. 2. 为增加和删除项目提供默认的动画效果,也可以自定义. RecyclerView项目结构如下: Adapter:使用RecyclerView之前,你需要一个继承自RecyclerView.Adapter的适配器

  • Android利用RecyclerView编写聊天界面

    本文实例为大家分享了Android RecyclerView编写聊天界面的具体代码,供大家参考,具体内容如下 1.待会儿会用到RecyclerView,首先在app/build.gradle(注意有两个build.gradle,选择app下的那个)当中添加依赖库,如下: dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.1'

  • Android中RecyclerView的item宽高问题详解

    前言 本文主要给大家介绍了关于Android中RecyclerView的item宽高问题的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 在创建viewholder传入的View时,如果不指定其viewgroup,就会出现宽高只包裹显示内容的问题. View view = LayoutInflater.from(context).inflate(R.layout.test_test,null); 上面的做法就会出问题 改成这样就可以正常显示设置的宽高 View vie

  • Android RecyclerView设置下拉刷新的实现方法

    Android RecyclerView设置下拉刷新的实现方法 1 集成 SwipeRefreshLayout 1.1 xml布局文件中使用 <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/refresh" android:layout_width = "match_parent" android:layout_height = "match_parent" &g

  • Android单个RecyclerView实现列表嵌套的效果

    很多时候会遇到一种需求,列表里面有列表,像这种需求之前一般都是用多个列表控件互相嵌套来实现,但是这样很容易出现一些问题,例如滚动冲突.数据显示不全.多余的逻辑处理等.后来发现,一个recyclerview就可以实现列表嵌套的效果,这里需要用到recyclerview的多布局功能. 效果图: recyclerview的多布局涉及到的主要方法是getItemViewType,作用是设置每个item要显示的布局类型.之前不了解的时候,都是直接用数学逻辑直接去计算,多少个position后显示什么布局,

  • Android使用RecyclerView实现今日头条频道管理功能

    使用过今日头条的伙计们对这个效果肯定很熟悉.拖拽可排序,点击标签后可以删除.今天我们采用RecyclerView来实现. 实现思路: 通过ItemTouchHelper来绑定RecyclerView的子控件触摸事件. 当滑动拖拽的时候,通知适配器来交换两个子控件的显示位置. 更改数据源,使数据源与子空间显示内容一致. 这就是实现的基本思路,是不是很简单?当然,首先要了解一下ItemTouchHelper这哥们儿是干啥的,有什么作用. This is a utility class to add

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

    写在前面: 在上篇文章中说过对于像 RecyclerView 或者 ListView 等等此类在有限屏幕中展示大量内容的控件,复用的逻辑就是其核心的逻辑,而关于复用导致最常见的 bug 就是复用错乱.在大上周我就遇到了一个很奇怪的问题,这也是我下决心研究 RecyclerView 的原因. RecyclerView 源码分析 而这篇文章的目的首先是讨论在 RecyclerView 复用错乱时,一些通用的解决思路,其次就是探究我遇到的那个奇怪的问题,帮助未来同样遇到的朋友们. 复用错乱的解决办法

  • Android性能优化大图治理示例详解

    目录 引言 1 自定义大图View 1.1 准备工作 1.2 图片宽高适配 1.3 BitmapRegionDecoder 2 大图View的手势事件处理 2.1 GestureDetector 2.2 双击放大效果处理 2.3 手指放大效果处理 引言 在实际的Android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的:像列表页.查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上

  • Android RecyclerChart其它图表绘制示例详解

    目录 正文 1. 心电图 2. 睡眠图 正文 之前章节介绍了RecyclerChart 中一些通用的图表的相关绘制逻辑,本章节介绍两种Special的Chart的绘制,一种是心电图,一种是睡眠图.首先我们来看下心电图EcgChart的绘制. 1. 心电图 EcgChart 跟LineChart形态上是相似的,但是EcgChart的点相对于LineChart密集的多,之前的LineChart相当于每个RecyclerView的Itemview 中的Model对应的value值,而心电图的ItemD

  • Android 实现锚点定位思路详解

    相信做前端的都做过页面锚点定位的功能,通过<a href="#head" rel="external nofollow" > 去设置页面内锚点定位跳转. 本篇文章就使用tablayout.scrollview来实现android锚点定位的功能. 效果图: 实现思路 1.监听scrollview滑动到的位置,tablayout切换到对应标签 2.tablayout各标签点击,scrollview可滑动到对应区域 自定义scrollview 因为我们需要监听

  • Android开发环境搭建过程图文详解

    一.工具 IDE:Android Studio4.1+genymotion (Android studio 自带AVD着实有些不好用,这里选择使用genymotion模拟器) JDK:1.8 SDK:7.1 版本管理:Git 二.环境搭建 1.安装jdk 这里使用的是jdk1.8 ,安装并配置环境变量,通用步骤,不一 一介绍了 2.安装Android Studio 安装:android-studio-ide-201.6858069-windows.exe ,默认安装即可配置sdk (可以选择设置

  • Android mvvm之LiveData原理案例详解

    1. 生命周期感知 1.1 生命周期感知组件 我们知道,Controller(Activity or Fragment) 都是有生命周期的,但是传统的 Controller 实现方式只负责 Controller 本身的生命周期管理,而与业务层的数据之间并没有实现良好解耦的生命周期事件交换.所以业务层都需要自己主动去感知 Controller 生命周期的变化,并在 Controller 的生存期处理数据的保活,而在消亡时刻解除与 Controller 之间的关系,这种处理方式随着业务规模的扩大往往

  • Android View的事件体系教程详解

    目录 一.什么是View?什么是ViewGroup? 二.View的位置 三.View的触摸事件 1.MotionEvent 2.TouchSlop 3.VelocityTracker 5.Scroller 四.View的滑动 1)使用Scroll 2)通过动画 3)使用延时策略 五.View的事件分发机制 六.View的滑动冲突问题 View的滑动冲突常见可以简单分为三种: 滑动冲突的处理规则 滑动冲突的解决方法 一.什么是View?什么是ViewGroup? View是Android中所有控

  • Android TabLayout 自定义样式及使用详解

    目录 基本使用 XML静态设置TabItem 联动ViewPager2动态设置TabItem 1. Activity布局代码 2. 创建三个Fragment给ViewPager2设置 3. Fragment对应XML布局 4. 绑定起来 最终效果 根据数据源动态生成TabItem 1.Activity布局代码 2. Activity代码 最终效果 修改TabLayout背景颜色 修改indicator layer-list 制作圆形的indicator 制作圆角矩形indicator 修改边距

  • Android ChipGroup收起折叠效果实现详解

    目录 一.先上效果图 二.ChipGroup和Chip 三.在ChipGroup中动态添加Chip 四.找到每个Chip位于的行数 五.实现思路 六.需要注意的问题 1.Chip的复用问题 2.Chip的ID设置 3.点击同一个Chip返回的ID为-1的问题 一.先上效果图 借用某东App的效果,如下. 折叠时的效果: 展开时的效果: 二.ChipGroup和Chip chipGroup和chip之前写过博客,可移步Android Material 常用组件,看关于chip和chipGroup的

  • Android Compose衰减动画Animatable使用详解

    目录 前言 animateDecay splineBasedDecay rememberSplineBasedDecay exponentialDecay 实战 最后 前言 之前介绍了 Animatable 动画以及其 animateTo和 snapTo两个开启动画 api 的使用,实际上 Animatable 除了这两个 api 以外还有一个 animateDecay即本篇要介绍的衰减动画. 什么是衰减动画呢?就是动画速度由快到慢最后停止,最常见的应用场景就是惯性动画,比如滑动列表时手指松开后

随机推荐