在 RecyclerView 滑动过程中,如何避免因频繁创建 ViewHolder 导致的卡顿?
解读
国内主流 App(电商、短视频、社交)的 RecyclerView 往往要承载 10 万级以上动态卡片,且卡片布局嵌套深、动效多。面试官真正关心的是:
- 你是否理解“创建 ViewHolder”这条路径在 16 ms 帧内的耗时分布;
- 能否把“减少 createViewHolder 调用次数”与“减少每次调用的耗时”同时落地;
- 是否具备线上验证与灰度回滚的闭环经验。
一句话:不是简单回答“用复用”,而是给出“可灰度、可监控、可回滚”的国内大厂级方案。
知识点
- RecyclerPool 与缓存层级:mAttachedScrap → mCachedViews → ViewCacheExtension → RecycledViewPool
- 预加载:GapWorker + Prefetch 默认开启,但国内 4.x 机型被阉割,需手动兼容
- 异步 inflate:Support Library 26+ 提供 LayoutInflaterCompat.setFactory2 + AsyncLayoutInflater,可让 IO/反射在子线程完成
- 布局层级扁平化:ConstraintLayout 2.x 的 Flow、Layer,Merge+ViewStub 替代传统 Linear 嵌套
- 线程与消息:主线程 Binder 阻塞、GC 抖动、IO 阻塞都会放大创建耗时;需结合 systrace 的 “binder transaction” 与 “GC” 标签
- 线上监控:字节内部 RMonitor、腾讯 Matrix、支付宝 Codewatcher 均把 “createViewHolder 平均耗时 / 次数” 作为一级指标,阈值 3 ms、单屏 5 次
- 国内 ROM 差异:华为、OPPO 对 RenderThread 调度策略不同,需灰度开关控制预加载线程数
- 业务层动态模板:阿里 Tangram、美团 DinamicX 通过二进制模板下发布局,避免反射 inflate,但带来额外内存,需要 LRU 淘汰
- 低端机降级:当 SDK_INT < 24 或内存 < 2 GB 时,关闭动效、减少预加载数量,用 JSON 配置下发
- 回退策略:若线上灰度发现 “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%。
拓展思考
- Jetpack Compose 的 LazyColumn 已没有 ViewHolder 概念,但仍有 “Item 创建耗时”。如何沿用上述池化思想,在 Compose 运行时层做 “Item 模板缓存”?
- 当 RecyclerView 嵌套在 ViewPager2 时,Fragment 生命周期与 RecyclerPool 生命周期不一致,导致池被提前清空,如何定制生命周期对齐策略?
- 在折叠屏展开/折叠的 Configuration Change 瞬间,系统会重建 Activity,RecycledViewPool 被重置,如何借助 ViewModel+SavedState 让池跨配置恢复,实现“零卡顿”切换?