
当表格有 10 万行、几十列卡死的往往不是绘制而是你随手调用的那行刷新代码。前言一个表格控件往里面塞几万行、几十列数据划起来居然还很流畅拖拽列宽、整列变色、自动适配列宽都行云流水。这是怎么做到的很多人以为数据量一大瓶颈在渲染。其实对于 Android 的列表控件屏幕上的可见行永远只有那么几十个真正的杀手是**“无脑刷新”**数据一变就notifyDataSetChanged()让所有可见项重新绑定、重新测量。当每一行还有几十个单元格要重新布局时一次全量刷新就能让你肉眼可见地卡一下高频操作更是直接卡死。这篇文章从真实实现里提炼几个关键手段如何只更新看得见的那些格如何在拖拽列宽时不用重建整张表如何用一次测量算出每列的最合适宽度。一、问题的样子一次notifyDataSetChanged有多贵假设表格是 4 个列表拼起来的每个列表用 RecyclerView 承载。内容列表的onBindViewHolder里要为一行创建几十个单元格classBodyAdapter:RecyclerView.AdapterCellHolder(){overridefunonBindViewHolder(holder:CellHolder,position:Int){valrowdata[position]holder.grid.removeAllViews()for(colin0until columnCount){valcellcreateCell(row,col)holder.grid.addView(cell)}}}removeAllViews()再addView()光这一步就要走一遍 View 的 attach/detach 流程。如果某次操作触发notifyDataSetChanged()那么屏幕上所有可见行全部重走一遍。数据量越大行内单元格越多一次全量刷新越接近卡死。第一课就一句话永远别全量刷新。能局部刷新就局部刷新能只刷一行就只刷一行。二、拖拽列宽只改可见的那几个 Holder列宽是可拖拽调整的。改列宽最直观的实现是每列宽度变了整行重刷。但这样在几万行里等于全量重建。聪明的做法是把当前被实例化出来的 Holder缓存起来改列宽时只遍历这些缓存里的可见 Holder逐个更新子控件宽度。数据没变只是宽度变了不需要重新绑定任何内容只需改宽度。// 缓存所有正在显示的 Holder用弱引用避免长期持有导致内存泄漏privatevalholderCachemutableListOfWeakReferenceCellHolder()overridefunonBindViewHolder(holder:CellHolder,position:Int){holderCache.add(WeakReference(holder))// ... 正常绑定}funonColumnWidthChanged(column:Int,newWidth:Int){// 只改可见项不触发任何 notifyholderCache.forEach{weak-weak.get()?.let{holder-valcellholder.grid.getChildAt(column)?:returnletcell.layoutParams.widthnewWidth cell.requestLayout()}}}注意这里有个细节为什么不用recyclerView.getChildAt()去拿可见项而要自己维护缓存因为列表控件的可见项分两级屏幕上正在显示的、和即将复用进入二级缓存的。只用getChildAt()会漏掉那些刚被回收、还带着旧宽度的项导致滑动时突然出现某一行宽度没跟上的闪错。所以需要自己维护一份完整的、包含回收待复用项的缓存。同时用WeakReference而不是强引用是因为这个缓存是长生命周期的挂在 Adapter 上一旦某个 Holder 被真正回收销毁弱引用会被自动清空避免Adapter 持有 View 引用式的内存泄漏。三、payload 局部刷新只重绘被选中的一格表格里有选中某一行/某一格的需求比如点击一行让它高亮。最省事的写法是notifyItemChanged(position)但这会导致那一行全部单元格重新绑定。其实我们只想改那一行的背景色。RecyclerView 提供了payload 机制notifyItemChanged(position, payload)传一个对象进去onBindViewHolder会收到这个 payload然后只处理需要变化的部分跳过整行重建。funselectRow(position:Int){notifyItemChanged(position,SelectPayload)// 只发选中信号}overridefunonBindViewHolder(holder:CellHolder,position:Int,payloads:ListAny){if(payloads.isEmpty()){super.onBindViewHolder(holder,position,payloads)// 全量绑定return}// 只处理 payload刷新背景不重建单元格if(payloads.contains(SelectPayload)){holder.grid.setBackgroundColor(if(isSelected(position))selectedColorelsenormalColor)}}这样选中逻辑不再触发单元格的重建只是轻量地改一下背景。拖拽选行、高亮当前行这类高频操作就不会卡了。四、列宽自适应一次测量算清所有列最后是自动适配列宽表格加载时根据每列最长内容算出每列的合适宽度让内容不被截断。朴素做法是每个单元格 inflate 出来量一下几万行几十列光 inflate 就能卡死。这里用的是文本测量用Paint在内存里直接量文本宽度连 View 都不用建。privatevalmeasurePaintPaint()funprepareColumnWidth():IntArray{valwidthsIntArray(columnCount)for(rowindata){for(colin0until columnCount){valtextrow[col].toString()valwmeasurePaint.measureText(text).toInt()paddingif(wwidths[col])widths[col]w}}returnwidths}进一步如果列里有图标或复选框宽度还要加上图标宽度和间距如果要求填满屏幕等比例放大就再算一个放大系数把所有列按比例放大到总宽匹配屏幕。因为全程只用了Paint.measureText这种纯内存运算几万行几十列一次算完也就几毫秒。结论数据量大的列表全量刷新是头号卡顿源能用局部刷新就绝不整表刷新。改列宽这类只动几何不动内容的操作遍历缓存的可见 Holder 逐个改宽度别触发绑定。用WeakReference缓存可见项能拿到即将复用的项避免滑动闪错也不怕内存泄漏。选中高亮用payload 局部刷新只重绘受影响的部分跳过整行重建。列宽自适应用Paint.measureText纯内存测量数据再多也只是几次算术毫秒级完成。你在处理大数据表格时还遇到过哪些意想不到的卡顿点欢迎在评论区分享你的踩坑经历。关键词标签#Android#性能优化#RecyclerView#大数据表格#局部刷新#Paint测量#Kotlin