关于RecyclerView的优化,其实无非两点,尽可能的最大化使用viewholder的缓存,如果不能使用缓存,将构建和绑定viewholder的过程耗时降低到最低。
缓存
这里再总结下RecyclerView的缓存知识:
分类
- scrap缓存:用于缓存页面暂时分离的viewholder,分为changeScrap和attachScrap,changeScrap用于缓存要update的viewholder,attachscrap缓存非update的viewholder。 不作用于页面滑动,开发干预不了该缓存。它缓存的个数是一屏的viewholder。
- cacheview缓存:是一个arraylist的结构,在滑动的时候,滑出屏幕的viewholder会保存到该缓存中,默认保存的数量是2个,当超过2个的时候,会先移除集合中第一个viewholder,并把该viewholder进行重置。 然后加入到recyclerpool缓存中,最后把新的viewholder加入到cacheview缓存尾部,整个过程,cacheview缓存相当于是一个队列,先进先出的规则。它是根据position来取的,不需要重新bind
- ViewCacheExtension:mViewCacheExtension又称拓展缓存,为开发者预留的缓存池,开发者可以自己拓展回收池,一般不会用到。
- 最后就是recyclerpool缓存,它是在cacheview满了的时候,加入到该缓存中的,它是根据viewtype缓存的。在它里面的viewholder都被重置过了的,所以从它里面取出来的viewholder都需要重新bind。
缓存回收
在不滚动情况下: 会把页面上可见的viewholder缓存到scrap中,如果viewholder中有flag_update标记的时候,则把它添加到changeScrap中,否则加入到attachScrap中。 在滚动情况下: 会把滑出recyclerview的viewholder先添加到cacheView缓存中,如果cacheView缓存满了话,会把集合开始的位置viewholder给放到RecycledViewPool中,RecycledViewPool会按照每种viewtype的viewholder为5的容量进行回收,如果超过5个的时候,就不会往里面存了。
缓存复用
如果是在pre-layout阶段,会去changeScrap缓存中通过position查找viewholder,如果通过position找不到,则通过id去查找,从changeScrap缓存中取出的。 在pre-layout和post-layout阶段会先从attachScrap缓存中通过position查找viweholder,如果没找到则会从cacheview缓存中通过position查找viewholder,如果没找到再通过id从attchScrap和cacheview中找viewholder,如果还没找到,则从ViewCacheExtension中找,如果还没找到则从RecycledViewPool中找,如果都没找到,则创建viewholder,由于changeScrap不参与post-layout阶段,所以在post-layout阶段会走创建表项和绑定表项,attachScrap由于是精准匹配,所以无需创建和绑定,cacheview缓存也是精准匹配,RecycledViewPool中的viewholder由于都重置了,所以需要走绑定。
缓存说明
scrap缓存只会在非滑动场景下进行保存,并且它保存的数量是一屏的数据,该缓存开发无法干预,cacheview缓存的是可以直接用的viewholder,无需bind和create,但是默认容量很小,可以动态设置,RecycledViewPool里面的viewholder都是重置过的,需要重新bind,按viewtype进行存储,每种viewtype的容量默认是5,也是可以设置大小。而ViewCacheExtension缓存虽然是扩展缓存,但是很少去用,所以可优化的缓存只有cacheview和RecycledViewPool
如何最大化使用缓存?
- 多使用scrap缓存来局部刷新
- 前面分析过使用notifyDataSetChange的时候会使可见的viewholder和缓存中的viewholder都失效了,导致所有的viewhodler都会先从pool缓存中找一遍,如果有的话,就需要重新bind,如果找不到则先走创建,然后走bind过程,所以局部刷新使用notifyItemChange和notifyItemRemove,如果只是刷新viewholder中的某一个子view,则使用payload的形式。
- 使用diffutils来实现局部刷新,无需关心刷新的索引,只需要提供变化的数据源
- 合理使用pool缓存,如果一屏展示的viewholder比较多,则可以适当增加pool缓存的最大数量,减少频繁创建viewholder
- 合理设置cacheview的缓存,如果recyclerview需要经常来回滑动,则可以适当增加cacheview的缓存数量
- 重写adapter的getItemId并且给recyclerview设置setHasStableIds为true来给每一个viewholder增加唯一索引,这样在缓存查找的时候能增大复用。
- 如果两个recyclerview的viewholder有部分相同或者基本相同的情况下,给这两个recyclerview设置同一个RecycledViewPool来增加viewholder的复用度,注意此时给recyclerview设置pool的时候,需要在设置adapter之前
- 如果同一个recyclerview需要切换视图的时候,并且视图的样式是不同的adapter的时候,此时可以考虑用swapadapter来切换adapter,swapadapter是把attachScrap给清空,并把cacheview中的缓存加入到RecycledViewPool中,而setAdapter是清空所有的缓存。由于swapadapter是复用了RecycledViewPool中的缓存,因此要求前后的viewholder是同一种类型,否则会出现异常。
构建过程如何降低到最低?
- 所谓构建就是指viewholder的create过程,该阶段主要是通过解析xml,来创建view,该阶段涉及到文件读取的io操作,以及反射生成view。一般我们可以通过动态创建view的形式来消除io操作和反射生成view。或者将xml的层级降到最低,减少inflate的时间。
- 提前解析xml,然后存放到缓存池中,等到使用的时候直接从池子中取。
绑定过程如何降低到最低?
- 给视图设置监听器的时候,不要通过直接创建listener的形式,通过外界传入进来,然后在外界处理逻辑
- 绑定视图的时候不要做计算逻辑,将计算逻辑前置化,绑定应该是一个纯展示的过程
其它
- 如果item的大小是固定的,则使用setHasFixedSize(true),这样可以避免在更新、添加、删除表项的时候重新requestLayout,而该过程会等到下一个vsync信号来的时候,走绘制流程,然后才是测量,使用该方法后,会给Choreographer发送一条animtion的消息,在下一个vsync来的时候,直接进行recyclerview的dispatchLayout。
- 开启recyclerview的预加载,recyclerview的预加载默认是开启的,如果要关闭通过layout.setItemPrefetchEnabled(false)来关闭,如果是自定义layoutmanager,则通过重写collectAdjacentPrefetchPositions来实现预加载
- recyclerview会加载屏内可见的viewholder,如果viewholder对应的itemview高度或宽度很大的时候,可能加载的屏外viewholder很少,此时重写layoutmanager的calculateExtraLayoutSpace来实现屏外的viewholder加载,关于这块可以看viewpager2如何实现的屏外viewholder的加载。