RecyclerView

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 ,需要重新调用onBindViewHolder绑定数据。

按照 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中进行耗时的操作。

其他方面的优化:

(1).设置高度固定

如果item高度固定,可以使用RecyclerView.setHasFixedSize(true)来避免requestLayout浪费资源。

notify一系列方法会执行到下面这个方法

区别就在于当设置setHasFixedSize会走if分支,而没有设置则进入到else分支,else分支直接会调用到requestLayout方法,该方法会导致视图树进行重新绘制,onMeasure,onLayout最终都会被执行到,根据上述源码可以得到一个优化的地方在于,当item嵌套了rv并且rv没有设置wrap_content属性时,我们可以对该rv设置setHasFixedSize,这么做的一个最大的好处就是嵌套的rv不会触发requestLayout,从而不会导致外层的rv进行重绘。

(2).增加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应该预留的额外空间(显示范围之外,应该额外缓存的空间)。

(3).swapAdapter

我们使用RecyclerView时候,一般是setAdapter一次,之后通过调用adapter.notify()来更新数据和UI(不讨论差量更新)。一个界面中由一个RecyclerView承载所有内容,但是可以通过界面内tab_button来切换内容类别的情况,用于内容数据量较大,希望来回切换能流畅迅速。因此这里我采用了多个adapter来记录不同的类别数据,来回切换只要调用setAdapter(Adapter adapter)即可这是一个和setAdapter类似的方法,不过针对于界面view结构类似或者相同,需要频繁设置adapter的时候,做了优化,能够再切换的时候复用相同的viewHolder,减少一定的开销。

(4).DiffUtil一个神奇的工具类

DiffUtil是配合rv进行差异化比较的工具类,通过对比前后两个data数据集合,DiffUtil会自动给出一系列的notify操作,避免我们手动调用notifiy的繁琐.

但DiffUtil不太好用,有弊端:

(1).必须准备两个数据集

(2).实现callback接口,areContentsTheSame是最难实现的,涉及到对比同type的item内容是否一致,比较效率的问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容