哈Ha! 我向您介绍了文章
“应该停止使用ASP.NET的System.Drawing的五个原因”的翻译。

好吧,他们做到了。 corefx团队最终同意了
无数请求,并将System.Drawing包含在.NET Core中。
(日期为2017年7月的原始文章)
即将发布的System.Drawing.Common软件包将包含整个.NET Framework中的大多数System.Drawing功能,并且打算用作那些希望迁移到.NET Core但由于依赖关系而不能这样做的用户的兼容性选项。 从这个角度来看,微软在做正确的事情。 由于采用.Net Core是更值得的目标,因此需要减少摩擦。
另一方面,System.Drawing是.Net Framework的最贫穷和最匮乏的地区之一,我们中的许多人希望.NET Core的实现将意味着System.Drawing的缓慢死亡。 伴随着死亡,应该有机会做得更好。
例如,Mono团队为Google的跨平台
Skia图形库(称为
SkiaSharp)制作了.NET兼容的包装。 为了简化安装,Nuget在支持每种平台的本机库方面已经走了很长一段路。 Skia功能十分强大,其性能表现为System.Drawing。
ImageSharp团队在重复大部分System.Drawing功能方面也做得很好,但是具有最佳的API和100%的C#实现。 他们仍未准备好进行生产性利用,但似乎他们已经足够接近这一点。 关于此库的一个小警告,因为我们正在谈论在服务器应用程序中的使用:现在,在默认配置中,内部使用Parallel.For来加快某些操作的速度,这意味着最终将使用ASP.NET池中的更多工作流结果,
降低了应用程序的整体吞吐量 。 我希望此行为会在发布之前得到修改,但即使现在更改一个配置行也足以使其更适合在服务器上使用。
无论如何,如果要在服务器上的应用程序中绘制,绘图或将文本绘制成图像,则无论是否切换到.NET Core,都应认真考虑将System.Drawing更改为任何内容。
就我而言,我已经为.NET和.NET Core组装了一个高性能的图像处理管道,该管道提供了System.Drawing无法提供的图像质量,并且在专门为服务器设计的高度可扩展的体系结构中做到了。 到目前为止,仅适用于Windows,但是跨平台已在计划中。 如果使用System.Drawing(或其他方式)调整服务器上图像的大小,最好考虑使用
MagicScaler作为替代。
但是,System.Drawing复活使某些开发人员更容易过渡,但由于开发人员被迫寻找替代方案,因此可能会扼杀这些项目获得的大部分动力。 不幸的是,在.NET生态系统中,无论替代方案有多出色,Microsoft库和软件包都将始终取胜。
这篇文章试图修复一些System.Drawing错误的计算,希望即使System.Drawing仍然是一个选择,开发人员也可以探索替代方案。
我将从System.Drawing文档中经常引用的
免责声明开始。
讨论System.Drawing.Common时,在Github上的
讨论中多次提出了这种拒绝。
“不支持在Windows或ASP.NET服务中使用具有System.Drawing命名空间的类。 试图在这些类型的应用程序中使用这些类可能会导致意外的问题,例如服务器性能下降和运行时错误。”
像你们中的许多人一样,我很久以前阅读了此免责声明,然后我跳过了此免责声明,仍然在ASP.NET应用程序中使用System.Drawing。 怎么了 因为我爱危险。 要么,要么没有其他可行的选择。 你知道吗? 没什么事 我很可能不应该这样说,但我敢打赌,你们中的许多人都经历过同样的事情。 那么,为什么不继续使用System.Drawing或基于它的库呢?
原因1:GDI描述符
如果您在服务器上使用System.Drawing遇到过问题,则很可能是这种情况。 如果未测试,则这是最可能的原因之一。
System.Drawing大部分是围绕Windows GDI + API的薄包装。
GDI描述符支持许多System.Drawing对象,并且它们对处理器和用户会话有一定的限制。 如果达到此阈值,您将收到“内存不足”异常和/或GDI +“通用”错误。
问题在于,在.NET中,即使在轻负载下,垃圾回收和进程终止也可能在达到限制时延迟释放这些描述符。 如果忘记了(或不知道需要什么)在包含此类描述符的对象上调用Dispose(),则很可能在环境中遇到此类错误。 与大多数与资源限制或泄漏有关的错误一样,这种情况很可能会成功测试,并带您进行生产性操作。 自然,当您的应用程序承受最大负载时,就会发生这种情况,以便最大数量的用户了解您的耻辱。
对处理器和用户会话的限制取决于操作系统的版本,并且对处理器的限制是可自定义的。 但是版本无关紧要,因为 GDI描述符在内部由USHORT数据类型表示,因此每个用户会话有65,536个描述符的严格限制,即使编写良好的应用程序也有在足够负载下达到此限制的风险。 如果您认为功能更强大的服务器将允许您在一个实例上同时为越来越多的用户提供服务,那么这种风险就变得更加现实。 真的,谁愿意创建对扩展性有严格限制的软件?
原因2:并发
尽管其中许多与
Windows7 / Windows Server 2008 R2中的体系结构更改有关,但是GDI +始终在并发方面存在问题,您仍然可以在新版本中看到其中的一些。 最值得注意的是在DrawImage()操作期间由GDI +安排的
进程锁定 。 如果使用System.Drawing(或包装它的库)在服务器上调整图像的大小,则DrawImage()方法可能是此代码的基础。
此外,当同时对DrawImage()进行多次调用时,
所有它们都将被阻塞,直到它们全部执行为止。 即使响应时间对您来说不是问题(为什么?您讨厌用户吗?)请记住,与这些请求关联的任何内存资源以及与这些请求关联的对象持有的所有GDI描述符都与运行时相关。 实际上,启动服务器并不需要太多的负载就可以引发问题。
当然,有针对此特定问题的解决方法。 例如,一些开发人员为每个DrawImage()操作创建一个外部过程。 但是实际上,这样的解决方法只会增加额外的脆弱性,而您实际上不应该这样做。
原因3:记忆
考虑一个生成图表的ASP.NET处理程序。 他应该做这样的事情:
- 将位图创建为画布
- 使用笔和/或画笔在位图上绘制多个形状
- 使用一种或多种字体绘制文本
- 将位图另存为PNG到MemoryStream
假设图表以600点乘400点计。 这是总共240,000个点,对于默认RGBA格式,一个点乘以4个字节,对于位图总计为960,000字节,再加上一点用于绘制对象和保存缓冲区。 整个请求的大小设为1mb。 在这种情况下,您很可能不会遇到内存问题,并且如果遇到任何问题,则很可能会限制描述符的数量,因为图像,画笔,钢笔和字体都有自己的描述符,因此我在前面已经提到过。
真正的问题出在将System.Drawing用于成像任务时。 System.Drawing主要是一个图形库,而图形库通常都是基于所有东西都是内存中的位图的思想构建的。 当您考虑这些小事情时,这很棒。 但是图像可能真的很大,而且每天都在变大,因为 百万像素的相机越来越便宜。
如果您采用System.Drawing的幼稚图像构建方法,那么对于调整大小处理程序,您将获得以下内容:
- 创建一个位图作为目标图像的画布。
- 将原始图像加载到另一个位图中。
- 使用调整大小,使用目标图像的“ image-source”参数调用DrawImage()。
- 将目标位图以JPEG格式保存到内存流中。
假设目标图像将为600x400,如前面的示例所示,那么我们又为
目标图像和内存流分配了1MB的空间。 但是,假设有人从他们的新DSLR中上传了24兆像素的图像,那么对于RGB格式的已解码源位图,我们需要6000x4000像素,每个像素3字节(72 MB)。 我们将使用System.Drawing中的HighQualityBicubic重采样,因为它看起来很不错。 然后,我们需要考虑其他6000x4000点(每个点有4个字节),以便
在被调用方法内部进行
PRGBA转换 ,并增加96mb的已用内存。 对于转换一张图像的请求,总共需要169mb(!)。
现在,假设您有多个用户在执行此类操作。 现在请记住,在所有请求都被完全执行之前,请求将被阻止。 内存用完需要多长时间? 而且,即使您不担心会耗尽所有可用的内容,也请记住,有很多方法可以更好地使用服务器的内存,而不是保留一堆像素。 考虑内存压力对应用程序/系统其他部分的影响:
- ASP.NET缓存可能开始刷新需要重新创建的项目
- 垃圾收集器将更频繁地启动,从而降低应用程序速度
- IIS内核缓存或Windows文件系统缓存可以删除有用的元素
- 应用程序池可能超出内存限制,并可能重新启动
- Windows可能会开始将内存交换到磁盘,从而降低整个系统的速度
您真的不想要任何一个吗?
专为图像处理任务设计的图书馆以完全不同的方式解决了这个问题。 他们不需要将整个源图像或目标图像加载到内存中。 如果您不想在其上绘制,则不需要画布/位图。 这样做更像这样:
- 为目标图像的JPEG编码器创建流
- 从原始图像加载一行并水平压缩
- 根据需要重复多次以形成目标文件的一行
- 垂直压缩结果线
- 从第2步开始重复,直到处理完源文件的所有行。
使用此方法,可以使用总共1MB的内存来处理同一图像,甚至更大的图像也将需要稍微增加开销。
我只知道一个通过此原理优化的.NET库,我会给您一个提示:这不是System.Drawing。
原因4:CPU
System.Drawing的另一个面向图形的而不是面向图像的副作用是DrawImage()在CPU使用率方面效率很低。 我在
之前的文章中对此进行了详细介绍,但是可以通过以下事实来总结此讨论:
- 在System.Drawing中,HighQualityBicubic比例转换仅适用于PRGBA格式。 在几乎所有情况下,这都意味着图像的额外副本。 这不仅会(大量)使用更多的内存,还会消耗处理器周期来转换和处理额外的alpha通道。
- 即使在图像为原始格式之后,HighQualityBicubic比例转换也执行比获得正确转换结果所需的计算大约多4倍的计算。
这些事实会增加大量浪费的CPU周期。 在按分钟付费的多云环境中,这直接增加了托管成本。 当然,您的响应时间也会受到影响。
考虑一下这样一个事实,即会消耗更多的电并产生热量。 您对图像处理任务使用System.Drawing直接影响全局变暖。 你是个怪物。
原因5:图像处理看似复杂
除了性能之外,System.Drawing在许多方面都无法正确处理图像。 使用System.Drawing意味着要么输出不正确,要么学习有关ICC配置文件,颜色量化,exif方向,校正和许多其他特定内容的一切。 这是一个兔子洞,大多数开发人员都没有时间也没有探索的欲望。
诸如ImageResizer和ImageProcessor之类的库已经吸引了很多支持者,他们会注意其中的一些细节,但是要小心,它们内部装有System.Drawing,并且附带了我在本文中详细介绍的所有行李。
奖励理由:您可以做得更好
如果像我一样在生活中的某个时刻不得不戴眼镜,您可能还记得这是第一次戴眼镜。 我以为我的视线正常,如果我斜视正确,那么一切都会很清楚。 但是后来我戴上了这些眼镜,世界变得比我想象的要详细得多。
System.Drawing几乎相同。 如果
正确填写设置 ,它会做正确的事情,但是如果您使用最好的实用程序,您会惊讶于您的图像看起来更好。
我仅以此处为例。 与默认的MagicScaler设置相比,这是最好的System.Drawing。 也许您的应用程序将从获得积分中受益...
Gdi:

MagicScaler:
雅各布·欧文斯(Jakob Owens)摄环顾四周,探索替代方法,请以对小猫的热爱为名,停止在ASP.NET中使用System.Drawing