SkiaSharp图片缩放内存占用过高,求原因排查与优化方案
问题描述
基于.NET 7 Web MVC + SkiaSharp 2.88.5实现了图片缩放接口:LoadImage方法接收imageId和scaleFactor参数,对JPG/PNG格式图片进行缩放。相关代码如下:
控制器代码
public IActionResult LoadImage(int imageId, double scaleFactor) { var path = _imageService.GetImagePath(imageId); if (System.IO.File.Exists(path)) { var extension = Path.GetExtension(path); var contentType = FileTypeMapping.GetMimeTypeFromExtension(extension); var img = System.IO.File.OpenRead(path); var resizedImage = FileHelpers.ResizeImage(img, extension, scaleFactor); return File(resizedImage, contentType); } return NotFound(); }
SkiaSharp缩放方法代码
private static readonly Dictionary<string, SKEncodedImageFormat> _skiaSharpImageFormatMapping = new(StringComparer.InvariantCultureIgnoreCase) { {"png", SKEncodedImageFormat.Png }, {"jpg", SKEncodedImageFormat.Jpeg }, {"jpeg", SKEncodedImageFormat.Jpeg }, {"jpe", SKEncodedImageFormat.Jpeg }, }; public static Stream ResizeImage(Stream imgStream, string extension, double scaleFactor) { using (var skData = SKData.Create(imgStream)) { using (var skImage = SKBitmap.Decode(skData)) { int newHeight = (int)(skImage.Height * scaleFactor); int newWidth = (int)(skImage.Width * scaleFactor); using (var scaledBitmap = skImage.Resize(new SKImageInfo(newWidth, newHeight), SKFilterQuality.Low)) { using (var image = SKImage.FromBitmap(scaledBitmap)) { using (var encodedImage = image.Encode(_skiaSharpImageFormatMapping[extension], 50)) { var stream = new MemoryStream(); encodedImage.SaveTo(stream); stream.Seek(0, SeekOrigin.Begin); return stream; } } } } } }
现象:待缩放的JPG图片磁盘大小为2963KB(分辨率4032x3025px),调用/LoadImage?imageId=555&scaleFactor=0.5接口时,进程内存从385MB增至471MB,单次调用内存增长达86MB;多客户端并发调用时服务器易出现内存不足问题。
疑问:
- 该内存增长是否正常?
- ResizeImage方法是否存在问题导致该现象?
- 如何优化内存占用?
问题解答
1. 内存增长是否正常?
这个内存增长偏高但符合位图处理的基础逻辑,但存在可优化的冗余开销:
- 原始JPG是压缩格式,磁盘大小仅2963KB,但解码后会转为未压缩的位图:按ARGB32格式(每个像素4字节)计算,4032×3025像素的内存占用约为
4032*3025*4 ≈ 47MB。 - 缩放后的位图(2016×1512像素)内存占用约12MB。
- 加上SkiaSharp处理过程中的临时对象、编码缓冲区等,理论上单次处理的内存增量应在60MB左右,实际增长86MB说明存在不必要的内存浪费。
2. ResizeImage方法的问题
ResizeImage方法存在3个导致内存浪费的问题:
- Stream重复拷贝:
SKData.Create(imgStream)会把整个文件流读入内存,后续返回的MemoryStream又保存了编码后的图片数据,相当于同一时间内存中存在原始位图、缩放后位图、编码后流三份数据。 - 未处理格式异常:如果
_skiaSharpImageFormatMapping找不到对应扩展名,会抛出KeyNotFoundException,且未释放已分配的资源。 - 全尺寸位图加载冗余:直接用
SKBitmap.Decode(skData)加载完整原始位图,没有利用SkiaSharp的直接解码到目标尺寸的能力,额外占用了全尺寸位图的内存。
3. 内存优化方案
(1)重构ResizeImage方法,减少内存拷贝
直接传入文件路径解码,跳过Stream到SKData的内存拷贝,同时增加格式校验:
private static readonly Dictionary<string, SKEncodedImageFormat> _skiaSharpImageFormatMapping = new(StringComparer.InvariantCultureIgnoreCase) { {".png", SKEncodedImageFormat.Png }, {".jpg", SKEncodedImageFormat.Jpeg }, {".jpeg", SKEncodedImageFormat.Jpeg }, {".jpe", SKEncodedImageFormat.Jpeg }, }; public static Stream ResizeImage(string imagePath, double scaleFactor) { if (!_skiaSharpImageFormatMapping.TryGetValue(Path.GetExtension(imagePath), out var format)) throw new ArgumentException("不支持的图片格式"); // 直接从文件路径解码,避免Stream到SKData的内存拷贝 using var originalBitmap = SKBitmap.Decode(imagePath); int newWidth = (int)(originalBitmap.Width * scaleFactor); int newHeight = (int)(originalBitmap.Height * scaleFactor); using var scaledBitmap = originalBitmap.Resize(new SKImageInfo(newWidth, newHeight), SKFilterQuality.Low); using var image = SKImage.FromBitmap(scaledBitmap); using var encodedData = image.Encode(format, 50); var stream = new MemoryStream(); encodedData.SaveTo(stream); stream.Position = 0; return stream; }
(2)修复控制器的文件流泄漏问题
原控制器中File.OpenRead(path)打开的流未被释放,会导致文件句柄和内存泄漏,修改后:
public IActionResult LoadImage(int imageId, double scaleFactor) { var path = _imageService.GetImagePath(imageId); if (!System.IO.File.Exists(path)) return NotFound(); try { var extension = Path.GetExtension(path); var contentType = FileTypeMapping.GetMimeTypeFromExtension(extension); var resizedImage = FileHelpers.ResizeImage(path, scaleFactor); return File(resizedImage, contentType); } catch (ArgumentException ex) { return BadRequest(ex.Message); } }
(3)使用SKCodec直接解码到目标尺寸
通过SKCodec跳过全尺寸位图的加载,直接解码到缩放后的尺寸,大幅降低内存占用:
public static Stream ResizeImage(string imagePath, double scaleFactor) { using var codec = SKCodec.Create(imagePath); var originalInfo = codec.Info; int newWidth = (int)(originalInfo.Width * scaleFactor); int newHeight = (int)(originalInfo.Height * scaleFactor); var scaledInfo = new SKImageInfo(newWidth, newHeight, originalInfo.ColorType, originalInfo.AlphaType); using var scaledBitmap = new SKBitmap(scaledInfo); // 直接解码到缩放后的位图,避免加载原始全尺寸位图 codec.GetPixels(scaledInfo, scaledBitmap.GetPixels(), scaledInfo.RowBytes); using var image = SKImage.FromBitmap(scaledBitmap); if (!_skiaSharpImageFormatMapping.TryGetValue(Path.GetExtension(imagePath), out var format)) throw new ArgumentException("不支持的图片格式"); using var encodedData = image.Encode(format, 50); var stream = new MemoryStream(); encodedData.SaveTo(stream); stream.Position = 0; return stream; }
(4)添加并发控制与结果缓存
- 用
SemaphoreSlim限制并发处理的图片数量,防止瞬间大量请求导致内存暴涨:
private static readonly SemaphoreSlim _imageProcessingSemaphore = new SemaphoreSlim(4); // 根据服务器配置调整数量 public async Task<IActionResult> LoadImage(int imageId, double scaleFactor) { var path = _imageService.GetImagePath(imageId); if (!System.IO.File.Exists(path)) return NotFound(); await _imageProcessingSemaphore.WaitAsync(); try { var extension = Path.GetExtension(path); var contentType = FileTypeMapping.GetMimeTypeFromExtension(extension); var resizedImage = FileHelpers.ResizeImage(path, scaleFactor); return File(resizedImage, contentType); } catch (ArgumentException ex) { return BadRequest(ex.Message); } finally { _imageProcessingSemaphore.Release(); } }
- 对高频使用的缩放结果添加内存缓存(如使用
IMemoryCache),避免重复处理相同参数的请求。
内容的提问来源于stack exchange,提问作者Aleks Vujic
相关产品推荐
相关产品推荐

