来自流的安全图像
缓解从流和文件创建的 Image 对象的问题。
引言
当从任何类型的 Stream 创建 Image 对象时,该 Image 在后续可能会表现出一些特有的行为。MSDN 文档中的一行就解释了原因:
引用您必须在 Image 的整个生命周期内保持流处于打开状态。
有时,了解 Stream 的生命周期可能很困难。当您持有对 Stream 的引用时,更难知道何时最终可以释放该 Stream。本文提供了一个类来解决这些问题。
一个相关的问题是,如果从文件创建 Image 对象,该文件将在对象的整个生命周期内被锁定。这意味着在您的应用程序中为图像创建临时文件意味着在图像完全释放之前无法删除这些文件。
Using the Code
解决这些问题有两种基本方法。最常见的方法是不关闭创建 Image 的 Stream。然而,这可能会很麻烦,因为 (a) 您的应用程序中到处都有打开的 Stream,它们在不再需要后仍然占用资源,并且 (b) GC 可能会注意到孤立的流并在 Image 仍在使用时将其释放,导致其显示与您关闭流相同的奇怪行为。
SafeImage 类通过复制 Stream 来解决上述问题,然后从该副本创建 Image。然后,对副本 Stream 和 Image 的引用会一起维护和释放。但是,原始 Stream 的正确释放仍然取决于您。
另一种可能且经常实现的解决方案是创建 Image 的副本并使用该副本而不是存在上述问题的副本。这是一个良好而可靠的解决方案,前提是您在复制图像后立即释放原始图像。此解决方案的唯一问题是复制图像可能会很慢,因此如果您这样做很多,它可能会影响您应用程序的性能。SafeImage 可根据需要选择使用此方法。
您可以通过将可选的 UseStreamType 枚举值传递给构造函数来告诉 SafeImage 使用哪种算法来保护 Image 的完整性。此参数的可能值为:
UseStreamType.None | 使用 Stream 创建一个临时的 Image,然后维护其副本。应注意,使用此类型的构造函数实际上会显式创建一个 Bitmap,因此内部 Image 可以安全地强制转换为 Bitmap。 |
UseStreamType.MemoryStream | 将 Stream 复制到内部 MemoryStream,然后从该 MemoryStream 创建新的 Image。MemoryStream 会在 SafeImage 的生命周期内保持活动状态。 |
UseStreamType.TempFileStream | 将原始 Stream 复制到临时文件,打开该文件,然后从该 FileStream 创建 Image。 |
UseStreamType.BestFit | 如果原始流的 Length 大于任意静态 MaxMemoryStream 值(目前设置为约 200MB),则使用 TempFileStream,否则使用 MemoryStream。这是大多数构造函数的默认设置。 |
SafeImage 可以基本用作 Image 的即插即用替换。这是通过定义 `implicit` 运算符在 SafeImage 和 Image 之间进行转换来实现的。这意味着类似这样的代码...
SafeImage img = new SafeImage(filename);
pictureBox1.Image = img;
...将正常工作。返回的图像实际上是正在保存的图像的副本,因为 SafeImage 无法知道接收者可能会如何使用它(例如,释放它可能会很麻烦)。
创建 SafeImage 的所有工作都在各种构造函数中完成。static FromXxxx 方法只是调用具有匹配签名的构造函数。每个构造函数将在下面描述。
从现有 Image 构建 SafeImage
SafeImage(Image image, UseStreamType streamType = UseStreamType.None);
复制提供的 Image。默认情况下,此方法使用 Bitmap.FromImage 方法创建新的 Image。但是,如果传递了 UseStreamType.MemoryStream、UseStreamType.TempFileStream 或 UseStreamType.BestFit 选项之一,则此构造函数将创建适当的 Stream,将原始 Image 保存到其中,然后从新创建的 Stream 创建新的 Image。SafeImage.FromBitmap 方法调用此构造函数来创建其返回值。
从 Stream 构建 SafeImage
SafeImage(Stream stream, UseStreamType streamType = UseStreamType.BestFit);
SafeImage(Stream stream, bool useEmbeddedColorManagement,
UseStreamType streamType = UseStreamType.BestFit);
SafeImage(Stream stream, bool useEmbeddedColorManagement,
bool validateImageData, UseStreamType streamType = UseStreamType.BestFit);
上述每个构造函数都会创建一个相应类型(MemoryStream 或 FileStream)的新 Stream,并将原始 Stream 的内容复制到该新 Stream 中。然后,它使用具有相应签名的 Image.FromStream 方法从副本 Stream 创建 Image。此副本 Stream 的引用保存在 SafeImage 中,直到 SafeImage 被释放才会关闭。
如果这些构造函数传递了 UseStreamType.None 选项,则直接从输入的 Stream 创建一个临时的 Image。然后通过复制临时 Image 来创建新的 Image,最后释放临时 Image。代码如下:
using (Image temp = Image.FromStream(stream)) {
_image = new Bitmap(_image);
}
从文件构建 SafeImage
SafeImage(string filename, UseStreamType streamType = UseStreamType.BestFit);
SafeImage(string filename, bool useEmbeddedColorManagement,
UseStreamType streamType = UseStreamType.BestFit);
上述构造函数的工作方式是打开文件,并将内容复制到新的 MemoryStream 或 FileStream(根据需要)。然后它们关闭中间的 FileStream,并从复制的数据创建 Image。这样做的目的是释放文件上的锁定,以便应用程序可以对其进行任何认为合适的其他操作。
与 Stream 类型构造函数一样,如果这些构造函数传递了 UseStreamType.None 值,则从打开的 FileStream 创建一个临时的 Image,然后进行复制。FileStream 和临时 Image 都将被释放。
一些示例
SafeImage 的一些特别有用的应用包括下载图像或从数据库 BLOB 字段读取图像。
// Download an Image
SafeImage image;
using (HttpWebRequest request = (HttpWebRequest)WebRequest.Create ("http://someImageUrl") {
using(HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
using (Stream responseStream = response.GetResponseStream()) {
image = new SafeImage(responseStream);
}
}
}
// Get Database BLOB Images
DbConnection connection;
// ... set up the connection
List<SafeImage> images = new List<SafeImage>();
using (DbCommand cmd = connection.CreateCommand()) {
cmd.CommandText = "SELECT Image FROM Images WHERE Image IS NOT NULL";
using (DbDataReader rs = cmd.ExecuteReader()) {
while (rs.Read()) {
images.Add(new SafeImage(rs.GetStream(0));
}
}
}
// Assign a SafeImage to a PictureBox
string tempImage = "myFile.jpg";
pictureBox1.Image = new SafeImage(tempImage);
File.Delete(tempImage); // Succeeds
关注点
有时,我注意到操作系统在关闭文件后可能需要一些时间才能完全释放所有锁定。因此,在删除 SafeImage 使用 UseStreamType.TempFileStream 标志创建的临时文件时,如果删除失败,我会每 1/4 秒重试一次,最多重试约 10 秒。为了不让整个系统在此过程中被阻塞,我将其放在一个单独的线程中。99% 的情况下,第一次或第二次尝试就会成功,因此该线程会很快终止。但如果其他操作正在占用该文件,或者偶尔发生操作系统出现问题并且其锁计数出现混乱,它仍然不会阻塞整个应用程序。这意味着通常您的应用程序会自己干净利落地清理,最坏的情况是临时文件夹中会留下一个文件,由用户的常规维护程序进行清理。
