
使用 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 PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
$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 \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing();
$drawing->setName("Thumbnail_{$row}");
$drawing->setDescription("Thumbnail for {$product['ref']}");
$drawing->setResizeProportional(true);
$drawing->setImageResource($gdImage);
$drawing->setRenderingFunction($isPng
? \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::RENDERING_PNG
: \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::RENDERING_JPEG);
$drawing->setMimeType($isPng
? \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::MIMETYPE_PNG
: \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::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 . " / $totalProducts\n";
}⚠️ 注意事项:
立即学习“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 区间(取决于单图尺寸)。










