Android端Native业务开发中涉及到列表的场景时,我们最先想到的就是使用RecyclerView
或ListView
(两者基本原理是相通的,以下均以RecyclerView
为例说明),这篇文章将尝试通过基于一个具体场景来说明RecyclerView
内部的优化原理。
全量加载替换为逐项懒加载
为了对比说明RecyclerView
的优势,这里以LinearLayout
作为参考进行对比。
在列表场景中,我们使用RecyclerView
来代替LinearLayout
,是因为前者通过对单项ItemView
进行加载的方式来渲染整个页面,而不是基于整个页面进行一次性渲染。
举个例子,假如一个页面需要加载有100项的列表,其中每一项加载耗时需要5ms。如果使用传统的LinearLayout
,一次性全部加载的时间就是100 * 5ms = 500ms
,500ms加载时间对于用户而言是什么?以Android系统为了使得用户能在端上拥有60FPS的体验,而做出的每过1s / 60FPS ≈ 16ms
发出一个VSYNC
信号进行UI刷新为准,500ms就意味着在加载这个列表时将出现(500ms - 16ms) / 16ms ≈ 30
帧的丢失,而用户感受到的就是当前列表加载时非常卡顿。而如果使用RecyclerView
方式进行进行渲染,则每项渲染时间为5ms,两项渲染的间隙里仍然可以进行VSYNC
对应的UI刷新操作,这种场景下几乎不会造成丢帧现象。
除此之外,RecyclerView
对单项进行加载并非简单地从1次加载100项拆分为100次加载各加载1项,它在此基础上还做了懒加载的操作,即只有当用户界面滑动到当前项时才会进行加载。
举个例子,用户手机一屏最多能展示10项时,此时RecyclerView
便只会渲染10次,每次渲染1项。当用户往下滑动到第11项准备要“露出来”的时候,RecyclerView
才会渲染第11项,剩下的89项也是同样的加载方式。这样的设计带来的好处同常用的懒加载类似,就是将短时间内的密集型任务进行离散化分配,尽可能降低CPU的压力。
该特性是RecylerView内部实现的,面向开发者是透明的
ItemView复用
仅仅单项懒加载还不够,还要更快,这时我们需要将矛头指向耗时操作的罪魁祸首——LayoutInflater#inflate
。在这一步中,Android系统会去解析layout.xml
的资源文件,然后还可能会进行一系列的反射操作来实例化View,这一步的操作相较于View信息的填充而言,是尤为耗时的。而对于100项的列表,如果我们每次都去进行inflate
操作的话,也将会对运行性能带来不少压力。
RecyclerView
对应该问题解法就是复用,它内部会有个mCachedViews
的缓存变量,可以抽象理解为里面存储着每一个创建的子View,每次渲染子View时如果缓存中有未使用的子View会优先从缓存中取,当缓存的View不足以界面展示时,它就会再去创建新View并添加进去。
比如,总共需要展示100项,一屏显示10项,此时缓存中包含10个子View。
- 当滑动到第11项时,发现缓存的10项全部用来UI展示,没有未使用的子View,此时便再创建一项子View并放到缓存中
- 当滑动到第12项时,此时第1项已经完全从屏幕上方滑到屏幕显示之外了,该项便是未使用的子View,此时
RecylerView
会直接复用该项来渲染第12项 - 后续项的加载都会隶属于上面的两种情况
注:该特性也是RecyclerView内部实现的,一样不需要开发者额外处理;而更早设计的ListView会不存在该特性的,我们开发者一般会通过
View#setTag
方式来手动实现该特性。这也是为什么RecyclerView的官方Api中直接显式的提出ViewHolder
概念而ListView中并未有任何ViewHolder
概念(ListView的ViewHolder
是我们开发中手动加入的)的原因。