RecyclerView浅析,憋个大招

注释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能提高性能?

findViewByI![](https://www.hualigs.cn/image/61dba891ed8ee.jpg) d在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浅析,憋个大招

在RecyclerView里面也是如此,它和ListView不同之处是在于它让Adapter强制的依赖于ViewHolder,也就是要我们强制使用。

3. RecyclerView的缓存机制原理

========================================================================================

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

由于ListView也是由它的缓存机制,并且比RecyclerView简单很多。而且RecyclerView缓存的本质和ListView的是差不多的。

所以我先从简单的ListView看起。

3.1 ListView的缓存机制


RecyclerView浅析,憋个大招

每当要找一个目标 convertView时,ListView先会去找 RecycleBin(回收站)

第一步:RecycleBin在ActiveView(即在屏幕上显示的、活跃的View)中寻找有没有目标View

第二步:如果第一步没有找到,则取 Scrap View(即废弃的,丢掉的View)中寻找有没有目标View

第三步:如果前两步都没有缓存的目标View,则通过 getView()里面去创建一个新的View。

再看下图:

RecyclerView浅析,憋个大招

假如框框是我们的屏幕,那么框框里面显示的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浅析,憋个大招

RecyclerView实现的缓存层级比ListView更多,它有四级缓存,缓存的内容就是 ViewHolder,因为ViewHolder装载了View的信息,所以缓存ViewHolder就是缓存View。

  • Scrap

和ListView中的ActiveView一样。

查看当前屏幕上是否有View可以复用。

  • Cache

  • ViewCacheExtension

开发者自己实现的缓存策略

  • RecycledViewPool

列表池,和复用也有关系。

当四级缓存都找不到目标View时,并通过我们 onCreateViewHolder()来创建。

再看下图,看看他们的缓存场景:

RecyclerView浅析,憋个大招

  1. ScapView

和ListView中的ActiveView作用一致,具体看3.1节。

  1. Cache

只存储少量的刚从屏幕上消失的item。只关系position

它的作用是在用户来回滑时,直接通过position在Cache中拿到对应的View。直接渲染在屏幕上。

  1. ViewCacheExtension

开发中自定义缓存机制。平时开发中基本也没用到,等下看个例子。

  1. 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。

这个场景会产生一个问题:

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

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

它有这么几个特点:

  1. Android5.0后加入了 RenderThread来缓解UI线程大量渲染导致的压力。

而 RecyclerView就是在这个线程上做了 prefetch数据预读取,

  1. 只有 LienarLayoutManger才有这个API,StaggeredGid、Grid则没有。

  2. 只有嵌套在内部的 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和改动所计算的样本时间:

RecyclerView浅析,憋个大招

可以看到对于一些小的改动,几乎没什么消耗,但是对很多、很大的变动,可能会有20、30+ms的消耗。

因为他是小于16.66ms的,所以可能会产生掉帧。

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

  • 使用开创Thread去计算,计算后通过Handler发给主线程

  • 使用Rxjava做线程切换

  • 谷歌考虑到了这个情况,所以提供了两个类 AsyncListDiff/ListAdapter的Api供我们使用。网上也有文章讲解了如何使用。

5. ItemDecoration

===================================================================================

ItemDecoration是LinearLayoutManger的内部类。

5.1 画分割线


上一篇:URL统一资源定位符(二)URL、URI、URN区别


下一篇:Android-搭建简单服务端+ListView异步加载数据,终局之战