RecyclerView的缓存和优化
一:RecyclerView缓存的是啥?
ListView缓存的是ItemView。
RecyclerView
缓存RecyclerView.ViewHolder,这个ViewHolder中持有对应的ItemView的所有信息,比如说position,view,width,flag等等.
二:RecyclerView的四级缓存
一级缓存:屏幕内缓存(mAttachedScrap)
屏幕内缓存指在屏幕中显示的ViewHolder,为了屏幕内 item 快速复用而存在, (RecyclerView/ListView具有两次 onLayout() 过程,第二次onLayout() 中直接使用第一次 onLayout() 缓存的 View,而不必再创建)。这些ViewHolder会缓存在mAttachedScrap、mChangedScrap中 :
mChangedScrap 表示数据已经改变的ViewHolder列表,需要重新绑定数据(调用onBindViewHolder), 该层缓存目的是为了当调用notifyItemChanged(pos),notifyItemRangeChanged(pos,count)后该位置信息发生改变的缓存
mAttachedScrap 未与RecyclerView分离的ViewHolder列表, 该层缓存目的是在调用notfyXxx时未改变的item,以及影响RecyclerView重新绘制的情况。
二级缓存:屏幕外缓存(mCachedViews)
用来缓存移除屏幕之外的 ViewHolder,默认情况下缓存容量是2,可以通过 setViewCacheSize 方法来改变缓存的容量大小。如果mCachedViews 的容量已满,根据FIFO规则会优先移除旧 ViewHolder,把旧ViewHolder移入到缓存池RecycledViewPool 中。mCachedViews中携带了原来的ViewHolder的所有数据信息,可以直接拿来复用. mCachedViews是根据position 来匹配相应的 ViewHolder 的,这里的 position 指的是 RecyclerView 预测的、可能进入屏幕的 item 的 position,它是由当前屏幕滑动方向和可见的 item 位置来共同决定的。例如:屏幕向下滑动,那么可能进入屏幕的 item 的 position 就是当前可见第一个 item 的 position - 1;屏幕向上滑动,那么可能进入屏幕的 item 的 position 就是当前可见的最后一个 item 的 position + 1。
举个栗子:当前屏幕内第一个可见的item的position是1,用户进行了一个下拉操作,那么当前预测的position就相当于(1-1=0),也就是position=0的那个item要被拉回到屏幕,此时RecyclerView就从Cache里面找position=0的数据,如果找到了就直接拿来复用。
三级缓存:自定义缓存(ViewCacheExtension)
给用户的自定义扩展缓存,需要用户自己管理View 的创建和缓存,可通过Recyclerview.setViewCacheExtension()设置。
四级缓存:缓存池(RecycledViewPool )
ViewHolder 缓存池,在mCachedViews中如果缓存已满的时候(默认最大值为2个),先把mCachedViews中旧的ViewHolder 存入到RecyclerViewPool。如果RecyclerViewPool缓存池已满,就不会再缓存。从缓存池中取出的ViewHolder ,需要重新调用bindViewHolder绑定数据。
按照 ViewType 来查找ViewHolder
每个 ViewType 默认最多缓存 5 个
可以多个 RecyclerView 共享RecycledViewPool
为啥要有四缓: 可以由开发者主动向内填充数据(RecycledViewPool#putRecycledView(ViewHolder)),技术上可以实现多个 RecyclerView 共用同一个RecyclerViewPool
三:RecyclerView的缓存策略
按四级缓存的策略查找,没有找到就创建(如果取到直接丢给rv来展示,如果取不到最终才会执行熟悉的oncreateviewholder和onbindviewholder方法),其中只有RecyclerViewPool找到时才会调用onBindViewHolder,流程如下:
四:RecyclerView的缓存过程
场景一
先看图的左边(此时假设cache 与 pool 中没有东西),当向下滑动时,3 最先进入 mCachedViews,随后是 4 与 5,5 会将3挤出来,3就会跑到 pool 中去了。
再看图的右边,继续向下滑动时,4被 6 挤出来,放到了 pool 中,同时,8需要显示,那么就会先从 pool 中取,发现正好有一个 3,那么就会取出来,将 3 重新显示到屏幕上。
场景二:
如果,向下滑倒7显示出来之后,不再继续向下,而是往上滑动,那么又会怎么样呢?
看图的右边,很明显,5 从 cache 中被取出来直接复用,不用重新绑定,7 被放入了 cache 中。
五:RecyclerView的优化
RecyclerView做性能优化要说复杂也复杂,比如说布局优化,缓存,预加载等等。
其优化的点很多,在这些看似独立的点之间,其实存在一个枢纽:Adapter。
因为所有的ViewHolder的创建和内容的绑定都需要经过Adaper的两个函数onCreateViewHolder和onBindViewHolder。
因此我们性能优化的本质就是要**减少这两个函数的调用时间和调用的次数**。
1.从减少方法的调用次数来看:
(1)setItemViewCaches(int)
RecyclerView可以设置自己所需要的ViewHolder的cacheViews缓存数量,默认大小是2。CacheViews中的缓存只能position相同才可得用,且不会重新bindView,CacheViews满了后移除到RecyclerPool中,并重置ViewHolder,如果对于可能来回滑动的RecyclerView,把CacheViews的缓存数量设置大一些,可以减少bindView的次数,加快布局显示。
注:此方法是拿空间换时间,要充分考虑应用内存问题,根据应用实际使用情况设置大小。
(2).setRecyclerViewPoll(复用poll缓存)
RecyclerView设置一个ViewHolder的对象池,这个池称为RecycledViewPool,这个对象池可以节省你创建ViewHolder的开销,更能避免GC。即便你不给它设置,它也会自己创建一个。
如果多个RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置RecyclerView.setRecycledViewPool(pool); 来共用一个RecycledViewPool。
RecycledViewPool使用:先从某个RecyclerView对象中获得它创建的RecycledViewPool对象,或者是自己实现一个RecycledViewPool对象,然后设置个接下来创建的每一个RecyclerView即可。
应用场景a)针对item中包含rv的情况下才适用,如果rv的item都是普通的布局就不需要复用poll
b). Tabs+ViewPager+RecyclerView
c).一个竖直的recyclerview包含多行可分别左右滑动的recyclerview
(3).recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);
当我们调用notifyDataSetChanged() 或者 notifyItemRangeChanged(i, c) (c这个范围非常大的时候),那么很多 viewHolder 都会最终被放入到 pool 中,因为 pool 只能放置5 个,那么多余的就会被丢弃,等待回收。最重要的是会重新 create 与 bind 对性能影响比较大。如果你的列表能够容纳很多行,而且使用notifyDataSetChanged 方法比较频繁,那么你应该考虑设置一下容量大小。
(4).使用局部刷新
调用了notifyDataSetChanged方法,recyclerView 不知道到底发生了什么,所以它只能认为所有的东西都发生了变化,即,将所有的 viewHolder 都放入到 pool 中。会导致整个页面范围内的ViewHolder重新调用onBindViewHolder方法这样就重复做了一次Bind操作。这时我们换用notifyItemRemoved方法。可以看到,这时只会由于第一个移除,导致新的一个position=8进入并展示,所以只有position=8调用了onBindViewHodler方法,而其他的已经绑定的ViewHolder不需要重新绑定.
2减少方法执行的时间:
(1)布局优化 降低item的布局层次constraintlayout
(2)去除冗余的setitemclick事件,一般都是在onbind方法中设置监听,但是onbindview调用时机很多,会导致在recyclerview滑动过程中创建很多对象.可以全局创建一个.
(3)避免在onbindview中进行耗时的操作
其他优化:
设置高度固定
如果item高度是固定的话,可以使用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源。
Notify一系列方法会执行到下面这个方法
区别就在于当设置过setHasFixedSize会走if分支,而没有设置则进入到else分支,else分支直接会调用到requestLayout方法,该方法会导致视图树进行重新绘制,onmeasure,onlayout最终都会被执行到,根据上述源码可以得到一个优化的地方在于,当item嵌套了rv并且rv没有设置wrap_content属性时,我们可以对该rv设置setHasFixedSize,这么做的一个最大的好处就是嵌套的rv不会触发requestLayout,从而不会导致外层的rv进行重绘
增加RecyclerView预留的额外空间
额外空间:显示范围之外,应该额外缓存的空间
new LinearLayoutManager(this) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
return size;
}
};
一屏只能显示一个元素的时候,第一次滑动到第二个元素会卡顿。
RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了缓存机制重用子 view(简而言之就是,系统只将屏幕可见范围之内的元素保存在内存中,在滚动的时候不断的重用这些内存中已经存在的view,而不是新建view)。
这 个机制在我们这里会导致一个问题,启动应用之后,在屏幕可见范围内,我们只有一张卡片可见(估计作者的屏幕比较小),当我们滚动的时 候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个feed的时候就会有一定的延时,但是第二个feed之 后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。
getExtraLayoutSpace将返回LayoutManager应该预留的额外空间(显示范围之外,应该额外缓存的空间)。
SwapAdapter
我们使用RecyclerView时候,一般是setAdapter一次,之后通过调用adapter.notify()来更新数据和UI(不讨论差量更新)。一个界面中由一个RecyclerView承载所有内容,但是可以通过界面内tab_button来切换内容类别的情况,用于内容数据量较大,希望来回切换能流畅迅速。因此这里我采用了多个adapter来记录不同的类别数据,来回切换只要调用setAdapter(Adapter
adapter)即可这是一个和setAdapter类似的方法,不过,针对于界面view结构类似或者相同,需要频繁设置adapter的时候,做了优化,能够再切换的时候复用相同的viewHolder,减少一定的开销。
diffutil一个神奇的工具类
diffutil是配合rv进行差异化比较的工具类,通过对比前后两个data数据集合,diffutil会自动给出一系列的notify操作,避免我们手动调用notifiy的繁琐.
弊端:1.必须准备两个数据集
2.实现callback接口,addContentsTheSame是最难实现的,涉及到对比同type的item内容是否一致,比较效率的问题