RecyclerView之所以能支撑起大量列表项显示的场景,离不开其优秀的缓存机制
多级缓存
RecyclerView采用了多级缓存机制,按照优先级排列如下:
缓存 |
保存位置 |
一级缓存 |
mChangedScrap、mAttachedScrap、mCachedViews |
二级缓存 |
mViewCacheExtension |
三级缓存 |
mRecyclerPool |
一级缓存
mChangedScrap
、mAttachedScrap
和mCachedViews
所缓存的ViewHolder的类型是不同的(此处不是指的viewType类型,而是指的不同状态下的ViewHolder)
mChangedScrap
:数据有变化的ViewHolder将会缓存在此处,例如通过notifyItemChanged(position)
方法通知RecyclerView某个索引位置对应的组件数据有变化,通过该字段缓存的View无需重建,但是需要重新绑定数据(调用onBindViewHolder
方法)
mAttachedScrap
:目前在屏幕上仍然可见的组件所对应的ViewHolder将会缓存在此处,在页面绘制时,无需重建View且无需重新绑定数据
mCachedViews
:最近被移出(例如用户滑动)或即将被移出的View所对应的ViewHolder将会缓存在此处,重绘时无需重建ViewHolder且也无需重新绑定数据。主要场景是用户在短距离内快速滑出又快速划入,所以这个缓存列表并不是很大,默认长度为2,类型为SparseArray
(Android系统针对整数类型的键的Map优化后的数据结构,适用于键为整数的场景)
二级缓存
由开发者自定义,通过继承RecyclerView.ViewCacheExtension
类并重写getViewForPositionAndType
方法来实现
三级缓存
mRecyclerPool
实际就是一个SparseArray
,其键为viewType
(Int类型),值为一个ArrayList
,缓存的ViewHolder就存储在这里。
如何理解viewType
?viewType
的含义实际上是开发者决定的,例如有一个列表,列表的第一个组件始终是一个广告,除此之外,下面的组件都是视频封面组件(参考B站手机端主页)。那么我们就可以定义两种viewType
:
1 2
| val ad_type = 1 val video_type = 2
|
然后我们就可以重写RecyclerView.Adapter
的getItemViewType
方法:
1 2 3
| override fun getItemViewType(position: Int): Int = if (position == 0) { ad_type } else { video_type }
|
这样一来,就会通过重写的getItemViewType
方法来获取viewType
的值并传入到onCreateViewHolder
的viewType
参数中。实际上,在调用onCreateViewHolder
之前,会先根据getItemViewType
返回的值去缓存中寻找是否存在现成的ViewHolder对象(此处是在三级缓存中进行查找)避免重复创建。所以getItemViewType
的返回值也会作为缓存查找的依据,而不仅仅是position
缓存失效
如果在三级缓存中都没有找到符合条件的可重用ViewHolder,那么就会执行创建新ViewHolder的流程(onCreateViewHolder
→ onBindViewHolder
)
请思考一个问题:这个新创建出来的ViewHolder会被立刻缓存吗?如果是,将会换存到我们上面说的哪一级缓存中?
答案是并不会被立刻缓存起来。你可能会有疑惑,既然我都创建它了,说明其需要显示在屏幕上,那不应该被换存到mAttachedScrap
中吗?对于这个问题,我们需要深入RecyclerView的布局流程以及缓存时机。
众所周知,RecyclerView的布局是代理给LayoutManager
进行的,缓存实际上是LayoutManager
在重新布局前,通过调用Recycler
的相关方法执行的。在如下情况下会导致重新布局:第一次显示、用户滑动列表、各种可能导致RecyclerView尺寸发生变化的情况(例如旋转屏幕、键盘弹出)。
在这些场景下,LayoutManager
会先调用Recycler.detachAndScrapAttachViews
方法,这个方法会遍历当前RecyclerView中所有attached状态的子View,将它们的ViewHolder对象放入mAttachedScrap
中(缓存就是在这里!),并将其状态从attached设为detached(这两个状态是通过位字段标识),接着LayoutManager
会重新布局,重新布局时需要用到的View的信息是通过调用Recycler
相关的方法获取的,Recycler
查找View的顺序是:
mAttachedScrap
/ mChangedScrap
: 优先查找那些被“废弃”的 View,这些 View 仍然保留着布局信息,可能不需要重新测量。如果匹配到,且不需要更新数据,则直接使用。
mCachedViews
: 其次查找最近的缓存。
mRecyclerPool
: 再次查找回收池。
onCreateViewHolder()
: 如果以上缓存都没有,才创建新的 ViewHolder
。
其实就是我们上面讲的根据缓存优先级进行查找,这里就联系起来了。
在这个过程中每用到一个已缓存的View,就将其detached状态清除(即变为attached状态)
布局完成后仍然是detached状态的View所对应的ViewHolder会被Recycler
集中处理,要么进入mCachedViews
,要么进入mRecyclerPool
。
通过上面的过程,我们可以看出来缓存是随着布局进行而进行的,是一个连续的过程,而不是在某一个时间点进行所谓的“集中缓存”
自定义缓存策略
自定义缓存需要我们继承RecyclerView.ViewCacheExtension
并实现getViewForPositionAndType
方法,这里以前面所说的类似B站首页第一个组件是广告,其余组件为视频封面为例:
先编写Adapter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| class VideoAdAdapter(val dataList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { val VIDEO_TYPE = 1 val AD_TYPE = 2 }
val caches = SparseArray<View>(1)
class VideoHolder(view: View): RecyclerView.ViewHolder(view) { val cover_img = view.findViewById<ImageView>(R.id.video_img_iv) val title_tv = view.findViewById<TextView>(R.id.video_title_tv) }
class ADHolder(view: View): RecyclerView.ViewHolder(view) { val ad_img = view.findViewById<ImageView>(R.id.ad_img_iv) }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { if (viewType == VIDEO_TYPE) { val view = LayoutInflater.from(parent.context).inflate(R.layout.video_cover_item, parent, false) return VideoHolder(view) } else if (viewType == AD_TYPE) { val view = LayoutInflater.from(parent.context).inflate(R.layout.ad_item, parent, false) return ADHolder(view) } else { throw Exception("viewType is invalid") } }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is VideoHolder -> { holder.cover_img.setImageResource(R.drawable.video_cover) holder.title_tv.text = dataList[position] } is ADHolder -> { holder.ad_img.setImageResource(R.drawable.ad) caches.put(position, holder.itemView) } } }
override fun getItemCount(): Int { return dataList.size }
override fun getItemViewType(position: Int): Int { return if (position == 0) AD_TYPE else VIDEO_TYPE } }
|
VideoAdAdapter中声明的caches
成员变量将在MyCacheExtension
中用到
注意这里一定要重写getItemViewType
方法来保证返回正确的viewType,否则后续创建ViewHolder以及缓存的时候会出现问题
承载RecyclerView的Fragment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class FrgFourthPage : Fragment() { private lateinit var viewBinding: FragmentFrgFourthPageBinding private lateinit var adapter: VideoAdAdapter
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { viewBinding = FragmentFrgFourthPageBinding.inflate(layoutInflater) return viewBinding.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val rv = viewBinding.videoAdRv adapter = VideoAdAdapter(getData()) rv.adapter = adapter val layoutManager = LinearLayoutManager(requireContext()) layoutManager.orientation = LinearLayoutManager.VERTICAL rv.layoutManager = layoutManager rv.setViewCacheExtension(MyCacheExtension()) }
private fun getData(number: Int = 100): ArrayList<String> { val list = ArrayList<String>() for (i in 1..number) { list.add(i.toString()) } return list }
inner class MyCacheExtension: RecyclerView.ViewCacheExtension() { override fun getViewForPositionAndType( recycler: RecyclerView.Recycler, position: Int, type: Int ): View? { return if (type == AD_TYPE) { adapter.caches[position] } else { null } } }
}
|
在此Fragment的最后,我们定义了MyCacheExtension
,这是自定义缓存的关键,在其getViewForPositionAndType
方法中,根据当前传入的类型来返回相应的View
对象,如果是AD_TYPE
类型,则直接返回缓存在adapter.caches
中的对象
页面布局:
fragment_frg_fourth_page.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".FrgFourthPage">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/video_ad_rv" android:layout_width="match_parent" android:layout_height="wrap_content" />
</FrameLayout>
|
video_cover_item.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">
<ImageView android:id="@+id/video_img_iv" android:layout_width="match_parent" android:layout_height="300dp" android:scaleType="centerInside" android:contentDescription="" />
<TextView android:id="@+id/video_title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal"/>
</LinearLayout>
|
ad_item.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content">
<ImageView android:id="@+id/ad_img_iv" android:layout_width="match_parent" android:layout_height="400dp" android:scaleType="centerCrop" android:contentDescription=""/>
</FrameLayout>
|