RecyclerView浅析

属于查漏补缺,因为之前一直没有对RecyclerView、ListView做技术总结,平时用就用了,但是像ViewHoder、RecyclerView的缓存机制并没有进行系统的学习,所以这次通过在RecyclerView使用上和源码上进行一个总结。

1. ListView和RecyclerView的区别

首先,ListView和RecyclerView他们的作用是一样的:在有限的手机屏幕上显示大量(无限)的View
RecyclerView之所以有个Recycler,是因为一个View如果被显示了并且通过用户滑动屏幕,这个View进入到了手机不可见的区域,它就被 Recycler(回收)了。

因为有限不仅仅是手机屏幕,手机的内存也是如此。以内存的角度来说,RecyclerView直观上性能会更优于ListView。

1.1 ListView的缺陷

  • 只可以纵向显示布局
  • 不支持动画
  • 接口设计和系统不一致
    譬如像 setOnItemClickListener()setOnItemLongClickListener()等,因为View本身就已经有了自己的触摸反馈了,而ListView这样的设计会显得重复。
  • 没有强制实现ViewHolder
  • 在列表较为复杂时,性能相较于RecyclerView低下。

1.2 RecyclerView的优势

Google为了解决ListView的局限,重写了一套用于列表展示的View,就是RecyclerView,所以它的优势就是解决了ListView的劣势:

  • 默认支持 Linear、Grid、StaggeredGid 三种布局,且每种布局可以实现横向、纵向的展示。
  • 支持一些动画API
  • 强制实现ViewHolder
  • 代码架构设计解耦,便于程序开发
  • 性能更好

上面优势中有个代码架构设计解耦,是因为RecyclerView分成了多个组件构成,比较重要的有:

  • RecyclerView
  • LayoutManager
    负责View的布局和绘制
  • Adapter
    适配器模式,将View适配到屏幕上
  • ItemAnimator
    用于动画

2. ViewHolder

ViewHolder是RecyclerView中必须要实现的机制,在ListView中可以选择实现,ViewHolder是什么?它和item view又是什么关系呢?

ViewHolder在RecyclerView和ListView中有区别么?
没有区别,他们都是为了解决一个问题而产生的。这个问题是什么,请看下面的代码展示:

2.1 ListView中的ViewHolder实现

我们先看看ListView实现和不实现ViewHolder,来看看有什么区别。
我们知道,ListView实现View的生成是在getView()中的,并且在ListView中,每个item的名称叫做 convert_view,而在RecyclerView中,每个item的名称叫做 item_view,他们本质是一样的。

1. 不实现ViewHolder的ListView

public View getView(int position, View convertView, ViewGroup parent) {  
    Fruit fruit = getItem(position);  
    View view;  
    // 1
    if (convertView == null) {   
        view = LayoutInflater.from(getContext()).inflate(resourceId, null);  
    } else {  
        view = convertView;  
    }  
    
    // 2
    ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);  
    TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);  
    
    // 3
    fruitImage.setImageResource(fruit.getImageId());  
    fruitName.setText(fruit.getName());  
    return view;  
}  

上面的代码可以分成三个部分,
注释1部分:系统返回一个 convertView,这个是系统的复用,在代码中看看这个convetView是不是空的, 如果是空的就给它布局,如果不是就直接复用。

注释2部分:通过 findViewById找到目标控件

注释3部分:让数据源给设置数据。

这是一个没有使用ViewHolder的ListView。接下来看看实现了ViewHolder的代码是什么样的:

2. 实现了ViewHolder的ListView

public View getView(int position, View convertView, ViewGroup parent) {
         Fruit fruit = getItem(position);  
         ViewHolder holder = null;
         // 1
         if (convertView == null) {
             convertView = mInflater.inflate(R.layout.lv_item, null);
             holder = new ViewHolder();
             holder.textView = (TextView)convertView.findViewById(R.id.fruit_name);
             holder.imageView = (ImageView)convertView.findViewById(R.id.fruit_image);
             convertView.setTag(holder);
         } else {
             holder = (ViewHolder)convertView.getTag();
         }

         // 2
         holder.fruitImage.setImageResource(fruit.getImageId());  
         holder.fruitName.setText(fruit.getName()); 
         return convertView;
     }
}

 public static class ViewHolder {
     public TextView fruitName;
     public ImageView fruitImage;
 }

上面是一个实现了ViewHolder的ListView,它的代码可以分成两个部分:

注释1部分:复用ListView的 convertView
(1)如果为空,为其创建一个布局和ViewHolder(一个装载着布局的容器),然后通过 findViewById()找到目标控件,将convertView和目标控件装进ViewHolder,通过 setTag()装到view中去
(2)如果不为空,从convertView中取出 ViewHolder

注释2部分:让数据源给设置数据,设置在 ViewHolder的成员----目标控件中去。

1.2 通过上述代码了解ViewHolder作用

对比上节中的两块代码,我们发现实现了 ViewHolder的代码和不实现的只有一个区别:
不实现ViewHolder的getView无论如何,每次都要通过findViewById去寻找目标控件,而使用了ViewHolder之后,它不用每次都做findViewById()去寻找目标控件,这也正是ViewHolder解决的问题

这也解答了为什么使用了ViewHolder能提高性能?
findViewById在Android底层,所有资源ID应该是以树的形式存储的,然后这个方法实现是通过 DFS去寻找目标控件的,DFS的时间复杂度是 O(n)。
O(n)的复杂度对于程序开发来说,其实是算快的,但是在convertView非常多,view里面的控件也非常多,那每次getView都时候都要调用findViewById,这对性能来说还是会容易产生大量的消耗。

所以ViewHolder的出现,就是为了减少findViewById()的使用。

1.3 ViewHolder和View的关系

我之前也误解,以为一个 ViewHolder里面会存放多个View,但通过上面的代码就知道我是错的。

ViewHolder和View就是一对一的关系,ViewHolder里面装的不是多个View,而是一个View和它里面的控件。是这样的:
在这里插入图片描述
在RecyclerView里面也是如此,它和ListView不同之处是在于它让Adapter强制的依赖于ViewHolder,也就是要我们强制使用。

3. RecyclerView的缓存机制原理

RecyclerView的缓存机制是一个较为庞大的体系,这里不会去具体解析源码,但是会了解RecyclerView是如何缓存的。

由于ListView也是由它的缓存机制,并且比RecyclerView简单很多。而且RecyclerView缓存的本质和ListView的是差不多的。
所以我先从简单的ListView看起。

3.1 ListView的缓存机制

在这里插入图片描述
每当要找一个目标 convertView时,ListView先会去找 RecycleBin(回收站)
第一步:RecycleBin在ActiveView(即在屏幕上显示的、活跃的View)中寻找有没有目标View
第二步:如果第一步没有找到,则取 Scrap View(即废弃的,丢掉的View)中寻找有没有目标View
第三步:如果前两步都没有缓存的目标View,则通过 getView()里面去创建一个新的View。

再看下图:
在这里插入图片描述
假如框框是我们的屏幕,那么框框里面显示的View就是ActivieView。

这个时候,假设屏幕往上滑动,那么最下面的View是没有创建过的,这个时候RecycleBin就会去ScrapView中找,假如我们新的convertView的type是1,而 ScrapView中正好也有type为1的View,那么,ListView就会通过getView()返回这个View
由于这个View的数据是脏的,所以需要对它重新的赋值。

这里还有一个问题:为什么会有ActiveView缓存呢?这些View都没有离开屏幕,我们为什么要缓存它??
解答:在Android的屏幕渲染机制中,我们知道是每隔16.666ms刷新一次View,在刷新的时候,需要清干净屏幕上的内容,然后再显示,或者我们切到别的界面,再切回来,这个时候屏幕要渲染数据,如果ListView这个时候发现Adapter数据没有变,那么就会直接从ActiveViews中渲染数据。
这里有个ListView的源码解析,就讲解了这一个部分:ListView缓存机制的实现

这就是ListView的缓存机制的原理。它是二级实现。它的本质就是复用。
接下来我们看下RecyclerView是如何缓存的。

3.2 RecyclerView的缓存机制

在这里插入图片描述
RecyclerView实现的缓存层级比ListView更多,它有四级缓存,缓存的内容就是 ViewHolder,因为ViewHolder装载了View的信息,所以缓存ViewHolder就是缓存View。

  • Scrap
    和ListView中的ActiveView一样。
    查看当前屏幕上是否有View可以复用。
  • Cache
  • ViewCacheExtension
    开发者自己实现的缓存策略
  • RecycledViewPool
    列表池,和复用也有关系。

当四级缓存都找不到目标View时,并通过我们 onCreateViewHolder()来创建。
再看下图,看看他们的缓存场景:
在这里插入图片描述

  1. ScapView
    和ListView中的ActiveView作用一致,具体看3.1节。
  2. Cache
    只存储少量的刚从屏幕上消失的item。只关系position
    它的作用是在用户来回滑时,直接通过position在Cache中拿到对应的View。直接渲染在屏幕上。
  3. ViewCacheExtension
    开发中自定义缓存机制。平时开发中基本也没用到,等下看个例子。
  4. RecycledViewPool
    复用池,它可以存储大量的,消失很久的item。如果上述1、2、3都不能取出时,则从RecycledViewPool池中取出。这个时候由于数据是脏的,所以需要重新渲染。
    只关心viewType。

上述1、2级缓存是直接取出来用。不会走 onCreateViewHolderonBindViewHolder
第4级不用走 create方法,但是要走 onBindViewHolder()进行数据渲染。

4. RecyclerView性能优化策略

4.1 不要在onBindVieHolder里设置点击监听事件

通过RecyclerView的缓存机制,我们知道item view被复用其实还是很频繁的。
尤其是 RecycledViewPool缓存机制它会调用onBindViewHolder()可能会执行多次。那其实一直在这个地方设置监听器,监听器如果内容少(ViewType少,if…else少,或者需要设置点击事件的地方少)就还好,但是如果多的话可能就会造成内存抖动。

解决方案:
onCreateViewHolder()中设置监听事件

4.2 LinearLayoutManager.setInitialPrefetchItemCount()

这个使用的场景比较特殊,又比较常见,看下下面这个图:

在这里插入图片描述
这是微博,我们刷微博的时候是纵向的,它是纵向的 RecyclerView。但是有时候会出来一些推荐的东西。
就比如上面红色框框弹出的“微博故事”,它就是在RecyclerView滑着滑着的时候,突然出现一个 横向的RecyclerView。

这个场景会产生一个问题:
由于需要创建更复杂的RecyclerView以及多个子View,在显示这个页面的瞬间,可能会产生卡顿。

所以这个时候就可以调用 LinearLayoutManager.setInitialPrefetchItemCount(int n)这个API,它的作用是定义横向列表初次显示时可见的item个数。也就是说它会做一个预渲染。
它有这么几个特点:

  1. Android5.0后加入了 RenderThread来缓解UI线程大量渲染导致的压力。
    而 RecyclerView就是在这个线程上做了 prefetch数据预读取,
  2. 只有 LienarLayoutManger才有这个API,StaggeredGid、Grid则没有。
  3. 只有嵌套在内部的 RecyclerView才会生效。外部的RecyclerView则不会。

4.3 RecyclerView.setHasFixedSize()

FixedSize英文意思就是固定尺寸。
在RecyclerView中,如果内容改变了会有这么一个情况:

  • 如果设置 setHasFixedSize(true)即 设置 mHasFixedSize == true
    调用 layoutChildren()
  • 如果没有设置
    调用 requestLayout()

我们知道 requestLayout重走一遍绘制流程。而 layoutChildren则只用layout子View并绘制。
显然前者耗费的性能是大于后者的。我们当然希望能少走绘制流程。

而使用这个Api的前提是:如果Adapter的数据变化不会导致RecyclerView的宽高变化,则调用 RecyclerView.setHasFixedSize(true)可以优化性能。
这里有一篇解析可供参考:RecyclerView setHasFixedSize 作用

4.4 多个RecyclerView共用缓存(RecycledViewPool)

我们可以在多个不同的RecyclerView间共用RecycledViewPool
假如一个Activit中有好几个RecyclerView,(比如他们是通过viewpager+tab方式展示),并且他们的viewType是共用的,那么我们可以调用下面代码来共用他们的第四级缓存:

RecyclerView.RecycledViewPool rvp = new RecyclerView.RecycledViewPool();
recyclerViewA.setRecycledViewPool(rvp);
recyclerViewB.setRecycledViewPool(rvp);
recyclerViewC.setRecycledViewPool(rvp);
recyclerViewD.setRecycledViewPool(rvp);

其实平时一些使用的RecyclerView如果比较简单,或者甚至没有设置ViewType,那么我们就可以调用这样的方法来共用第四级缓存。

4.5 DiffUtil

有这么一个场景:
我们都知道 notifyDataSetChange(),每次拿到数据时,我们都调用这个方法来渲染整个布局。而后来RecyclerView还有局部更新的方法 notifyItemSetChange(),但有的时候,我们也不是很了解每个item的位置,这个方法用起来,好像要做很多事情。
所以我们就偷懒,反正一有数据更新,我们就调用 notifyDataSetChange。这样做会有什么弊端:

  • 整个布局跑一遍绘制流程
  • 重新创建+绑定ViewHolder
  • 会失去动画效果

DiffUtil就是为了解决这个问题而产生的,它适用于整个页面需要刷新,但是有部分数据可能相同的情况
它的作用是可以计算新的List和旧的List哪些数据改变了,根据这个改变去局部的绘制,而不是重跑一遍绘制流程。
我们来看下其部分源码:

// DiffUtil.java
    public abstract static class Callback {
        public Callback() {
        }
        public abstract int getOldListSize();
        public abstract int getNewListSize();
        public abstract boolean areItemsTheSame(int var1, int var2);
        public abstract boolean areContentsTheSame(int var1, int var2);
        @Nullable
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return null;
        }
    }

其实这些参数,就可以看出 DiffUtil计算的变化的角度:大小、内容。通过Callback流程,它会去计算前后数据源的变化,看下它的调用链:
areItemsTheSame:item有没有变化(viewType、position等信息),如果发生了变化则真个结构要发生变化,否则调用areContentsTheSame
areContentsTheSame:item的内容有没有变化(属性),如果没有,则说明Adapter数据源没变,则不重绘。否则调用 getChangePayload方法
getChangePayload :通过动态规划来计算出item的哪些属性发生了变化。

当我们要使用时,我们需要创建一个类来继承 DiffUtil.Callback,通过实现这些类来计算出增量,然后调用:

DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
result.dispatchUpdatesTo(adapter);

网上有很多玩法和解析,这里不再列举了。

DiffUtil计算时间
getChangePayload()areItemsTheSame()areContentsTheSame()这些算法时,其实会计算一些时间。
因为我们在主线程的,所以我们就有必要去考虑这些计算的时间。下图是谷歌官方针对于多个item和改动所计算的样本时间:
在这里插入图片描述
可以看到对于一些小的改动,几乎没什么消耗,但是对很多、很大的变动,可能会有20、30+ms的消耗。
因为他是小于16.66ms的,所以可能会产生掉帧。

所以在列表很大的时候,我们有必要异步计算diff,这里有几个方案:

  • 使用开创Thread去计算,计算后通过Handler发给主线程
  • 使用Rxjava做线程切换
  • 谷歌考虑到了这个情况,所以提供了两个类 AsyncListDiff/ListAdapter的Api供我们使用。网上也有文章讲解了如何使用。

5. ItemDecoration

ItemDecoration是LinearLayoutManger的内部类。

5.1 画分割线

我们会使用 :

rv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

来添加分割线,DividerItemDecoration是谷歌专门的为分割线而写的类。
我们平时实现分割线的方式除了这个,还可以直接在我们的item的layout xml里面添加分割线。但是这样对性能不好
因为我们会根据position来显示/隐藏分割线,这必然会导致我们在ViewHolder中存储分割线。根据缓存机制,我们知道,RecyclerView的ViewHolder存储能少一个View是一个View,能少一次findViewById就少一次 O(n)的遍历。

而ItemDecoration的设计是为了让我们能在RecyclerView的装饰上玩出花来,但是我们玩的花样也不多,最多的就是加分割线,所以谷歌后来才设计了这个东西。

它有两种绘制方式,本质上是通过Drawable.setBounds()来确定大小边界:

  • inset
    绘制item之间,这样做不会是改变item的高度/宽度。它是官方默认实现。
  • overlay
    通过 ItemDecoration.onDrawOver,绘制在item里面,在item最底部的上面。

5.2 其他作用

ItemDecoration不仅仅能画出分割线,他还有一些其他的作用。

  • HighLights
    这个看官方文档不是很懂…
    就是说它可以对Item进行高亮处理。
  • 分组
    RecyclerView可以实现像通信录那样,家人分一块,同事分一块这样。具体实现要看专门的例子。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal 是 Java 中的一个类,它提供了一种线程局部变量的机制。线程局部变量是指每个线程都有自己的变量副本,每个线程对该变量的访问都是独立的,互不影响。 ThreadLocal 主要用于解决多线程并发访问共享变量时的线程安全问题。在多线程环境下,如果多个线程共同访问同一个变量,可能会出现竞争条件,导致数据不一致或者出现线程安全问题。通过使用 ThreadLocal,可以为每个线程提供独立的副本,从而避免了线程安全问题。 ThreadLocal 的工作原理是,每个 Thread 对象内部都维护了一个 ThreadLocalMap 对象,ThreadLocalMap 是一个 key-value 结构,其中 key 是 ThreadLocal 对象,value 是该线程对应的变量副本。当访问 ThreadLocal 的 get() 方法时,会根据当前线程获取到对应的 ThreadLocalMap 对象,并从中查找到与 ThreadLocal 对象对应的值。如果当前线程尚未设置该 ThreadLocal 对象的值,则会通过 initialValue() 方法初始化一个值,并将其存入 ThreadLocalMap 中。当访问 ThreadLocal 的 set() 方法时,会将指定的值存入当前线程对应的 ThreadLocalMap 中。 需要注意的是,ThreadLocal 并不能解决共享资源的并发访问问题,它只是提供了一种线程内部的隔离机制。在使用 ThreadLocal 时,需要注意合理地使用,避免出现内存泄漏或者数据不一致的情况。另外,由于 ThreadLocal 使用了线程的 ThreadLocalMap,因此在使用完 ThreadLocal 后,需要手动调用 remove() 方法清理对应的变量副本,以防止内存泄漏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值