
使用 phpspreadsheet 的 `memorydrawing` 插入数千张商品缩略图时易触发内存耗尽;根本解法是避免长期持有 gd 资源与 `memorydrawing` 实例,及时释放图像资源并分批写入磁盘,而非依赖内存中持续维护整个工作表对象。
在导出含大量图片(如 6000+ 商品缩略图)的 excel 文件时,直接循环创建 MemoryDrawing 实例会导致 php 进程内存持续攀升——原因在于:
- 每个 MemoryDrawing 关联一个 GD 图像资源(imagecreatefrompng/jpeg),该资源不会被 PHP 自动回收,除非显式销毁;
- MemoryDrawing::setWorksheet() 会将绘图对象注册到工作表中,但 unset($drawing) 并不能释放底层 GD 资源;
- 即使调用 $spreadsheet->garbageCollect(),已绑定到 worksheet 的绘图对象仍被强引用,无法释放。
✅ 正确做法是 “即用即弃” + “分段落盘”:
- 每插入若干行(如 50–100 行)后,立即保存当前文件并重置对象;
- 每次处理前重新加载工作表(非追加式读取),但关键点在于:不复用旧 MemoryDrawing,也不保留旧 GD 资源;
- 严格手动释放 GD 资源(使用 imagedestroy());
- 避免在内存中累积所有绘图对象。
以下是优化后的推荐实现(支持断点续传、内存可控):
use PhpOfficePhpSpreadsheetSpreadsheet; use PhpOfficePhpSpreadsheetWriterXlsx; use PhpOfficePhpSpreadsheetReaderXlsx as XlsxReader; use PhpOfficePhpSpreadsheetCellDataType; $filePath = 'products_with_thumbnails.xlsx'; $batchSize = 50; $totalProducts = count($products); // 初始化空文件(仅首段) if (!file_exists($filePath)) { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setCellValue('A1', 'Ref')->setCellValue('B1', 'Title')->setCellValue('C1', 'Thumbnail'); $sheet->getColumnDimension('A')->setAutoSize(true); $sheet->getColumnDimension('B')->setAutoSize(true); $sheet->getColumnDimension('C')->setAutoSize(true); (new Xlsx($spreadsheet))->save($filePath); } // 分批写入 for ($start = 0; $start < $totalProducts; $start += $batchSize) { $end = min($start + $batchSize, $totalProducts); $batch = array_slice($products, $start, $batchSize); // 重新加载当前文件(确保从磁盘读取最新状态) $reader = new XlsxReader(); $spreadsheet = $reader->load($filePath); $sheet = $spreadsheet->getActiveSheet(); // 从第2行开始写入(跳过标题行),计算实际起始行号(已有数据行数 + 1) $currentRow = $sheet->getHighestRow() + 1; foreach ($batch as $i => $product) { $row = $currentRow + $i; $sheet->setCellValueByColumnAndRow(1, $row, $product['ref']); $sheet->getCellByColumnAndRow(1, $row)->setDataType(DataType::TYPE_STRING); $sheet->setCellValueByColumnAndRow(2, $row, $product['title']); if (!empty($product['image'])) { $imagePath = $product['image']; // 假设为本地路径 $sheet->getRowDimension($row)->setRowHeight(80); // 创建 GD 资源并立即绑定绘图 $isPng = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION)) === 'png'; $gdImage = $isPng ? imagecreatefrompng($imagePath) : imagecreatefromjpeg($imagePath); if ($gdImage === false) { continue; // 跳过损坏图片 } $drawing = new PhpOfficePhpSpreadsheetWorksheetMemoryDrawing(); $drawing->setName("Thumbnail_{$row}"); $drawing->setDescription("Thumbnail for {$product['ref']}"); $drawing->setResizeProportional(true); $drawing->setImageResource($gdImage); $drawing->setRenderingFunction($isPng ? PhpOfficePhpSpreadsheetWorksheetMemoryDrawing::RENDERING_PNG : PhpOfficePhpSpreadsheetWorksheetMemoryDrawing::RENDERING_JPEG); $drawing->setMimeType($isPng ? PhpOfficePhpSpreadsheetWorksheetMemoryDrawing::MIMETYPE_PNG : PhpOfficePhpSpreadsheetWorksheetMemoryDrawing::MIMETYPE_JPEG); $drawing->setHeight(80); $drawing->setCoordinates('C' . $row); $drawing->setWorksheet($sheet); // ✅ 关键:立即销毁 GD 资源(否则内存永不释放) imagedestroy($gdImage); unset($gdImage, $drawing); // 显式清除变量 } } // 保存当前批次结果到磁盘(覆盖原文件) $writer = new Xlsx($spreadsheet); $writer->save($filePath); // 清理内存(可选,但建议) $spreadsheet->disconnectWorksheets(); unset($spreadsheet, $sheet, $reader, $writer); // 可选:输出进度 echo "✓ Saved rows " . ($start + 1) . "–" . $end . " / $totalProductsn"; }
⚠️ 注意事项:
立即学习“PHP免费学习笔记(深入)”;
- 绝不复用 MemoryDrawing 实例:每个图片必须新建对象;
- 必须调用 imagedestroy():这是释放 GD 内存的唯一可靠方式;
- 避免 substr_count($imagePath, ‘.png’) 判断格式:应使用 pathinfo() 获取扩展名,更健壮;
- 不要在循环中 unset($spreadsheet) 后再 load() —— 这会丢失已写入的图片:正确方式是每次 load() 当前磁盘文件,它已包含之前写入的所有内容(包括图片);
- 若图片 URL 为远程地址,请先 file_get_contents() 下载到临时文件再处理,避免多次网络请求阻塞。
? 总结:PhpSpreadsheet 的图片写入本质是「序列化 GD 资源到 Excel 二进制流」,其内存压力主要来自未释放的 GD 图像。通过「分批加载 → 单批绘图 → 立即保存 → 显式销毁」四步闭环,即可稳定导出万级图片 Excel,内存占用恒定在 ~20–50 MB 区间(取决于单图尺寸)。