Android RecyclerView性能优化与对比

RecyclerView的优化是面试中经常提及的知识点,细节有很多,但可以分为几大类。

RecyclerView性能优化

数据处理和视图加载分离

我们知道,从远端拉取数据肯定是要放在异步的,在我们拉取下来数据之后可能就匆匆把数据丢给了 VH 处理,其实,数据的处理逻辑我们也应该放在异步处理,这样 Adapter 在 notify change 后,ViewHolder 就可以简单无压力地做数据与视图的绑定逻辑,比如:

1
mTextView.setText(Html.fromHtml(data).toString());

这里的 Html.fromHtml(data) 方法可能就是比较耗时的,存在多个TextView 的话耗时会更为严重,这样便会引发掉帧、卡顿,而如果把这一步与网络异步线程放在一起,站在用户角度,最多就是网络刷新时间稍长一点。

数据优化

分页拉取远端数据,对拉取下来的远端数据进行缓存,提升二次加载速度;对于新增或者删除数据通过 DiffUtil 进行局部刷新数据,而不是一味地全局刷新数据。

布局优化

减少过渡绘制

减少布局层级,可以考虑使用自定义 View 来减少层级,或者更合理地设置布局来减少层级。总之,减少布局层级深度。

减少 xml 文件 inflate 时间

这里的 xml 文件不仅包括 layout 的 xml,还包括 drawable 的 xml,xml文件 inflate 出 ItemView 是通过耗时的 IO操作,尤其当 Item 的复用几率很低的情况下,随着 Type 的增多,这种 inflate 带来的损耗是相当大的,此时我可以用代码去生成布局,即 new View() 的方式,只要搞清楚 xml 中每个节点的属性对应的 API 即可。

减少 View 对象的创建

一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化ItemView;设计 ItemType 时,对多ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套。

其他

其他并不代表不重要,而是我不能把他们进行分类哈,其中可能某些操作会对你的 RecyclerView 有很大的优化。

升级 RecycleView 版本到 25.1.0 及以上使用 Prefetch 功能,可参考RecyclerView 数据预取。

  • 如果 Item 高度是固定的话,可以使用RecyclerView.setHasFixedSize(true); 来避免 requestLayout浪费资源;

  • 设置 RecyclerView.addOnScrollListener(listener); 来对滑动过程中停止加载的操作。

  • 如果不要求动画,可以通过 ((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false);把默认动画关闭来提神效率。

  • 对 TextView 使用 String.toUpperCase 来替代android:textAllCaps=”true”。

  • 对 TextView 使用 StaticLayout 或者 DynamicLayout 的自定义View 来代替它。

  • 通过重写 RecyclerView.onViewRecycled(holder) 来回收资源。

  • 通过 RecycleView.setItemViewCacheSize(size); 来加大RecyclerView 的缓存,用空间换时间来提高滚动的流畅性。

  • 如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的RecyclerView 中存在一样的 Adapter,可以通过设置RecyclerView.setRecycledViewPool(pool); 来共用一个RecycledViewPool。

  • 对 ItemView 设置监听器,不要对每个 Item 都调用addXxListener,应该大家公用一个 XxListener,根据 ID 来进行不同的操作,优化了对象的频繁创建带来的资源消耗。

  • 通过 getExtraLayoutSpace 来增加 RecyclerView 预留的额外空间(显
    示范围之外,应该额外缓存的空间),如下所示:

1
2
3
4
5
6
new LinearLayoutManager(this) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state){
return size;
}
};

RecyclerView与ListView 对比浅析

ListView与RecyclerView缓存机制原理大致相似,如下图所示:

image-20221223152710518

滑动过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异.(这只是缓存使用的其中一个场景,还有如刷新等)

缓存机制对比

层级不同:

RecyclerView比ListView多两级缓存,支持多个离ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。
具体来说:
ListView(两级缓存):

image-20221223152935922

RecyclerView(四级缓存):

image-20221223153010279

ListView和RecyclerView缓存机制基本一致:
1). mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
3). RecyclerView的优势在于
a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;
b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

缓存不同

1). RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);
2). ListView缓存View。缓存不同,二者在缓存的使用上也略有差别,具体来说:
ListView获取缓存的流程:

image-20221223153337582

RecyclerView获取缓存的流程:

image-20221223153414820

1). RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView:

image-20221223153515347

而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView),相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//AbsListView源码:line2345
//通过匹配pos从mScrapView中获取缓存
final View scrapView = mRecycler.getScrapView(position);
//无论是否成功都直接调用getView,导致必定会调用createView
final View child = mAdapter.getView(position, scrapView,this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
...
}
}

2). ListView中通过pos获取的是view,即pos–>view;RecyclerView中通过pos获取的是viewholder,即pos –> (view,viewHolder,flag);
从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心。

局部刷新

由上文可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView。

image-20221223153818589

(RecyclerView和ListView添加,移除Item效果对比)
结合RecyclerView的缓存机制,看看局部刷新是如何实现的:以RecyclerView中notifyItemRemoved(1)为例,最终会调用requestLayout(),使整个RecyclerView重新绘制,过程为:onMeasure()–>onLayout()–>onDraw()

其中,onLayout()为重点,分为三步:
dispathLayoutStep1():记录RecyclerView刷新前列表项ItemView的各种信息,如Top,Left,Bottom,Right,用于动画的相关计算;
dispathLayoutStep2():真正测量布局大小,位置,核心函数为layoutChildren();
dispathLayoutStep3():计算布局前后各个ItemView的状态,如Remove,Add,Move,Update等,如有必要执行相应的动画.
其中,layoutChildren()流程图:

image-20221223153938716

image-20221223154013675

当调用notifyItemRemoved时,会对屏幕内ItemView做预处理,修改ItemView相应的pos以及flag(流程图中红色部分):

image-20221223154052503

当调用fill()中RecyclerView.getViewForPosition(pos)时,RecyclerView通过对pos和flag的预处理,使得bindview只调用一次.
需要指出,ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

总结

1、在一些场景下,如界面初始化,滑动等,ListView和RecyclerView都能很好地工作,两者并没有很大的差异:
于是便抛出了这样一个问题,微信Android客户端卡券模块,大部分UI都是以列表页的形式展示,实现方式为ListView,是否有必要将其替换成RecyclerView呢?

image-20221223154244016

答案是否定的,从性能上看,RecyclerView并没有带来显著的提升,不需要频繁更新,暂不支持用动画,意味着RecyclerView优势也不太明显,没有太大的吸引力,ListView已经能很好地满足业务需求。

2、数据源频繁更新的场景,如弹幕等,RecyclerView的优势会非常明显;
进一步来讲,结论是:列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其它情况(如微信卡包列表页)两者都OK,但ListView在使用上会更加方便,快捷。