当表格有 10 万行、几十列,卡死的往往不是绘制,而是你随手调用的那行刷新代码。
前言
一个表格控件,往里面塞几万行、几十列数据,划起来居然还很流畅;拖拽列宽、整列变色、自动适配列宽,都行云流水。这是怎么做到的?
很多人以为数据量一大,瓶颈在"渲染"。其实对于 Android 的列表控件,屏幕上的可见行永远只有那么几十个,真正的杀手是**“无脑刷新”**:数据一变就notifyDataSetChanged(),让所有可见项重新绑定、重新测量。当每一行还有几十个单元格要重新布局时,一次全量刷新就能让你肉眼可见地卡一下,高频操作更是直接卡死。
这篇文章从真实实现里提炼几个关键手段:如何只更新"看得见的那些格",如何在拖拽列宽时不用重建整张表,如何用一次测量算出每列的最合适宽度。
一、问题的样子:一次notifyDataSetChanged有多贵
假设表格是 4 个列表拼起来的,每个列表用 RecyclerView 承载。内容列表的onBindViewHolder里,要为一行创建几十个单元格:
classBodyAdapter:RecyclerView.Adapter<CellHolder>(){overridefunonBindViewHolder(holder:CellHolder,position:Int){valrow=data[position]holder.grid.removeAllViews()for(colin0until columnCount){valcell=createCell(row,col)holder.grid.addView(cell)}}}removeAllViews()再addView(),光这一步就要走一遍 View 的 attach/detach 流程。如果某次操作触发notifyDataSetChanged(),那么屏幕上所有可见行全部重走一遍。数据量越大,行内单元格越多,一次全量刷新越接近卡死。
第一课就一句话:永远别全量刷新。能局部刷新就局部刷新,能只刷一行就只刷一行。
二、拖拽列宽:只改可见的那几个 Holder
列宽是可拖拽调整的。改列宽最直观的实现是"每列宽度变了,整行重刷"。但这样在几万行里等于全量重建。
聪明的做法是:把"当前被实例化出来的 Holder"缓存起来,改列宽时只遍历这些缓存里的可见 Holder,逐个更新子控件宽度。数据没变,只是宽度变了,不需要重新绑定任何内容,只需改宽度。
// 缓存所有正在显示的 Holder,用弱引用避免长期持有导致内存泄漏privatevalholderCache=mutableListOf<WeakReference<CellHolder>>()overridefunonBindViewHolder(holder:CellHolder,position:Int){holderCache.add(WeakReference(holder))// ... 正常绑定}funonColumnWidthChanged(column:Int,newWidth:Int){// 只改可见项,不触发任何 notifyholderCache.forEach{weak->weak.get()?.let{holder->valcell=holder.grid.getChildAt(column)?:return@letcell.layoutParams.width=newWidth 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:List<Any>){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 都不用建。
privatevalmeasurePaint=Paint()funprepareColumnWidth():IntArray{valwidths=IntArray(columnCount)for(rowindata){for(colin0until columnCount){valtext=row[col].toString()valw=measurePaint.measureText(text).toInt()+paddingif(w>widths[col])widths[col]=w}}returnwidths}进一步,如果列里有图标或复选框,宽度还要加上图标宽度和间距;如果要求"填满屏幕等比例放大",就再算一个放大系数,把所有列按比例放大到总宽匹配屏幕。
因为全程只用了Paint.measureText这种纯内存运算,几万行几十列一次算完也就几毫秒。
结论
- 数据量大的列表,全量刷新是头号卡顿源,能用局部刷新就绝不整表刷新。
- 改列宽这类"只动几何不动内容"的操作,遍历缓存的可见 Holder 逐个改宽度,别触发绑定。
- 用
WeakReference缓存可见项,能拿到"即将复用"的项,避免滑动闪错,也不怕内存泄漏。 - 选中高亮用payload 局部刷新,只重绘受影响的部分,跳过整行重建。
- 列宽自适应用
Paint.measureText纯内存测量,数据再多也只是几次算术,毫秒级完成。
你在处理大数据表格时,还遇到过哪些意想不到的卡顿点?欢迎在评论区分享你的踩坑经历。
关键词标签:#Android#性能优化#RecyclerView#大数据表格#局部刷新#Paint测量#Kotlin