type
status
date
slug
summary
tags
category
password
icon
notion image
💡
ListView 有二级缓存,RecyclerView 有四级缓存 ListView 的复用机制( RecycleBin ):
notion image
ListView 通过 RecycleBin 实现复用,mActiveViews 是一个 View数组,负责存储当前显示的 itemView,当页面数据未改变,但是需要重新显示时(当调用 requestLayout 这种被动刷新时),会直接从这里获取 itemView 复用。mScrapViews 是第二级缓存,是一个 ArrayList<View> 的数组(相当于二维数组),界面已经加载过的,但是已经不显示的 itemView 都将保存到这里,当需要复用时,从这里获取,如果上面两个缓存中都没有,就创建新的 itemView。
之所以 mScrapViews 是二级数组,是因为列表中 itemView 可能有不同的ViewType,需要根据不同的布局类型,做保存。

RecyclerView 组件职责

notion image
  • RecyclerView 整体的管理者,以及本身作为 ViewGroup 的载体,最终负责展示各组件最终产⽣的 View 对象
  • LayoutManager 负责 View 的测量、布局、触摸反馈,以及何时能够回收的策略
  • ViewHolder 辅助“循环系统”中,可复⽤的 View 对象,例如减少 findViewById 的性能消耗。
  • Recycler 抽象概念中如其名,回收管理者,负责为 LayoutManager 组件提供 ViewHolder 对象,在其内部会根据策略不同选择恰当状态的 ViewHolder 对象
  • Adapter 将数据转化为 View 对象的转化⼯具,兼顾对 View 对象进⾏更新操作
  • ItemAnimator Item 改变时的动画帮助⼯具
  • ItemDecoration Item 增强⼯具,可以⾮常⽅便的⽤来绘制分割线,或者做⾼亮功能甚⾄⽤于做滚动条等

RecyclerView 简单运⾏机制

如下图所示:
  1. LayoutManager 通过 getViewForPosition() 向 Recycler 索要 View
  1. 当 Recycler 内部为空时,那么它会通过 Adapter 调⽤ onCreateViewHolder() 创建出⼀个全新的携带 View 的 ViewHolder
  1. 接着 Adapter 会调⽤ onBindViewHolder()ViewHolder 绑定数据
  1. 然后 Recycler 就可以将这个已经绑定好数据的 ViewHolder 的 View 交给 LayoutManager

Recycler 中「多层」缓存

notion image
  • CachedViews ⽤于缓存对应屏幕滑动时被「移⼊/移出」的 ViewHolder,默认⼤⼩为 2,可以通过 setItemViewCacheSize() 来修改⼤⼩,从这⼀层获取的 ViewHolder 是正确的 position ,命中缓存则⽆需绑定数据可以直接使⽤
  • RecycledViewPool 根据 ViewType 对 ViewHolder 进⾏缓存,每个 ViewType 默认⼤⼩为 5,可以通过 setMaxRecycledViews() 来修改⼤⼩,可以让多个 RecyclerView 共享同⼀个 RecycledViewPool,从这⼀层获取的 ViewHolder,position 不保证是正确的,命中缓存应该需要重新绑定数据
    • 💡
      RecycledViewPool 可以看成一个 SparseArray<ArrayList<ViewHolder>> SparseArray 是一个 key 只能为 Int 类型的 HashMap,这里使用是因为需要使用 R.layout.item 的 Id 作为 key,也就是 ViewType,之所以不用数组,是因为 Id 的值不是连续的。
  • ViewCacheExtension 可以⾃定义的缓存层,暂时没有⾮常有意义的实现
  • AttachedScrap & ChangedScrap pre/post-layout 提供⽀持的暂存层,没有⼤⼩限制
    • AttachedScrap
      • notion image
    • ChangedScrap
      • notion image
        💡
        当某个 ItemView A[3] 调用 notifyItemChanged() 更新数据时,默认采用的是这个 ItemView 和新的 ItemView 完全替换:在 prelayout 时,会从 mAttachedScrap 和 mChangedScrap 中获取到暂存的A[3],并从 mCachedViews 或者 mRecyclerPool 中复用或通过createViewHolder 生成一个新的 A[3],新的 A[3] 执行入场动画,当前的 A[3] 执行出场动画 可以通过androidx.recyclerview.widget.SimpleItemAnimator#setSupportsChangeAnimations(boolean) 方法开启/关闭简单动画,来防止上面更新 itemView 时闪烁的问题。 也可以通过androidx.recyclerview.widget.RecyclerView.Adapter#notifyItemChanged(int, java.lang.Object) 方法直接把新数据封装的新的 ViewHodler 保存到 mAttachedScrap 中,这样就不会走动画的逻辑,直接从暂存中获取,就实现了局部刷新

      核心机制 pre/post-layout

      notion image
      列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。
      这是怎么做到的?RecyclerView 如何知道表项 3 的动画轨迹?虽然动画的终点已经有了(表项 2 的顶部),那起点呢?LayoutManager 只加载所有可见表项,在删除表项 2 之前,表项 3 处于不可见状态,它并不会被 layout。
      对于这种情况 RecyclerView 的策略是 “执行两次 layout”:为动画前的表项先执行一次 pre-layout,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次 post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。
      notion image
      💡
      列表中 item 的改变,会执行旧的 item 执行离场动画,新的 item 执行入场动画。

notifyDataSetChanged()

notifyDataSetChanged() 的问题:
直接调⽤ notifyDataSetChanged() 后会把所有 ViewHolder 都放⼊ RecycledViewPool 中,RecycledViewPool 默认情况下每个 ViewType 缓存数量为 5,所以超出部分会被丢弃,这时候再次展示需要调⽤ Adapter#onCreateViewHolder 创建新的 ViewHolder,造成了不必要的开销
解决方法:
通过 Adapter#setHasStableIds(true),然后正确重写对应的 getItemId(),可以 ViewHolder 进⼊到 Scrap 层⽽不是 RecycledViewPool,这样就可以使得 Recycler 可以通过 itemId 来对 ViewHolder 进⾏合适缓存复⽤,避免了额外的开销。
其他改变数据的方式
  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)
BlockCanary 原理解析LeakCanary 原理解析
Loading...
shuouyang
shuouyang
android开发 ReactNative开发 小程序开发
最新发布
AOSP 环境搭建
2025-3-29
View 绘制流程-源码解析
2025-3-12
HTTP
2025-3-4
JVM 虚拟机
2025-2-28
蓝牙-BLE-基础
2025-2-28
从 OkHttp 的原理来看 HTTP
2025-2-19
公告
🎉热点信息🎉
--- 1 ---
Jet Brains 推出新的跨平台支持 Kotlin MultiPlatform
--- 2 ---
新的小巧便捷的依赖注入框架 Koin
--- 3 ---
新一代 API 查询语言 GraphQL