低影响图像






4.89/5 (42投票s)
本文介绍了一种处理应用程序中图像生命周期的棘手问题的方法。
引言
几天前,有人在一个论坛里发帖问,当一个图像被使用在多个地方时,谁有责任处置这个图像。在这种特定情况下,发帖人有一个图像,它既被用于一个用户控件,又被显示在一个窗体上。这是一个相当普遍的问题,有一个相当简单的解决方案,它利用了一个.NET中称为“弱引用”的便捷功能。
弱引用
在我们深入研究代码之前,我想回顾一些旧知识。如果你熟悉弱引用,那么可以跳过本节——如果你想了解弱引用的陷阱是什么,请继续阅读。
正如你所知,.NET是一种垃圾回收语言,这通常意味着我们不必担心内存;当内存不再使用时,垃圾收集器可以释放它。到目前为止,这应该对你来说不算什么新鲜事。
现在,.NET并非总是能释放所有内容,因为可能存在需要释放的非托管代码,而这些东西会随着时间的推移而累积——在编写良好的应用程序中,这些代码通过实现IDisposable
的类来移除(好吧,这是一种简化,但它是一种有用的方式来设想这个过程)。然而,如果你想释放一个尚未被解除引用的对象的内存,会发生什么?如果你的应用程序对一个对象有强引用,它将不会被垃圾回收。
答案是使用WeakReference
。一个弱引用的对象可以随时被垃圾回收,因此内存可以被释放,同时我们可以在某个时候获得对该对象的强引用,这意味着垃圾收集器将不再尝试回收它。
好了,理论讲够了——让我们来看看这在实践中是如何工作的。
private WeakReference _image = new WeakReference(null);
using (FileStream fs = info.OpenRead())
{
try
{
if (_image == null)
_image = new WeakReference(null);
_image.Target = Image.FromStream(fs, true, false);
fs.Close();
}
catch (Exception ex)
{
throw new ArgumentException("Unable to load the image file", ex);
}
}
在上面的代码中,我们创建了一个弱引用的对象(_image
),我们用它来加载图像。垃圾收集器可以随时回收这些信息,但如果信息仍然存在,我们可以使用它。那么,我们如何做到这一点呢?
细心的人会注意到,图像实际上存储在弱引用的目标中。换句话说,不是弱引用本身——目标是我们感兴趣的项目,这表明我们可以使用WeakReference
本身来确定图像是否已被回收(或者换句话说,我们的应用程序将对WeakReference
对象拥有强引用,该对象维护着对图像的弱引用——这意味着收集器无法回收WeakReference
对象,但它可以回收图像)。总之,这是确定项目是否未被垃圾回收的一种方法。
if (_image.IsAlive)
{
Image strongReference = _image.Target as Image;
}
IsAlive
告诉我们图像没有被垃圾回收,而strongReference
行创建了一个对图像的强引用。听起来不错,对吧?好吧,这就是我之前提到的陷阱所在。你看到了吗?这里的代码稍作调整,应该能说明这一点
Image strongReference = _image.Target as Image;
if (strongReference == null)
{
using (FileStream fs = info.OpenRead())
{
try
{
_image.Target = Image.FromStream(fs, true, false);
fs.Close();
}
catch (Exception ex)
{
throw new ArgumentException("Unable to load the image file", ex);
}
}
strongReference = _image.Target as Image;
}
是的,我们不是在检查IsAlive
然后获取引用,而是使用引用本身来确定它是否已被垃圾回收。换句话说,我们不能在这里依赖IsAlive
,因为可能存在一个竞争条件,即垃圾收集器在IsAlive
和从Target
分配之间移除了引用。
ImageManager
在附件的解决方案中,我们有一个图像管理器类,我们(巧妙地)用它来添加图像,并且这个类负责管理图像的生命周期。这意味着我们已经很大程度上消除了决定在哪里处置这些项目的难题。
图像名义上存储在一个字典中,并可以使用键来引用。实际上,图像存储在一个单独的ImageCache
类中,该类以弱引用的方式维护图像,而ImageManager
提供了对它的访问。所有对图像的访问都通过这个类进行管理,该类实现为单例。
添加图像很简单——这是包含的窗体中的代码,演示了如何在列表框中选择一个项目时添加图像并显示它。
private void btnFile_Click(object sender, EventArgs e)
{
using (FileDialog dialog = new OpenFileDialog())
{
dialog.CheckFileExists = true;
dialog.ValidateNames = true;
dialog.Filter = "All files (*.*)|*.*";
if (dialog.ShowDialog() == DialogResult.OK)
{
string file = dialog.FileName;
string key = txtKey.Text;
if (string.IsNullOrEmpty(key))
{
key = Guid.NewGuid().ToString();
}
ImageManager.Instance.Add(key, file);
listKeys.Items.Add(key);
ShowImage(key);
}
}
}
private void ShowImage(string key)
{
displayImage.Image(key);
}
private void KeyChosen(object sender, EventArgs e)
{
string key = listKeys.SelectedItem.ToString();
ShowImage(key);
}
当我们从ImageManager
类中返回图像时,我们得到的回报不是原始图像,而是它的一个克隆。之所以需要ImageCache
类,是因为我们需要保留对路径的引用,以便在必要时通过加载它的新实例来恢复图像。
最后的想法
我知道这不符合我通常提倡WPF的风格。这个示例中没有WPF代码,这是有原因的;WPF的设计理念就是围绕弱引用,所以在WPF领域,我在这里展示的内容是很常见的。请随时试用代码;如果它有用,请使用它——如果没用,请忽略它。
历史
- 2010年6月8日 - 更新了代码,以引发更合适的异常,处理某些会抛出GDI+异常的文件,并使用了Sacha Barber出色的通用弱引用实现(谢谢Sacha)。