You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;多客户端并发调用时服务器易出现内存不足问题。

疑问:

  1. 该内存增长是否正常?
  2. ResizeImage方法是否存在问题导致该现象?
  3. 如何优化内存占用?

问题解答

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 03:54:57