注释1部分:复用ListView的 convertView
(1)如果为空,为其创建一个布局和ViewHolder
(一个装载着布局的容器),然后通过 findViewById()
找到目标控件,将convertView和目标控件装进ViewHolder,通过 setTag()
装到view中去。
(2)如果不为空,从convertView中取出 ViewHolder
注释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()
的使用。
我之前也误解,以为一个 ViewHolder里面会存放多个View,但通过上面的代码就知道我是错的。
ViewHolder和View就是一对一的关系,ViewHolder里面装的不是多个View,而是一个View和它里面的控件。是这样的:
在RecyclerView里面也是如此,它和ListView不同之处是在于它让Adapter强制的依赖于ViewHolder,也就是要我们强制使用。
========================================================================================
RecyclerView的缓存机制是一个较为庞大的体系,这里不会去具体解析源码,但是会了解RecyclerView是如何缓存的。
由于ListView也是由它的缓存机制,并且比RecyclerView简单很多。而且RecyclerView缓存的本质和ListView的是差不多的。
所以我先从简单的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是如何缓存的。
RecyclerView实现的缓存层级比ListView更多,它有四级缓存,缓存的内容就是 ViewHolder
,因为ViewHolder装载了View的信息,所以缓存ViewHolder就是缓存View。
- Scrap
和ListView中的ActiveView一样。
查看当前屏幕上是否有View可以复用。
-
Cache
-
ViewCacheExtension
开发者自己实现的缓存策略
- RecycledViewPool
列表池,和复用也有关系。
当四级缓存都找不到目标View时,并通过我们 onCreateViewHolder()
来创建。
再看下图,看看他们的缓存场景:
ScapView
和ListView中的ActiveView作用一致,具体看3.1节。
Cache
只存储少量的,刚从屏幕上消失的item。只关系position
它的作用是在用户来回滑时,直接通过position在Cache中拿到对应的View。直接渲染在屏幕上。
- ViewCacheExtension
开发中自定义缓存机制。平时开发中基本也没用到,等下看个例子。
RecycledViewPool
复用池,它可以存储大量的,消失很久的item。如果上述1、2、3都不能取出时,则从RecycledViewPool池中取出。这个时候由于数据是脏的,所以需要重新渲染。
只关心viewType。
上述1、2级缓存是直接取出来用。不会走 onCreateViewHolder
和 onBindViewHolder
第4级不用走 create方法,但是要走 onBindViewHolder()
进行数据渲染。
=======================================================================================
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个数。也就是说它会做一个预渲染。
它有这么几个特点:
- Android5.0后加入了
RenderThread
来缓解UI线程大量渲染导致的压力。
而 RecyclerView就是在这个线程上做了 prefetch数据预读取,
-
只有 LienarLayoutManger才有这个API,StaggeredGid、Grid则没有。
-
只有嵌套在内部的 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,那么我们就可以调用这样的方法来共用第四级缓存。
有这么一个场景:
我们都知道 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供我们使用。网上也有文章讲解了如何使用。
===================================================================================
ItemDecoration
是LinearLayoutManger的内部类。