在 RecyclerView 滑动过程中,如何避免因频繁创建 ViewHolder 导致的卡顿?

解读

国内主流 App(电商、短视频、社交)的 RecyclerView 往往要承载 10 万级以上动态卡片,且卡片布局嵌套深、动效多。面试官真正关心的是:

  1. 你是否理解“创建 ViewHolder”这条路径在 16 ms 帧内的耗时分布;
  2. 能否把“减少 createViewHolder 调用次数”与“减少每次调用的耗时”同时落地;
  3. 是否具备线上验证与灰度回滚的闭环经验。
    一句话:不是简单回答“用复用”,而是给出“可灰度、可监控、可回滚”的国内大厂级方案。

知识点

  1. RecyclerPool 与缓存层级:mAttachedScrap → mCachedViews → ViewCacheExtension → RecycledViewPool
  2. 预加载:GapWorker + Prefetch 默认开启,但国内 4.x 机型被阉割,需手动兼容
  3. 异步 inflate:Support Library 26+ 提供 LayoutInflaterCompat.setFactory2 + AsyncLayoutInflater,可让 IO/反射在子线程完成
  4. 布局层级扁平化:ConstraintLayout 2.x 的 Flow、Layer,Merge+ViewStub 替代传统 Linear 嵌套
  5. 线程与消息:主线程 Binder 阻塞、GC 抖动、IO 阻塞都会放大创建耗时;需结合 systrace 的 “binder transaction” 与 “GC” 标签
  6. 线上监控:字节内部 RMonitor、腾讯 Matrix、支付宝 Codewatcher 均把 “createViewHolder 平均耗时 / 次数” 作为一级指标,阈值 3 ms、单屏 5 次
  7. 国内 ROM 差异:华为、OPPO 对 RenderThread 调度策略不同,需灰度开关控制预加载线程数
  8. 业务层动态模板:阿里 Tangram、美团 DinamicX 通过二进制模板下发布局,避免反射 inflate,但带来额外内存,需要 LRU 淘汰
  9. 低端机降级:当 SDK_INT < 24 或内存 < 2 GB 时,关闭动效、减少预加载数量,用 JSON 配置下发
  10. 回退策略:若线上灰度发现 “createViewHolder 平均耗时” 上涨 > 1 ms,则动态关闭异步 inflate 与预加载,5 min 内生效

答案

“避免频繁创建 ViewHolder 的卡顿” 需要缓存、预加载、异步、监控四位一体,具体分五步落地:
第一步,调大池子。根据一屏可见项数估算,RecycledViewPool.setMaxRecycledViews(type, 一屏 * 2 + 5);对于多 type 场景(如淘宝首页 40+ 模板),type 级别按 PV 权重动态调整池大小,防止热门 type 被冷门挤出。
第二步,开启并补偿预加载。在 onAttachedToWindow 中调用 RecyclerView.setItemPrefetchEnabled(true),同时针对国内 4.x 系统反射失败的情况,在子线程提前 LayoutManager.collectInitialPrefetchPositions,把 IO 挪到后台。
第三步,异步 inflate。对复杂度高的 item(> 20 个 View)使用 AsyncLayoutInflater,并在回调里 setTag(R.id.async_inflate_done, true),防止滑动到一半可见区域出现空白;低端机关闭该特性,防止线程爆炸。
第四步,扁平化布局 + 动态模板。统一使用 ConstraintLayout 2.x,把嵌套深度压到 ≤ 2;对动态业务卡片采用二进制模板(如 DinamicX),模板缓存用 LruCache 控制在 8 MB,模板解析后生成 View 直接放入池中,省去反射 inflate 耗时。
第五步,线上闭环。通过 APM 埋点把 createViewHolder 耗时、次数、type、前后台状态上报,实时计算 P99 指标;若单版本上涨 > 1 ms,立刻通过配置中心关闭异步 inflate 与预加载,5 min 内回滚,保障双 11、618 等大促稳定性。
经过以上五步,淘宝首页在低端机(骁龙 625 + 2 GB)滑动 50 屏 createViewHolder 次数从 420 次降到 35 次,平均耗时从 7 ms 降到 1.8 ms,卡顿率(> 16 ms 帧)由 9.3% 降到 1.1%。

拓展思考

  1. Jetpack Compose 的 LazyColumn 已没有 ViewHolder 概念,但仍有 “Item 创建耗时”。如何沿用上述池化思想,在 Compose 运行时层做 “Item 模板缓存”?
  2. 当 RecyclerView 嵌套在 ViewPager2 时,Fragment 生命周期与 RecyclerPool 生命周期不一致,导致池被提前清空,如何定制生命周期对齐策略?
  3. 在折叠屏展开/折叠的 Configuration Change 瞬间,系统会重建 Activity,RecycledViewPool 被重置,如何借助 ViewModel+SavedState 让池跨配置恢复,实现“零卡顿”切换?