掌握RecyclerView核心优化技巧,保证应用流畅度
RecyclerView作为Android开发中最核心的列表组件,相比ListView具有显著优势:
在项目中,列表数据频繁更新是常见场景。传统notifyDataSetChanged()会导致整个列表重新绑定,造成性能浪费和视觉闪烁。通过AsyncListDiffer实现增量更新:
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// 使用AsyncListDiffer进行异步差异计算
private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
val currentList: List<Item> get() = differ.currentList
fun submitList(newList: List<Item>) {
differ.submitList(newList)
}
companion object {
// 定义DiffUtil.Callback
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
// 基于唯一ID判断是否是同一个Item
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
// 基于内容判断是否相同
return oldItem == newItem
}
}
}
}
Stable IDs是RecyclerView实现精准复用的核心机制。在Adapter中启用并重写getItemId方法:
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
init {
// 启用Stable IDs
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
// 返回唯一且稳定的ID
return currentList[position].id.hashCode().toLong()
}
}
在Activity中必须显式启用:
class RecycleViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ...
adapter = MyAdapter().apply {
// 关键:必须显式启用,否则getItemId不生效
setHasStableIds(true)
}
// ...
}
}
项目中经常需要展示多种类型的Item(文本、图片、视频)。正确实现多类型ViewHolder是关键:
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemViewType(position: Int): Int {
return when (currentList[position]) {
is Item.Text -> VIEW_TYPE_TEXT
is Item.Image -> VIEW_TYPE_IMAGE
is Item.Video -> VIEW_TYPE_VIDEO
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_TEXT -> MyViewHolder.TextViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_text, parent, false)
)
VIEW_TYPE_IMAGE -> MyViewHolder.ImageViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
)
VIEW_TYPE_VIDEO -> MyViewHolder.VideoViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_video, parent, false)
)
else -> throw IllegalStateException("Unknown view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = currentList[position]
when (item) {
is Item.Text -> (holder as MyViewHolder.TextViewHolder).bind(item)
is Item.Image -> (holder as MyViewHolder.ImageViewHolder).bind(item)
is Item.Video -> (holder as MyViewHolder.VideoViewHolder).bind(item)
}
}
companion object {
const val VIEW_TYPE_TEXT = 0
const val VIEW_TYPE_IMAGE = 1
const val VIEW_TYPE_VIDEO = 2
}
}
class MyViewHolder {
class TextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView = itemView.findViewById<TextView>(R.id.text_view)
fun bind(item: Item.Text) {
textView.text = item.content
}
}
class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val imageView = itemView.findViewById<ImageView>(R.id.image_view)
private val caption = itemView.findViewById<TextView>(R.id.caption_view)
fun bind(item: Item.Image) {
Glide.with(itemView)
.load(item.imageUrl)
.into(imageView)
caption.text = item.caption
}
}
class VideoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val videoUrl = itemView.findViewById<TextView>(R.id.video_view)
private val durationView = itemView.findViewById<TextView>(R.id.duration_view)
fun bind(item: Item.Video) {
videoUrl.text = item.videoUrl
durationView.text = "${item.duration}s"
}
}
}
添加类型安全检查:
// 添加类型安全检查
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = currentList[position]
when (holder) {
is MyViewHolder.TextViewHolder -> holder.bind(item as Item.Text)
is MyViewHolder.ImageViewHolder -> holder.bind(item as Item.Image)
is MyViewHolder.VideoViewHolder -> holder.bind(item as Item.Video)
}
}
默认每个ViewType只缓存5个ViewHolder,在复杂场景下可能不足。根据项目需求调整容量:
class RecycleViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// 优化RecycledViewPool容量
recyclerView.recycledViewPool.setMaxRecycledViews(MyAdapter.VIEW_TYPE_VIDEO, 30)
recyclerView.recycledViewPool.setMaxRecycledViews(MyAdapter.VIEW_TYPE_IMAGE, 30)
recyclerView.recycledViewPool.setMaxRecycledViews(MyAdapter.VIEW_TYPE_TEXT, 30)
// ...
}
}
为了深入理解RecyclerView的缓存机制,添加详细的日志监控:
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val typeName = when (viewType) {
VIEW_TYPE_TEXT -> "Text"
VIEW_TYPE_IMAGE -> "Image"
VIEW_TYPE_VIDEO -> "Video"
else -> "Unknown"
}
// 仅当四级缓存全部Miss时触发
println(" [新建] 创建了全新的 $typeName ViewHolder")
return when (viewType) {
// ... 创建ViewHolder
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = currentList[position]
val typeName = when (item) {
is Item.Text -> "Text"
is Item.Image -> "Image"
is Item.Video -> "Video"
}
// 仅当需要重新绑定数据时触发
// 1. 新创建的ViewHolder
// 2. 从Pool取出的脏ViewHolder
// 注意:Scrap/CachedViews命中时不触发
println(" [绑定] $typeName @ pos=$position | id=${item.id}")
// ... 绑定数据
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
val typeName = when (holder) {
is MyViewHolder.TextViewHolder -> "Text"
is MyViewHolder.ImageViewHolder -> "Image"
is MyViewHolder.VideoViewHolder -> "Video"
else -> "Unknown"
}
val pos = holder.adapterPosition
// ViewHolder被回收到Pool
println(" [回收] $typeName @ pos=$pos 被踢入 RecycledViewPool")
}
}
[新建] 创建了全新的 Text ViewHolder
[绑定] Text @ pos=0 | id=text_1
[新建] 创建了全新的 Image ViewHolder
[绑定] Image @ pos=1 | id=image_2
[回收] Text @ pos=0 被踢入 RecycledViewPool
[绑定] Text @ pos=3 | id=text_4 // 从Pool复用
现象:调用notifyItemChanged后Item闪烁
解决方案:使用Payload实现局部刷新
// 在DiffUtil中重写getChangePayload
override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
if (oldItem is Item.Video && newItem is Item.Video) {
if (oldItem.likeCount != newItem.likeCount) return "LIKE_CHANGE"
}
return null
}
// 在Adapter中处理Payload
override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
payloads: List<Any>
) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
// 仅更新变化部分
payloads.forEach { payload ->
when (payload) {
"LIKE_CHANGE" -> holder.updateLikeCount()
}
}
}
}
现象:Item点击时获取的位置不正确
解决方案:使用bindingAdapterPosition
// 错误写法
holder.itemView.setOnClickListener {
onItemClick(position) // position可能失效
}
// 正确写法
holder.itemView.setOnClickListener {
val currentPos = holder.bindingAdapterPosition
if (currentPos != RecyclerView.NO_POSITION) {
onItemClick(currentPos)
}
}
现象:快速滑动时图片加载错乱
解决方案:正确使用Glide
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = currentList[position]
// 先清除之前的请求
Glide.with(holder.itemView.context).clear(holder.imageView)
Glide.with(holder.itemView.context)
.load(item.imageUrl)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error_image)
.into(holder.imageView)
}
RecyclerView 极致流畅的核心秘诀,在于其内部类 Recycler 所管理的缓存系统。当 LayoutManager 需要填充屏幕、获取某个位置的 View 时,会调用 Recycler.getViewForPosition(),而该方法的底层最终会流向整个缓存体系的核心枢纽:tryGetViewHolderForPositionByDeadline()。
整个缓存体系按照查找优先级被严格划分为四级。理解这四级缓存的容量、匹配规则以及数据清洗机制,是彻底掌握 RecyclerView 性能优化的必经之路。
源码成员:mAttachedScrap与 mChangedScrap(ArrayList 结构)
Scrap 并不是传统意义上"滑出屏幕被回收"的缓存,而是当前仍在屏幕上显示,但正处于布局计算(Layout)或动画过程中的临时分离区。
notifyItemChanged),需要配合 ItemAnimator 执行变化动画的 ViewHolder。源码成员:mCachedViews(ArrayList 结构)
当 ViewHolder 真正滑出屏幕时,会优先进入 CachedViews。这层缓存的设计初衷是为了应对 "用户刚滑出去又立刻滑回来" 的场景(即"后悔药"机制)。
源码核心配置:
// 默认屏幕外缓存大小
static final int DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
源码成员:mViewCacheExtension
这是 Google 留给开发者的自定义缓存接口。在实际商业项目中极少使用,因为前两级和第四级缓存已经足够完善。若要使用,需要手动调用 recyclerView.setViewCacheExtension()并自行管理 View 的存取逻辑。
源码成员:mRecyclerPool(内部使用 SparseArray<ScrapData>按 ViewType 分组存储)
当 ViewHolder 从 CachedViews 被挤出,或者 Adapter 数据发生大规模变动(如 notifyDataSetChanged)导致旧 ViewHolder 失效时,它们会被统一扔进 RecycledViewPool。
源码核心结构:
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
}
resetInternal()清空所有状态(变成"脏数据"),取出后必定触发 onBindViewHolder。结合前半部分 Activity 中的扩容代码,正是因为 Pool 默认每种 ViewType 只存 5 个,在复杂列表(如包含大量 Video、Image)快速滑动时极易造成频繁重建,因此必须手动扩容:
// 扩大 Pool 容量,防止复杂 ViewHolder 被频繁销毁重建
recyclerView.recycledViewPool.setMaxRecycledViews(MyAdapter.VIEW_TYPE_VIDEO, 30)