ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RecyclerView卡顿优化:从布局嵌套到DiffUtil的完整排查指南

RecyclerView卡顿优化:从布局嵌套到DiffUtil的完整排查指南 先聊一个高频得不能再高频的问题RecyclerView 卡顿、滚动不流畅几乎每个做过一段时间安卓开发的人都会撞上。明明列表数据也不多甚至单个 item 也就是几行文字加一张图可一滑起来就是掉帧跟吃了德芙的竞品一比体验瞬间就低了一个档次。我自己见过太多类似的排查场景有刚入门的新手把图片加载、布局嵌套全堆在onBindViewHolder里也有做了几年的老手在嵌套列表、动画开关这些细节上翻车。所以这篇内容不是给你背 API 文档而是把 RecyclerView 卡顿的常见原因、定位思路和能直接落地的优化方案捋一遍你手头正好有这类问题的话按这个顺序排查大概率能解决一大半。先说个总体判断RecyclerView 本身不背锅卡顿的本质是主线程负担过重。列表滑动时每一帧都要求在 16ms 内完成输入事件处理、动画更新、布局、绘制这一整套流程。任何一环超时系统就会跳过一帧视觉上就是卡顿。RecyclerView 只是把 item 的创建、绑定、布局这些动作集中暴露出来真正拖后腿的往往是你的写法、布局结构、数据加载方式以及和 ListView 时期遗留下来的坏习惯。搞清楚这一点后面的所有优化就都有了靶心。详细拆解放在下面从原理定位到布局、绑定、复用机制再到高频问题实录每一部分都是可以直接照着改的实操内容。1. 先把卡顿问题拆明白原理与定位1.1 卡顿的本质主线程调度不过来安卓的 UI 渲染依赖垂直同步信号每个周期就是屏幕刷新一帧的间隔常见的 60Hz 屏幕约 16.6ms120Hz 屏幕约 8.3ms。你的代码、系统框架的测量布局绘制、渲染线程的任务都得在这个时限内完成。如果某个任务超时Choreographer 就会跳过这一帧的绘制掉帧就发生了。连续掉帧、或者单帧耗时超过一两百毫秒用户体感就是明显的顿挫、迟滞。RecyclerView 场景里主线程上的主要开销集中在三块Item 布局的 measure 与 layout布局层级越深、嵌套越多遍历子 View 的计算量就越大。ViewHolder 的绑定bind如果你在onBindViewHolder里做数据解析、图片解码、格式化、甚至读写 SharedPreferences这些都会直接占掉宝贵的帧时间。列表数据更新的通知与重排notifyDataSetChanged()会把整个列表标记为数据变更导致所有可见 item 全部重新绑定不仅浪费还可能引发局部闪烁。理解了这三块定位方向就清晰了卡顿发生时到底是 hit 在布局、绑定还是刷新逻辑上。接下来就看工具怎么说而不是靠猜。提示千万不要一卡顿就去改 RecyclerView 的源码或者引入黑科技库绝大多数问题都在业务代码层级。先把耗时点找出来再动手。1.2 我用得最多的两个定位工具自己在实际排查中最常用的两个工具组合是系统自带的Profile GPU Rendering开发者选项里的“硬件加速渲染分析”和Systrace / Perfetto。Profile GPU Rendering 会把每一帧的绘制耗实用彩色柱状图展示出来直接叠加在屏幕上。打开方式开发者选项 → 硬件加速渲染 → 在屏幕上显示分析。柱子的颜色有讲究蓝色代表测量和布局时间红色代表绘制时间执行 View 的 draw 方法橙色代表渲染线程的合成时间滑动列表时盯住柱子的高度如果蓝色部分很高问题多半在布局层级红色部分高多半是绘制过程比如过度绘制或复杂图形柱状图整体基线偏高指向的是主线程上其他任务占用了太多时间。如果屏幕上还不能判定精确方法就用 Systrace 抓一段滑动期间的 trace。老工具systrace.py可能已经不大好用了现在推荐直接抓 Perfetto trace然后去 Perfetto UI 里分析。RecyclerView 的关键跟踪点包括RV CreateView、RV BindView、RV Layout这几个 slice 的耗时能直接反映 ViewHolder 创建、绑定、布局阶段的真实开销。它们正是在手机上很难肉眼区分、但优化起来收益最大的目标。Systrace 里有一个很隐蔽的点如果RV BindView耗时不长但整体帧耗时依然很高注意看主线程的Choreographer#doFrame前后有没有其他高耗时任务比如频繁的 GC、主线程 I/O、Binder 调用。单线程的主线程只要被占住卡顿就会呈现为“间歇性猛卡”而不是持续掉帧。2. 布局是头号嫌疑人列表项 UI 层优化2.1 减少布局嵌套从源头压掉 measure 耗时布局优化在 RecyclerView 场景里优先级非常高原因很直接列表 item 是反复复用的一张 item 布局每一次出现在屏幕上都要经历 measure 和 layout。层级越多遍历子 View 的时间就越长而且这个成本是每个 item 每次滑入屏幕都要付的。我见过一个典型例子item 外层是LinearLayout横向排列里面嵌了三个子布局其中一个内部又是四层嵌套最里面还有一个FrameLayout套TextView。这个列表滑起来简直像放 PPT一帧一帧地跳。用 Layout Inspector 或者直接看布局文件的层级树会发现视觉上只需要一个圆形头像、两行文本、右侧一个操作按钮结果堆了四层容器。优化的方向很直接能用ConstraintLayout做扁平化约束的就别再用LinearLayoutFrameLayout层层嵌套。ConstraintLayout 在一次测量中就能确定所有子 View 的相对位置和尺寸代价远低于多层嵌套的测量。关系相对固定的布局可以用merge标签直接合并到父容器减少一层 ViewGroup。不需要同时显示的视图用ViewStub延迟加载。注意 ViewStub 一旦 inflate 后就不再参与后续测量这一点对列表 item 很友好。实操心得改布局前后一定要复测 Profile GPU Rendering 的蓝色柱。我自己优化过一个详情型 itemmeasure 时间从 8ms 降到 2ms整条列表滑动时从“肉眼可见不顺”变成“勉强能接受”再配合其他优化最终才达到流畅。2.2 过度绘制与 ViewStub 的正确用法过度绘制指的是同一个像素点被多个 View 重叠绘制GPU 要反复填充同一片区域的颜色。列表 item 里很容易出现一个背景色上叠了一个白色圆角背景再叠一层按钮背景三层绘制就砸在同一个像素上。开发者选项里的“调试 GPU 过度绘制”打开后会在屏幕上显示颜色覆盖层无色没有过度绘制蓝色绘制 1 次绿色绘制 2 次粉色绘制 3 次红色绘制 4 次及以上列表 item 区域出现大片绿色粉红色就说明背景层次有问题。常见做法是删掉多余背景特别是 item 根布局的背景如果和整个页面背景一致完全可以直接去掉。使用clipToPadding和适量的padding代替子 View 的外边距背景。自定义绘制中如果只需要部分区域圆角可以用clipPath或者canvas.saveLayer控制范围但这个偏性能热点优先把普通层级的背景清理干净再说。ViewStub 的使用场景主要是那些“只在某条件下出现”的视图比如列表 item 里的“已售罄”角标、商品标签区。如果用 ViewStub 而不是普通View.GONE前者不会参与初始布局的测量和绘制可以减轻一部分首屏压力。但这里有一个务实的提醒如果你的 item 里超过三分之一的子视图都可能被隐藏与其用 ViewStub 一个个延迟加载不如在数据层直接拆分出不同的 itemType让完全不同的布局各走各的 ViewHolder 创建路径反而更清晰。2.3 一个实际案例五层嵌套压到两层曾经帮人优化过一个订单列表 item。原始布局是这样LinearLayout垂直根布局 └─ LinearLayout水平顶部店铺信息 ├─ ImageView店铺图标 └─ LinearLayout垂直店铺名订单状态 └─ LinearLayout垂直商品区 └─ LinearLayout水平 ├─ ImageView商品图 └─ LinearLayout垂直商品名规格价格 └─ LinearLayout水平底部按钮区再加上各种 padding、背景总共快 8 层 View。后来用 ConstraintLayout 重写同一视觉结构压缩到 2 层一个根 ConstraintLayout里面用约束放图标、文本、按钮不需要中间容器。改完之后嵌套层级从 8 掉到 2measure 时间肉眼可见地下来一半以上。这里要提一个大家容易忽略的细节ConstraintLayout 的约束不能太极端。一个布局里几十个约束链在复杂情况下其内部算法开销也会上升。列表 item 属于高频场景尽量保持每屏 item 的总控件数在合理范围通常一屏 5-8 个 item每个 item 十几二十个控件问题不大但如果单个 item 有五六十个控件那不管什么 LayoutManager 都救不了你建议拆 itemType。经验之谈布局优化是最枯燥、最“肉眼可见但不好量化”的工作但效果恰恰是最扎实的。别的优化可能依赖于设备、数据量、动画开关布局层级的优化是实打实地降低每帧成本。3. 数据绑定阶段最容易被忽视onBindViewHolder 里的成本控制3.1 onBindViewHolder 里只做必要的事很多卡顿问题查到最后都发现是onBindViewHolder里做了不该做的事。举几个真实的例子在绑定里用SimpleDateFormat格式化时间戳每个 item 都 new 一个实例。在绑定里用BitmapFactory.decodeStream读了图片文件。在绑定里调用notifyDataSetChanged()这会导致整个列表重新绑定而这次重新绑定又会再次触发绑定里的耗时操作形成恶性循环。在绑定里做了网络请求回调回来以后直接刷新 UI。这条在基线上看可能不卡但网络抖动时下拉刷新等操作会引发大量 item 的异步回调互相竞争造成局部卡顿。这些操作的共同点是它们都发生在主线程而主线程每一帧只有 16ms 的预算。把任何耗时操作塞进绑定阶段都会直接压缩帧的剩余时间。正确的做法是在数据进入 Adapter 之前完成所有预处理时间格式化、价格计算、文案拼接放到集合装载的阶段或者用协程/线程池预计算后缓存结果。文件、数据库操作移出绑定改为异步读取后把结果存入内存缓存绑定阶段只负责从缓存取数据。SharedPreferences 的读取次数要克制可以把参数在列表加载前一次性读入并持有。如果你在写一个对性能要求比较高的列表可以把onBindViewHolder看作“只做 setText、setImageResource、setVisibility”的纯赋值方法。这样不仅快而且排查问题也简单——绑定方法里没有复杂逻辑自然不会有奇怪 bug。实操心得我有一次排查列表卡顿发现罪魁祸首是onBindViewHolder里调用了holder.itemView.invalidate()。这个调用看着人畜无害实际上会让系统在下一帧强制重绘整个 item连带着所有子 View 都会重新走一遍 draw。这种“强制重绘”在列表滚动场景下极其浪费删掉之后帧率立刻回升。绑定阶段尽量不要碰requestLayout、invalidate这类触发重绘重排的调用除非你完全清楚自己在做什么。3.2 图片加载要裁剪到合适尺寸图片加载在列表场景里是另一个高频雷区。RecyclerView 配合 Glide 或 Coil 是标准做法但不是“用了图片库就万事大吉”。最常见的性能问题包括加载原始大图例如服务器下发的是 2048 像素宽的商品图但 item 里的 ImageView 只需要 200 像素结果解码后内存和 GPU 开销都白花了。没有设置合适的overrideGlide 默认会按 ImageView 尺寸加载但如果 ImageView 尺寸来源不明可能在低内存设备上引发频繁 GC。缓存策略没有区分内存缓存和磁盘缓存或者关闭了缓存导致每次滑动都要重新加载。图片加载的正确姿势是让加载尺寸尽量接近实际显示尺寸同时让图片库的缓存策略真正生效。Glide.with(imageView.context) .load(url) .override(300, 300) // 根据实际显示尺寸的两倍以内设置兼顾清晰度和性能 .centerCrop() .into(imageView)这里的“两倍以内”是一个经验值主要是为 Retina/高密度屏幕留余量300 的 override 实际显示 150 的时候视觉上够清晰但内存占用远小于加载原图。如果列表 item 里的 ImageView 尺寸是固定的还可以用setImageResource加载本地 drawable省去网络成本。还有一个容易忽视的问题图片库的请求要避免在滚动时全量发起。Glide/Coil 本身有生命周期感知和滚动监听优化但要确保你没有在某些封装里强制preload所有数据也别在onBindViewHolder里额外写一个线程池去加载图片那样反而会打乱图片库自己的优先级队列。避坑提醒用File或Uri加载本地图片时注意图片旋转信息EXIF。如果没正确处理旋转角度ImageVIew 会显示得不正常于是有人可能在绑定里反复读取 EXIF 或做额外的 Bitmap 旋转直接毁掉帧表现。正确做法是用 Coil/Glide 自带的变换或者一次性提前处理好而不是在绑定阶段现场转。3.3 用 DiffUtil 替换全量刷新数据更新是另一个隐蔽的卡顿源。业务场景里常见的刷新方式是adapter.notifyDataSetChanged()这个调用会把整个列表标记为“数据变了”然后 RecyclerView 会让所有可见 item 全部重新绑定即便其中 99% 的数据根本没变。数据量小的时候没感觉数据量到几百上千或者 item 绑定本身有一定成本时这种全量刷新就会造成明显卡顿和闪烁。推荐使用ListAdapter配合DiffUtil来做数据更新class MyAdapter : ListAdapterItemBean, MyViewHolder(DiffCallback()) { class DiffCallback : DiffUtil.ItemCallbackItemBean() { override fun areItemsTheSame(oldItem: ItemBean, newItem: ItemBean): Boolean { return oldItem.id newItem.id } override fun areContentsTheSame(oldItem: ItemBean, newItem: ItemBean): Boolean { return oldItem newItem } } }使用submitList()之后DiffUtil 在后台线程计算差异然后只通知 RecyclerView 增删改对应的位置。这样做有更小的开销也避免了不必要的闪动。DiffUtil 使用的几个要点areItemsTheSame用稳定 ID如数据库主键不要用“内容完全相同才返回 true”。两个不同 ID 但内容相同的对象应该是不同的 item。areContentsTheSame判断内容是否变化如果两个 item 的 ID 相同但内容有变化需要返回 false否则 UI 不刷新。如果列表里同时存在大量位置变化DiffUtil 的位移算法会产生额外开销。此时如果列表结构变化很大可以考虑notifyDataSetChanged但前提是 item 绑定很轻量。另外一个不能忽略的点是onBindViewHolder里要使用getItemId()等稳定 ID 来标识作用域避免使用position作为异步回调时定位 item 的唯一依据。很多列表页卡顿背后其实是异步回调频繁触发notifyItemChanged且位置错乱造成的连锁刷新。稳定 ID 配合局部刷新可以大幅减少这种乱局。4. 复用机制与滚动细节让 RecyclerView 跑得更顺4.1 ViewHolder 复用失效的几个场景RecyclerView 的复用机制是它高效的核心滑出屏幕的 ViewHolder 会进入缓存池新滑入的 item 优先复用同类型的 ViewHolder而不重新创建。理解这个机制以后你就可以主动规避一些让复用失效的写法itemViewType 设置不当。如果同一个列表里不同位置返回的 itemViewType 不稳定比如根据数据里的字符串动态生成 type那么复用池就会失效每个 item 都可能新创建 ViewHolder。正确定义方式是把有限的界面形态映射成固定的 int 常量。在 ViewHolder 里动态添加 View。如果在onCreateViewHolder里只 inflate 基础布局然后在onBindViewHolder里又addView加子视图那第一次绑定和之后的复用会产生完全不同的 View 树缓存的意义就没了。ViewHolder 持有昂贵资源。如果你的 ViewHolder 持有监听器、动画对象、甚至 Bitmap这些资源在回收时会拖慢回收速度间接让复用池周转效率变低。释放不必要的资源放在onViewRecycled里做但这方法在滑动过程中会被频繁调用所以里面的动作必须轻量不能做耗时操作。复用机制还有一个反向作用item 视图被复用时之前位置残留的状态可能混淆。比如某些 item 隐藏了某个 View但复用到另一个需要显示该 View 的位置时如果绑定里没有重新设置可见性就会出现错误。因为所有属性都“继承”自上一个位置。这个问题看似是逻辑问题但也会导致你在绑定阶段额外加一堆判断然后绑定变慢又回到性能问题。解决方法是尽量把每个 item 的 UI 状态在绑定里显式设置完备让绑定方法有个明确且简洁的赋值清单。补充一个我自己常用的判断方法如果滑动列表时有明显“卡一下”的瞬间并且刚好发生在 item 类型变化的位置比如从普通文本 item 滑到带大图 item 时多半是 itemType 切换导致创建新 ViewHolder 的成本暴露了。这一类卡顿用 profile 抓RV CreateView耗时会非常明显优化方向是让同类 item 尽量集中或者降低创建 ViewHolder 时的 inflate 与初始化成本。4.2 预取机制与 setHasFixedSizeRecyclerView 从 25.1.0 版本开始内置了预取Prefetch能力。简单理解LayoutManager 在滑动手势开始并稳定后会提前一个 item 的间距利用下一帧的空闲时间提前创建和绑定即将滑入的 ViewHolder从而减少实际滑动时的耗时。这个机制绝大部分时候是自动工作的但有两个前提容易被破坏布局一定得是非嵌套结构如果列表被嵌在 ScrollView 里RecyclerView 无法准确判断预取向预取会失效。数据加载不能太慢。预取只会提前绑定一个 item如果你的onBindViewHolder里还在异步取数据预取帮不上忙。如果列表的宽高是固定的正确做法是设置setHasFixedSize(true)。它的含义是RecyclerView 知道列表自身的尺寸不会因为内容数量变化而变化因此可以在notifyItemChanged等局部更新时跳过重新测量自身。这个开关不能乱开如果你的 item 高度是动态的比如高度由内容决定且会变化开了setHasFixedSize(true)可能引发测量错误。只有明确列表宽度与高度固定时才设置。关于预取配置LayoutManager有两个值得关注的参数setInitialPrefetchItemCount(int)主要用在嵌套 recycle 场景指当这个列表作为子列表被滑入时预取多少个 item。子列表在横向滚动场景中效果尤其明显。GapWorker机制自动运行的但你可以通过避免在每个 item 都大量requestLayout来保证预取线程不被挤占。在自定义 LayoutManager 时预取支持需要实现LayoutManager#supportsPredictiveItemAnimations和组件协调但普通场景直接用系统自带的 LinearLayoutManager 即可。4.3 滚动监听与嵌套列表的坑滚动监听也是一个常见的隐性卡顿来源。很多人为了做“头图缩放”“标题渐变”会在 RecyclerView 上挂addOnScrollListener然后在onScrolled里频繁更新外部 UI。如果这些更新操作本身很重比如每次都修改布局参数、触发 requestLayout主线程就会被大量重复计算占满。优化方向在onScrolled里尽量只做状态判断把真正耗时的 UI 更新放到computeScroll或属性动画里用插值驱动。如果只需要监听是否滚动到底部或标题是否消失可以用标志位判断尽量少触发外部 View 的更新。避免在滚动监听里频繁 new 对象不然会触发 GC而 GC 又会卡住动画。嵌套 RecyclerView 是另一个大坑外层竖向列表item 里嵌一个横向列表看起来是很自然的设计但在低端设备上很容易出现滑动一顿一顿的情况。主要原因在于内层 RecyclerView 有自己的缓存池和预取机制但它和外层列表的交互会让系统无法准确判断滚动意图。如果内层列表的layoutManager每次都新建内层 item 无法复用缓存池形同虚设。嵌套列表的正确写法内层 RecyclerView 使用独立的RecycledViewPool多个同类横向列表共享同一个 pool减少覆盖面大的重复创建。内层 RecyclerView 的高度如果固定可以设置setHasFixedSize(true)。尽量避免让内层 RecyclerView 拦截外层滑动手势通过NestedScrolling或自定义scroll判断来协调。还有一种更极端的做法在需要复用大量横向列表并且条目极多的情况下可以自定义 LayoutManager 直接在外层绘制内层条目。这个方案代码量大一般不推荐除非你已经确认 RecyclerView 嵌套方案确实无法满足性能指标。注意嵌套列表内层不要设置android:nestedScrollingEnabledfalse后一劳永逸这确实可以让外层滚动更顺但同时内层就无法滚动了。很多“横向滑不动”的 bug 就是这么来的。正确的做法是让内层保留滚动能力但在滚动方向上协调处理。5. 常见问题与排查实录5.1 高频卡顿场景速查表下面这个表是我结合多个项目经验整理的几乎覆盖了日常开发里 RecyclerView 卡顿的大部分原因和对策。当你接到“列表卡顿”的反馈时可以按表格快速对号入座避免从零开始排查。典型场景根本原因直接对策滑动过程中持续掉帧主线程长期处于高负载比如任务队列被数据解析或 I/O 挤满把耗时任务移出主线程绑定阶段只做赋值首屏加载慢下滑到中部才卡图片加载过多、原图过大导致内存暴涨用图片库并设置合适的 override 与缓存策略滑动到 item 类型切换时明显顿挫itemType 切换导致新 ViewHolder 创建成本暴露同类 item 尽量集中降低 onCreateViewHolder 里的初始化成本刷新数据后整个列表闪烁卡顿使用 notifyDataSetChanged 全量刷新改用 DiffUtil 局部刷新列表嵌在 ScrollView 里滑动不畅预取机制失效外部 scroll 破坏了 RecyclerView 的测量策略用 NestedScrollView 代替或改用单层 RecyclerView 加 header/footer横向嵌套列表滑动顿挫内层没有设置合适的 RecycledViewPool复用率低共享 RecycledViewPool设置 setHasFixedSize(true)item 里复杂布局导致测量耗时布局嵌套层级过深用 ConstraintLayout 扁平化布局删除无意义容器滚动时手势被外部父控件抢占事件分发冲突导致抬起/按下状态误判协调 parent 的 onInterceptTouchEvent必要时使用 requestDisallowInterceptTouchEvent低内存设备上频繁 GC 卡顿绑定阶段创建临时对象、图片占内存过多减少绑定中的 new 对象压缩图片内存占用开启硬件缓存这个表不是万能药但它提供了一个完整的排查顺序先看主线程任务是否过重再看布局与绑定最后看刷新与复用。按这个顺序走九成的列表卡顿都能找到明确方向。5.2 我踩过的几个真实坑这里聊几个实际案例都来自真实项目供你参考。第一个坑静态数据变成动态数据后列表开始卡。具体的项目里列表本来是静态写死的所以 item 布局写得随意三四个容器嵌套无所谓。后来改成网络数据图片源从固定 drawable 变为网络图片卡顿立刻暴露。排查下来发现 adapter 里有个逻辑每次绑定都会根据“商品状态”重新 setTextColor而状态值是每次从 SharedPreferences 里读的。读一次 SharedPreferences 在帧时间内本来不至于卡顿但问题是列表里几十个 item 同时绑定每个都读一次主线程被反复唤醒 I/O帧预算就爆了。后来把状态一次性读入内存绑定阶段只用内存值卡顿立刻消失。第二个坑动态高度 item 导致的反复测量。有阵子做聊天列表气泡高度根据文本内容自动扩展每个 item 的高度不固定于是我在绑定里对气泡的 TextView 做了动态setMaxWidth和高度计算。这个做法本身没错但每次滑动时TextView 的宽高都要重新计算而且因为高度不固定LayoutManager 无法利用 pre-layout 信息频繁触发 requestLayout。后来改用固定最大宽度 合理换行策略让测量成本集中在首屏而非每次滚动都要重新走一遍测量流程问题才缓解。第三个坑横向 RecyclerView 嵌套纵向列表时内层很多 item 无法复用。原因是横向列表每次重置时我都 new 了一个 LinearLayoutManager 和 RecycledViewPool导致内层 holder 创建开销极高。改成把 innerLayoutManager 和 pool 作为外层 item 的共享对象之后横向滑动明显顺了。别看这个改动小低端机上差距非常大。第四个坑使用了自定义 ItemAnimator 但没控制动画成本。做列表删除/新增动画时自定义了默认的 DefaultItemAnimator 以外的动画效果比如让每个 item 平移加淡入一百个 item 同时执行时动画系统在主线程上的开销直接拉满。后来把冗长动画改成短促的 alpha 动画并在滚动状态下用recyclerView.setItemAnimator(null)暂时关闭效果滚动结束后再恢复。这个技巧在很多需要“滑动性能优先”的场景都适用关键点是判断滚动态而不是一上来就永久关掉动画。最后一个提醒卡顿问题不是一次优化就永远解决的。布局调整、数据量增大、低端机新增都可能导致问题复发。建议在关键版本的发布前用 Profile GPU Rendering 和 Perfetto 跑一遍核心列表路径设置一个简单的帧率指标比如滑动时掉帧率低于 5%有问题尽早发现。根据我个人的经验RecyclerView 的卡顿排查和优化最怕的就是“上来就改代码”。摸清原理、工具定位、按数据绑定、布局、复用、刷新这个顺序逐步推进往往几行改动就有质的提升。流畅的列表体验是用户对一个 App 最直观的感知之一值得你多花那点时间。
返回列表