尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
RecyclerView复用与回收机制全解析:从四级缓存到性能优化实战
如果你在现在的 Android 项目里做列表场景不管是资讯流、商品列表、聊天记录还是后台管理面板里的单据流水最常用的组件基本就是 RecyclerView。即使你只是刚入行没多久也一定听过它的核心卖点复用与回收。但说句实在话我面试过不少候选人简历上写着熟悉 RecyclerView真正能把复用池掰开揉碎讲清楚的却没几个——缓存分几层、每层命中条件是什么、position 变了缓存怎么走、为什么 onBindViewHolder 不能被滥用这些才是决定列表性能的关键路径。这篇文章我就把 RecyclerView 复用与回收机制从源码到实战完整拆一遍顺便把跟它强相关的两个高频问题——条目曝光埋点不准、嵌套点击事件无反应——一起做掉。这篇文章适合三类人一是被线上列表卡顿、掉帧折磨的性能优化者二是想从会用进阶到懂原理的中级开发三是做埋点和复杂交互时反复踩坑的业务开发。我会先从机制设计讲清楚为什么 RecyclerView 要这么设计再逐步拆解缓存层级、滑动链路、预取机制最后落到真实项目里的曝光埋点和嵌套点击问题。读完你至少能获得一套可以直接落地的列表优化思路而不是只会背几个 API 名字。1. 复用与回收机制的核心设计思路1.1 滑动列表的代价View 不是免费的先从一个最基础的问题问起为什么滑动列表一定要做复用答案其实很简单——创建 View 的成本非常高。一次LayoutInflater.inflate()背后涉及 XML 解析、构造函数调用、属性采集、measure、layout、draw 等一系列操作。手机上常见的一条列表 item复杂一点的甚至要 inflate 四五层嵌套布局单次创建轻松消耗几百微秒到几毫秒。如果不做任何复用每滑动一屏就创建七八个新 View用户在快速滑动时每帧都在大量 new 对象必然导致 GC 频繁、UI 卡顿、掉帧。而实际上屏幕一屏能显示的 item 数量非常有限通讯录一屏大概显示 8 到 10 条商品列表一屏也就 5 到 6 个卡片。用户滚动浏览的时候某个 item 滚出屏幕后它占用的内存空间和视图资源完全可以腾出来给新滚入的 item 使用这就是回收而新 item 不需要每次从 XML 重建直接拿旧 View 改改数据就能上屏这就是复用。所以 RecyclerView 的整个设计基调就是用有限的 View 对象支撑无限的数据集滚动。它把这条链路拆成了非常清晰的两个环节——创建 View 和绑定数据。对应到代码上就是onCreateViewHolder()和onBindViewHolder()。搞清了这一点再看后面的缓存层级就容易多了。1.2 从 ListView 到 RecyclerViewViewHolder 为什么是强制性的早期 ListView 也做复用它通过getView()方法里的convertView参数把滚出屏幕的 View 传回来开发者自己判断convertView null再决定是否 inflate。这种设计很灵活但也带来了一个实际问题很多人在 getView 里不判断或者判断错了导致列表卡顿或者 item 内容错乱而且 convertView 只是复用 View内部的子 View 查找每次都要重新 findViewById性能损耗不小。RecyclerView 把这件事结构化了它强制要求你使用 ViewHolder。ViewHolder 本质上是一个View 的壳里面持有 item 根布局和所有需要操作的子 View 引用。RecyclerView 复用时复用的不是裸 View而是整个 ViewHolder——这就把 findViewById 的耗时从每次绑定数据时都执行变成了只在创建 ViewHolder 时执行一次。这个改变看着不大但对列表滚动的性能提升是非常明显的。我自己的体会是把 RecyclerView 理解成一个工厂 仓库的产销体系特别形象onCreateViewHolder()是工厂生产新货架成本最高尽量避免onBindViewHolder()是给货架贴上新商品的标签也就是把数据填进 View成本相对低但也需要控制耗时缓存池就是仓库滚出去的 item 不是销毁而是先存进仓库等下次同类 item 滚进来时直接取出来。这套模型在 RecyclerView 内部就是 Recycler 类来承载的。后面要讲的四级缓存其实就是给这个仓库分出了不同货架区。2. 四级缓存每一层都装着什么RecyclerView 的复用与回收之所以比 ListView 高效核心在于它内部有一套多级缓存机制。这四级缓存分布在 Recycler 类里分别叫mAttachedScrap、mCachedViews、mViewCacheExtension和mRecyclerPool。下面逐个拆。2.1 mAttachedScrap正在眼前的备用区mAttachedScrap是 RecyclerView 自己维护的一个 ArrayList它里面存的 ViewHolder 仍然关联着 itemView但已经从 RecyclerView 的视图树中临时 detach 掉了。这听起来有点绕我解释一下应用场景。当调用notifyItemChanged()、notifyItemInserted()、notifyItemMoved()这类局部刷新方法时RecyclerView 需要为被刷新的 item 执行动画。比如你notifyItemChanged(3)第三项要淡入淡出RecyclerView 就需要先把 position 3 对应的旧 ViewHolder 从布局里摘下来等动画结束再决定是重新 bind 还是彻底回收。这个摘下来待命的容器就是 mAttachedScrap。它的命中条件非常苛刻必须 position 完全匹配必须 viewType 完全匹配命中之后不需要重新调用onBindViewHolder()直接可用。因为这时候 ViewHolder 里的数据还是对的只是暂时被摘掉了。你可以把 mAttachedScrap 理解成手术台上的临时搁置区position 和类型对得上直接放回去就行。2.2 mCachedViews默认只存 2 个的快速通道mCachedViews 是滑出屏幕时最先进入的缓存区。它的特点是命中后不需要重新 bind因为 ViewHolder 的数据仍然有效。默认容量是 2可以用setItemViewCacheSize()调整。为什么默认只有 2因为 RecyclerView 认为用户手指从某个位置往回滑时很大概率会滑到刚刚离开的那几个 item。缓存一两个可以直接上屏的 ViewHolder回滑时就能做到零 bind 直接显示视觉上非常顺滑。但要注意mCachedViews 的命中同样要求 position 和 viewType 都匹配。如果 position 变了即使 viewType 相同也无法命中这个缓存层级需要继续往下走。我在实际项目里一般不推荐把 mCachedViews 调得特别大。默认 2 就够了顶多调到 4 到 5。因为它的直接代价是内存而且一旦缓存项不是用户回滑想要的位置命中率其实很低不如把精力放在优化 bind 逻辑上。2.3 mViewCacheExtension留给开发者的自定义仓库mViewCacheExtension 是一个抽象类RecyclerView 默认不提供任何实现它是专门留给开发者做自定义缓存扩展的。你可以在这里实现自己的缓存策略比如把某些高频 item 单独缓存或者针对特定 viewType 走特殊逻辑。不过坦白讲日常业务开发里用到 mViewCacheExtension 的场景非常少。它更像是一个官方预留的接口。只有当你对 RecyclerView 的缓存策略非常熟悉并且明确知道默认的 LRU 风格缓存满足不了你的场景时才建议来这里做文章。多数情况下直接操作 mRecyclerPool 就已经够用了。2.4 mRecyclerPool按类型分堆的公共池如果 mCachedViews 装满了或者 position 匹配不上ViewHolder 就会进入 mRecyclerPool。mRecyclerPool 内部是一个 SparseArray以 viewType 为 key每个 viewType 对应一个 ArrayList 作为缓存堆栈。默认每个 viewType 最多缓存 5 个 ViewHolder。与 mCachedViews 最大的区别是从 mRecyclerPool 取出的 ViewHolder 必须重新调用 onBindViewHolder() 绑定数据。因为 pool 里只保证 viewType 一致不保证 position 和数据一致。你可以用RecyclerView.getRecycledViewPool().setMaxRecycledViews(viewType, maxCount)来调整某个 viewType 的缓存数量。这里有一个很实用的调优点如果你的列表里有大量同 viewType 的 item 同时滑出屏幕默认 5 个很容易装不下导致 ViewHolder 被真正销毁稍后滚回来又需要重新 onCreateViewHolder。这种情况下适当调大 pool 上限是有效的。但注意 pool 是 RecyclerView 级别的对象多个 RecyclerView 之间可以共用一个 mRecyclerPool如果你的页面里有多个列表且 item 结构一致共享 pool 可以减少重复创建——这也是很多人容易忽略的性能优化点。2.5 四级缓存的命中与耗时对比把这四级缓存整理成一张表看起来会更直观缓存层级命中条件命中后是否需要重新 bind默认容量典型场景mAttachedScrapposition viewType 匹配否屏幕内 item 数量局部刷新、动画移除mCachedViewsposition viewType 匹配否2回滑已滑出的 itemmViewCacheExtension自定义自定义无默认实现自定义缓存策略mRecyclerPool仅 viewType 匹配是每类型 5 个通用回收池、多列表共享从性能角度看从上到下命中成本逐渐升高。mAttachedScrap 和 mCachedViews 命中时连 bind 都不用做速度最快mRecyclerPool 命中时一定要重新 bind成本次之最差的情况是缓存全未命中只能走onCreateViewHolder()重新 inflate这是最需要避免的路径。理解了这张表你就知道为什么所有 RecyclerView 优化文章都在反复强调减少 onCreateViewHolder 次数、让 bind 尽量轻量了——因为真正的缓存红利就是让你每次滑动都尽量停留在前两层。3. 从滑动到复用的完整链路与调优抓手3.1 滑动时 ViewHolder 的回收与取出流程看懂了缓存分层还要搞懂一次滑动过程中它们是怎么协同工作的。RecyclerView 的核心方法是tryGetViewHolderForPositionByDeadline()这个方法就是工厂仓库调度中心。当某个 item 滚出屏幕RecyclerView 会调用recycleViewHolderInternal()它的处理顺序是先判断这个 ViewHolder 是否有不稳定的标识比如正在执行动画符合条件就放进 mAttachedScrap否则看 mCachedViews 有没有空位有空位就放进去mCachedViews 满了就把最早的那个挤出去挤出来的那个再放进 mRecyclerPool如果 mRecyclerPool 也满了ViewHolder 才会真正被丢弃等待 GC。而当新 item 即将滚入屏幕时tryGetViewHolderForPositionByDeadline()会按这个顺序取先在 mAttachedScrap 里找position 和 viewType 都匹配就直接用没找到再去 mCachedViews 里找匹配就直接用mViewCacheExtension 有自定义实现的话会在这里介入最后去 mRecyclerPool 里按 viewType 取取到后必须走onBindViewHolder()全都拿不到只能onCreateViewHolder()新建。这里有个值得注意的细节——**RecyclerView 会尝试从缓存取出 ViewHolder 后再调用一次onBindViewHolder()吗**答案是看情况。如果这个 ViewHolder 是从 mAttachedScrap 或 mCachedViews 取出来的并且数据没有被标记为无效RecyclerView 不会 bind但如果是从 pool 取出来的或者调用了notifyDataSetChanged()导致全部数据失效就必须重新 bind。这也是为什么很多人发现notifyDataSetChanged()之后列表所有 item 都闪烁一下——因为它们全部进入了 rebind 流程。3.2 Prefetch 预取机制让滑动不再掉帧RecyclerView 在 Android Support Library 25 之后引入了一个重要的性能优化Prefetch也就是预取机制。前面讲到的取用流程都是在 item 真正需要上屏时才触发的这意味着如果恰好遇到缓存未命中RecyclerView 就得在 UI 线程上现场 inflate一帧内既要完成布局又要完成 inflate很容易掉帧。Prefetch 的思路是在下一帧 VSYNC 到来之前提前把即将显示的 item 创建并绑定好等它们真正上屏时直接从缓存取。这个机制由GapWorker实现它在 Choreographer 的回调里工作。RecyclerView 在布局时会记录每个 item 的位置GapWorker 预判下一个滑动位置需要的 item提前执行createViewHolder和bindViewHolder。对我们项目开发的实际影响是默认情况下 RecyclerView 已经自动开启了 prefetch绝大多数场景不需要手动干预。但有一个方法值得记住——setInitialPrefetchItemCount()。它用于 LinearLayoutManager 等布局管理器作用是设置初始预取数量适用于一次加载多个 item 的场景。比如横向的 ViewPager 样式列表你可以把初始预取数量设置成 1 或 2让第一帧展示更完整。3.3 真正的性能瓶颈不是复用不够而是 bind 太重我做了这么多年列表优化一个深切的体会是绝大多数列表卡顿问题都不是缓存层级不够而是 bind 阶段埋雷。比如在onBindViewHolder()里做这些事都会直接拖垮滑动性能做 JSON 解析或数据转换尤其是大 JSON加载图片但不走内存缓存直接 I/O 读盘频繁创建新对象比如每次都new一个临时 Listener、Formatter在 bind 里做复杂的布局计算比如动态设置高度导致重排设置过多的 View 属性导致 requestLayout 被大量触发。真正的调优思路应该是先量化再针对性优化。工具上优先用 Layout Inspector 看布局层级用 CPU Profiler 或 systrace 看 bind 方法耗时。如果发现某个 item 的 bind 耗时超过 3 到 5 毫秒就算缓存命中率再高快速滑动时也会掉帧。我通常建议从几个方向优化 bind 成本把bind里的耗时操作放到异步线程用回调更新UI用 DiffUtil 代替notifyDataSetChanged()减少无谓的 rebind给 item 固定宽高或设置setHasFixedSize(true)避免每次测量都触发完整 layout如果 item 布局非常复杂考虑用 ConstraintLayout 减少层级或者拆成多个轻量 item 用 ConcatAdapter 组合。只要 bind 足够轻RecyclerView 的复用机制就能帮你把滑动体验做到非常流畅。4. 复用机制下的曝光埋点拦截、去重与防漏4.1 为什么 position 不能直接用来做曝光判断RecyclerView 的复用机制带来一个很隐蔽的问题同一个 ViewHolder 在不同的时间点承载着不同的数据。如果你习惯在onBindViewHolder()里直接对 itemView 做点击埋点、展示埋点很容易出现两类错误。第一类是重复曝光。因为 ViewHolder 被复用item 滚出屏幕再滚回来时同一个 ViewHolder 被再次 bind你如果每次 bind 都上报曝光用户来回滑动几下一条数据就被上报了五六次曝光。第二类是错位曝光。由于 ViewHolder 复用时不会立刻从屏幕上移除旧数据而是在新数据 bind 之前itemView 上显示的仍然是上一条数据的内容。如果在 bind 之前、或者 bind 过程中去读 position 和 View 的可见性拿到的可能是旧数据。所以做曝光埋点的第一步就是先明确曝光埋点不能依赖 onBindViewHolder 的调用时机必须基于View 是否真的出现在屏幕上来判断。4.2 一套可落地的曝光采集方案我在项目里用的曝光埋点方案核心思路是监听 RecyclerView 的滚动状态在滚动停止或滚动过程中动态计算当前可见的 item 列表再结合业务去重逻辑上报。具体分四步第一步给 RecyclerView 添加addOnScrollListener()这个监听器里有onScrolled()和onScrollStateChanged()两个回调。曝光上报我一般放在onScrollStateChanged里监听SCROLL_STATE_IDLE因为滚动停止时计算可见区域最准确上报次数也少。如果业务要求实时曝光再在onScrolled()里节流处理。第二步计算当前屏幕可见的 item 范围。用linearLayoutManager.findFirstVisibleItemPosition()和findLastVisibleItemPosition()拿到首尾可见 position再通过findViewByPosition()拿到具体 itemView。注意它们和findFirstCompletelyVisibleItemPosition()的区别——前者是部分可见就算后者是必须完整可见才算。曝光埋点用哪个取决于产品需求。如果要求严格就用 Completely 版本如果只要露出一个像素就算曝光那就用普通版本。第三步做可见性判定。很多时候一个 item 只在屏幕边缘露出 10 个像素这种曝光要不要上报产品通常要求按可见面积占比来决定。我的做法是在拿到 itemView 之后调用itemView.getGlobalVisibleRect(rect)获取它在屏幕上的可见矩形再和 itemView 自身的宽高做比例计算。下面这段代码是通用判定逻辑fun isItemVisible(itemView: View, threshold: Float 0.5f): Boolean { val rect Rect() val visible itemView.getGlobalVisibleRect(rect) if (!visible) return false val area itemView.width * itemView.height val visibleArea rect.width() * rect.height() return area 0 (visibleArea.toFloat() / area.toFloat()) threshold }threshold 取 0.5 表示至少有一半区域可见才算曝光。不同业务可以调这个值比如广告埋点一般要求更高。第四步上报前做去重。去重的 key 我推荐用业务ID 会话ID。用一个MutableSetString保存已曝光的 key每次准备上报前先判断 set 里有没有有就直接跳过。这里要注意 set 的生命周期管理如果是列表页的普通曝光进入页面时清空离开页面时清空如果是跨页面的全局曝光需要用持久化存储或内存单例。4.3 复用列表里我踩过的三个埋点坑曝光埋点做得多了有些坑是网上教程不会提醒你的我在这里集中说下。第一个坑不要在 onCreateViewHolder 里做监听注册时暴露出数据变化。ViewHolder 创建时 itemView 上还没有绑定当前数据如果你在此时把业务 ID 塞进某个监听器等复用发生时监听的还是旧数据。正确做法是在 onBindViewHolder 里统一设置 tag 或更新监听器持有引用。第二个坑setOnScrollListener 不要和 setOnScrollStateChanged 混淆。RecyclerView 的 addOnScrollListener 是可以叠加多个的它不会覆盖。如果页面里有多个模块都加了监听器务必在页面销毁或 RecyclerView 解绑时clearOnScrollListeners()否则会引发内存泄漏或重复上报。第三个坑加载更多后已曝光判断失效。因为 set 里的 key 不会因为列表插入新数据而清除所以加载更多触发的 item 不会出现重复曝光这是对的。但如果你在 set 里用的 key 只是 position那加载更多后前面 item 的 position 没变新插入的 item 和被挤下去的 item 就会产生错乱。所以用 key 时一定要用数据自带的业务 ID而不是 position。5. 嵌套点击事件失效当复用遇上触摸分发5.1 现象内层 RecyclerView 的 item 点击时灵时不灵现在很多页面都是列表嵌套外层滑动内层横向滑动或网格展示。典型场景是首页的推荐模块外层 RecyclerView 垂直滑动里面嵌了一个横向滑动的 RecyclerView或者一个两列的网格。这种布局下我经常收到测试提的 bug内层 item 点击有时候没反应。展开看这个问题的本质其实和复用机制关系不大它本质上是触摸事件分发与滑动冲突。但因为它出现在 RecyclerView 嵌套场景里而且表现得很随机所以经常被归类到RecyclerView 的诡异问题里。5.2 从事件分发链路看根因Android 触摸事件分发的核心链路是三条方法dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()。事件序列从 ACTION_DOWN 开始到 ACTION_UP 结束。在这条链路里父 View 的 onInterceptTouchEvent 一旦在某个事件上返回 true后续整个事件序列都会交给父 View 处理子 View 就收不到事件了只会收到一个 ACTION_CANCEL。嵌套 RecyclerView 时外层 RecyclerView 的 onInterceptTouchEvent 会判断当前触摸的移动方向。如果用户触摸位置落在内层横向列表上但手指稍微有一点点纵向位移外层可能判断为用户要纵向滑动于是把事件拦截走内层 item 的点击就丢了。如果点子正好用户手指没有触发 touchSlop 判断点击能正常触发一旦手指有轻微移动点击就消失——这就是时灵时不灵的来源。还有一个容易被忽略的细节外层 RecyclerView 在拦截事件时会调用requestDisallowInterceptTouchEvent的逻辑内层如果设置了clickabletrue且消费了 ACTION_DOWN外层通常不会拦截但如果没有正确消费 DOWN外层就有机会接管。5.3 三种实测有效的解决路径针对不同场景我给出三个修复方向按推荐程度排序。第一种如果是内层横向列表 外层垂直列表这种经典嵌套直接给内层横向 RecyclerView 的 item 根布局设置android:clickabletrue并且在内层 RecyclerView 的 onTouchEvent 中返回 true 消费触摸事件让内层自己完成滑动和点击的判定。这样可以避免外层在 DOWN 阶段就抢走事件。第二种使用requestDisallowInterceptTouchEvent(true)强制内层持有事件控制权。在内层 RecyclerView 的 onTouchEvent 里判断当前滚动方向是横向时调用父 View 的requestDisallowInterceptTouchEvent(true)告诉外层你别拦。代码如下innerRecyclerView.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener { override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { when (e.action) { MotionEvent.ACTION_DOWN - rv.parent.requestDisallowInterceptTouchEvent(true) MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL - rv.parent.requestDisallowInterceptTouchEvent(false) } return false } override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {} override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} })注意 ACTION_DOWN 时禁止外层拦截ACTION_UP 或 CANCEL 时恢复这样不影响外层列表的正常滑动。第三种如果外层列表的滚动方向和内层重叠比如都是垂直列表那么这种嵌套本身就不合理建议从布局层面调整改用单个 RecyclerView 多 viewType 实现而不是嵌套两个可滚动容器。这种做法不仅能彻底解决事件冲突还能避免内层滚动和外层滚动互相抢占性能资源列表的滚动帧率也会有明显提升。这三种方案里我实际项目中最常用的是第一种和第三种。第一种改动小、见效快第三种治本但是需要重构数据源和 Adapter。5.4 嵌套点击问题速查表问题场景现象根因推荐解法外层垂直 内层水平内层 item 点击偶尔失败外层误判纵向滑动而拦截内层 item 设置 clickable消费 DOWN外层垂直 内层水平横向滑动被外层吃掉外层在横向滚动时也拦截requestDisallowInterceptTouchEvent 锁定外层垂直 内层垂直内层无法独立滚动/点击错乱两个可滚动容器方向冲突多 viewType 合并为单个列表内层列表无响应点击和滑动都失效父容器 onInterceptTouchEvent 全部拦截检查父容器触摸逻辑重写拦截条件6. 列表性能调优清单先量化再动手前面几章把机制和问题都过了一遍最后把这几年做列表优化的一些实操经验整理成一份清单方便你直接拿去排查问题。我给自己定了个规矩任何列表优化先量化再动手。先把onCreateViewHolder和onBindViewHolder的耗时打点打出来再把掉帧情况通过 systrace 或 Perfetto 拉出来看确认瓶颈在创建、绑定还是布局然后再对症下药。最怕的就是不看数据上来就调缓存大小最后问题没解决内存倒是涨了一截。如下是一些可以落地到项目里的调优点优先保证 onBindViewHolder 里无耗时操作图片加载走异步 内存缓存文案拼接提前处理数据转换放到后台线程。使用 DiffUtil 做精准刷新代替 notifyDataSetChanged避免所有 item 无意义 rebind注意 DiffUtil 在大列表下计算成本高可以用 AsyncListDiffer。合理使用 setHasFixedSize(true)如果 item 数量和内容不影响 RecyclerView 自身尺寸声明 fixed size 可以减少 requestLayout 的触发。谨慎调整缓存参数setItemViewCacheSize(5)、pool.setMaxRecycledViews(viewType, 10)要结合实测数据来调。多列表共享 pool 时注意不同页面 item 是否真的同构否则共享反而增加 bind 负担。列表项布局层级越浅越好能用 ConstraintLayout 或自定义 View 画的就不要嵌套五六层 LinearLayout。布局测量耗时占了 item 渲染的大头。避免在 onBindViewHolder 中 setLayoutParams频繁设置 LayoutParams 会触发 measure 和 layout性能开销非常大尽量在 item 布局里固定好尺寸。复用 Exposure 逻辑时注意生命周期监听器、去重集合都要在页面销毁时清理避免内存泄漏。参考一个我最近实际处理过的案例某个首页 Feed 流快速滑动时总是偶发掉帧。打点发现onBindViewHolder平均耗时 4.8ms其中图片的占位背景色设置占了 1.2ms一个时间格式化函数占了 0.8ms还有动态设置 item 高度导致反复 requestLayout 占了 2ms。后来把时间格式化移到数据层预计算图片占位色用 shape drawable 固定item 高度改为固定 dpbind 耗时降到 1.5ms滑动就明显顺了。整个过程没有动任何缓存参数——这再次印证了我前面那句话瓶颈往往不是复用得不够而是绑定数据的工作太重。最后分享一点个人习惯我在项目里统一封装了一个 BaseRecyclerView把 onScrollListener、曝光去重、通用空布局、加载更多都沉淀进去。这样业务侧不直接操作原始 RecyclerView埋点和性能监控也能在框架层统一收口。如果你维护的项目里有大量列表页面建议也往这个方向做一次公共组件抽象长期来看收益非常明显。我见过太多团队在列表卡顿的时候盲目调缓存结果越调越糟。RecyclerView 的复用机制强大但它的发力点是避免重复创建如果绑定和布局阶段成本压不下来缓存再深也救不了你。顺着这个思路去排查大部分列表性能问题都能找到明确答案。
RELATED

相关推荐

给 Codex 装上超能力:Superpowers Skill 实战指南

给 Codex 装上超能力:Superpowers Skill 实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/15 6:59:15
AI算力革命:驱动人工智能发展的核心动力

AI算力革命:驱动人工智能发展的核心动力

1. 算力为何成为AI发展的核心引擎2012年,多伦多大学的研究团队在ImageNet竞赛中首次使用GPU训练深度神经网络AlexNet,以压倒性优势夺冠。这个标志性事件揭示了一个关键事实:当算法理论突破遇到足够强大的计算能力,人工智能的发展速…

📅 2026/9/15 6:59:15
头歌实践教学平台:Java高级特性-多线程基础(3)线程同步(四上)

头歌实践教学平台:Java高级特性-多线程基础(3)线程同步(四上)

第4关:使用volatile实现变量的可见性任务描述 本关任务:使用volatile关键字与同步实现右侧程序输出10000。相关知识 在并发编程中,volatile关键字扮演着非常重要的作用,接下来我们直接进入主题。什么是 volatile 关键字 volatile是…

📅 2026/9/15 6:59:15
MORE NEWS

更多资讯

📰

Java 21 ZGC调优实战:把响应时间压到1ms以内

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

一文讲透数据治理8大核心模块:数据标准、质量、资产、目录、血缘……

很多企业做数据治理,最后都会陷入一种很奇怪的状态: 标准越来越多,数据却没有越来越统一; 平台越来越全,业务找数还是困难; 质量规则建了几百条,月底对账依然经常出问题。 原因往往不是企业…

📰

Word一键批量调整图片大小与对齐:表格、VBA宏实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

任务编号1.3背后:前端列表页开发的需求拆解与工程落地实践

1. 任务拆解与需求边界先对齐我看到“任务1.3”这个编号,第一反应是——这大概率是项目排期或看板里某个阶段任务包的子项。编号“1.3”通常是“第一阶段第三个任务”的意思,往往伴随着一条干巴巴的原始描述,比如“实现用户列表筛选与批量操作…

📰

凯基特行程开关选型与耐用性实战:避免产线停摆的关键细节

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

OCR-EDR:视觉引导的OCR纠错技术实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬