技术文章

HotXLS 的行块单元格存储与流式 XLSX 保存

HotXLS 把工作表单元格存放在紧凑的 256 行块里,通过惰性的区间叠加来解析行、列和矩形的格式,而不是创建单元格对象,并在保存时把每一行直接送进压缩包的 deflate 流。这三项改动合在一起,决定了一份大型工作簿的内存画像:峰值用量随最大单行而变化,而不是随完整工作表 XML 的大小

这件事之所以重要,是因为一个每位电子表格开发者迟早都会遇上的形状。用户给整列套上格式——一次点击、一百万个单元格——而一个天真的对象模型用分配一百万个单元格对象来持有一个数字格式索引作为回应。磁盘上的文件保持很小,因为 XLSX 格式把它表达成单条 <col> 条目。进程却一点也不小

为什么给一列套格式比填满它更费内存

因为格式没有数据来为这个对象正名。一个带值的单元格总要在某处存在。一个空却带样式的单元格只是为了携带一个样式索引而存在,而实体化几百万个这样的单元格,是 Delphi 电子表格应用在一份 Excel 能瞬间打开的文件上耗尽地址空间的经典方式

区间样式叠加消除了这种需要。一条行、列或矩形的格式指令被作为范围加上它所贡献的样式部分一次性存储,并在该范围内某个单元格被实际访问时惰性解析。叠加能在结构性编辑后存活——在一个已格式化块之内插入一行会移动区间而不是重建它——并且以紧凑的列、行和仅样式单元格条目的形式往返,这正是 Excel 写出它们的方式

var
  Sheet: TXLSXWorksheet;
  State: TXLSXCellStyleState;
begin
  Sheet := Workbook.Sheets[1];
  // Style indexes come from the workbook style pools, e.g. from a cell
  // you have already formatted the way you want the range to look
  State := BuildStateFromTemplateCell(Sheet.Cells.Item[1, 1]);
  // Format columns B..D without creating a single empty cell object
  Sheet.StyleOverlays.Add(1, 2, MaxRowIndex, 4,
    [xfpNumberFormat, xfpAlignment], State);
end;

TXLSXFormatParts 这个集合决定了一项叠加贡献什么:xfpFontxfpFillxfpBorderxfpNumberFormatxfpAlignmentxfpProtection。只点名你真正想要的那些部分,正是让叠加能够合理分层的原因——一个提供数字格式的列叠加不会与一个提供填充的行叠加打架,因为两者都不声称占有对方的那部分

256 行的块给你带来什么

局部性。单元格按 256 行的块存放,带有稳定的公开句柄,按行优先序列化,所以写一张工作表时按它要发出字节的顺序走过内存,而不是在堆上追逐指针。稳定的句柄对 API 面很重要:调用方持有的句柄在块布局所做的内部重组之后仍然有效,这正是让紧凑表示成为一个实现细节而不是一次破坏性变更的关键

样式池压缩与它并行。每次保存之前,没有单元格引用的字体、填充、边框、数字格式、对齐和保护都被丢弃。长寿的工作簿累积未被引用的样式记录,正如长寿的文档累积未使用的样式,而一份被用户编辑了一小时的工作簿可以带着几百条这样的记录进入一个谁也不会再读它的文件

行流式保存,以及它不适用的场合

启用 StreamingWrite——这是默认值——之后,每一行工作表直接写进压缩包的 deflate 流。被这个标志关掉的另一种做法,是先构建完整的工作表 XML 再压缩,所以峰值内存随整张表而伸缩。流式让它只随一行而伸缩

共享字符串和辅助分部通过一个可复用的 UTF-8 序列化器遵循同样的纪律,一次发出一个条目,把峰值内存约束在最大的单一条目上而不是整个分部上。这覆盖了共享字符串表和透视记录,在一张宽的分析型工作簿上,它们常常比任何单张工作表都大

var
  Workbook: TXLSXWorkbook;
begin
  Workbook := TXLSXWorkbook.Create(nil);
  try
    Workbook.Open('ledger-2026.xlsx');
    // StreamingWrite defaults to True; turn it off only when a downstream
    // step requires the whole worksheet XML to exist before compression
    Workbook.StreamingWrite := True;
    Workbook.SaveAs('ledger-2026-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

除非你有具体理由否则别关掉它。非流式路径是为流水线里其他东西需要已组装 XML 的场合而存在的,默认就为它付代价,是为大多数应用永远碰不上的场合付代价

如何判断叠加是否真在被用

看单元格计数,而不是内存图。如果一张工作表在你套上大范围格式之后报告的物理单元格数量合理,叠加就在起作用。如果计数按格式化的范围大小跳升,说明代码路径里有某处实体化了那些单元格——通常是一个遍历范围内每个单元格来读取其样式的循环,这会一次一个地强制解析,破坏掉整个安排

在你需要某个单元格的有效格式时再去解析一次样式。不要为了查清楚某列有数字格式就为一百万个单元格解析样式;去问叠加。同样的规则适用于写入:把值赋给有值的那些单元格,让格式保持为一个区间

剩下的内存花在哪里

一旦单元格和样式都紧凑了,一份大型工作簿上接下来最大的消费方是共享字符串表和文件携带的任何卫星分部——透视缓存、绘图、来自对象模型不建模的分部的保留 XML。它们各有各的策略,而诚实的答案是没有任何单一设置能一次性解决全部

如果你的瓶颈在打开而不是保存,选择性加载才是那根杠杆:仅元数据与选择性工作表加载的详解覆盖了如何打开工作簿而不为不会碰的工作表付代价。对非常大文件的读路径吞吐,参见 并行 XLSX 解析与内存分配器的笔记;对完全不需要对象模型的纯输出工作负载,面向服务器批量任务的流式写入通常比这里任何调优都更合适

HotXLS 用原生 Delphi 和 C++Builder 代码读写 XLS 和 XLSX,无需安装 Excel、无需 OLE 自动化,这正是让这些内存特性可观察、可控制的前提——HotXLS 电子表格组件页列出了支持的格式与 RAD Studio 版本