65.9K
CodeProject 正在变化。 阅读更多。
Home

GDI+ 资源(Pen、Brush 等)的释放器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.17/5 (18投票s)

2009年7月7日

CPOL

7分钟阅读

viewsIcon

67503

用于调用已创建 GDI+ 资源的 Dispose() 方法的辅助类。

引言

System.Drawing 命名空间中找到的绘图资源类(PenSolidBrush 等)实际上封装了底层的原生 Windows GDI 资源。像所有系统资源一样,它们的数量是有限的,因此应尽快将它们返回给操作系统。

这就是为什么 .NET 文档告诉您在不再使用 PenBrush 等时,应立即调用其 Dispose() 方法。

背景

但是 .NET 是一个托管环境,并且有一个垃圾回收器,那么为什么我们必须手动释放资源呢?考虑以下常见的绘图代码

protected override void OnPaint(PaintEventArgs e)
{
    Pen myPen1 = new Pen(Color.Black, 3);
    Pen myPen2 = new Pen(Color.Lime, 3);
    SolidBrush myBrush1 = new SolidBrush(Color.Red);
    SolidBrush myBrush2 = new SolidBrush(Color.Lime);

    // ....
    // use myPenX and myBrushX for drawing
    // ...
}

PenSolidBrush 在内部持有非托管的原生 Win32 GDI 资源(HBRUSHHPEN 等)。这些是宝贵的 Windows 系统资源,应尽快释放。当 OnPaint() 退出时,对 myPenXmyBrushX 的唯一引用消失了,因此 .NET 垃圾回收器将在稍后某个时间释放实例。当然,这也会释放底层的原生句柄。但关键是,是**稍后某个时间**。在最后一个引用消失和垃圾回收器释放未使用的实例之间的时间段内,底层原生资源被占用,尽管它们不再使用。这在系统负载较重的情况下可能会成为问题。而且,像所有资源一样,我们应该尽可能少地使用它们,并在不再需要时立即返回它们。

因此,我们必须对所有 PenBrush 等调用 Dispose()

protected override void OnPaint(PaintEventArgs e)
{
    Pen myPen1 = new Pen(Color.Black, 3);
    Pen myPen2 = new Pen(Color.Lime, 3);
    SolidBrush myBrush1 = new SolidBrush(Color.Red);
    SolidBrush myBrush2 = new SolidBrush(Color.Lime);

    // ....
    // use myPen and myBrush for drawing
    // ...

    myPen1.Dispose();
    myPen2.Dispose();
    myBrush1.Dispose();
    myBrush2.Dispose();
}

如果您有一个复杂的绘图例程,其中包含许多画笔和刷子,那么对所有实例调用 Dispose() 可能是一项繁琐且容易出错的任务。您很容易在代码中间添加一个新的画笔,然后忘记在方法退出之前对其调用 Dispose()。并且请注意:如果您的绘图代码中抛出异常,控制流可能会跳出 OnPaint() 而不运行到其结束。因此,您的实例的 Dispose() 将根本不会被调用。

C# 为这种情况提供了 using 语句

protected override void OnPaint(PaintEventArgs e)
{
    using (Pen myPen1 = new Pen(Color.Black, 3))
    using (Pen myPen2 = new Pen(Color.Lime, 3))
    using (SolidBrush myBrush1 = new SolidBrush(Color.Red))
    using (SolidBrush myBrush2 = new SolidBrush(Color.Lime))
    {
        // use myPenX and myBrushX for drawing
    }
    // here all resources created inside using() are diposed,
    // also in case of an exception or return statement
}

这完美地解决了简单 OnPaint() 方法的问题。但是,如果涉及到根据控件状态使用不同绘图资源的更复杂绘图,我们必须在绘图之前创建所有可能使用的资源

protected override void OnPaint(PaintEventArgs e)
{
    using (Pen myPen1 = new Pen(Color.Black, 3))
    using (Pen myPen2 = new Pen(Color.Lime, 3))
    using (SolidBrush myBrush1 = new SolidBrush(Color.Red))
    using (SolidBrush myBrush2 = new SolidBrush(Color.Lime))
    {
        if ( some condition )
        {
            // use myPen, myBrush1 and myBrush2, myPen2 is not used
            if ( some other condition )
            {
                // use myPen2
            }
        }
        else
        {
            // use myPen1 and myBrush1 for drawing,
            // myPen2 and myBrush2 are never used
        }
    }
    // here all resources created inside using()
    // are diposed, also in case of an exception or return
}

当然,如果只不必要地创建了一个画刷或画笔,这并不是什么大问题。我为工业过程做可视化,在那里我们使用复杂的自定义控件,涉及大量复杂的绘图,使用许多不同的资源。而且,有两个大的 1600x1080 屏幕同时显示许多这些控件。所有这些不必要的创建的绘图资源对系统性能有可衡量的影响。为了避免这种情况,我们最终会得到这样的结果

protected override void OnPaint(PaintEventArgs e)
{
    using (Pen myPen1 = new Pen(Color.Black, 3))
    using (SolidBrush myBrush1 = new SolidBrush(Color.Red))
    {
        if ( some condition )
        {
            using (SolidBrush myBrush2 = new SolidBrush(Color.Lime))
            {
                // use myPen1, myBrush1 and myBrush2
                if ( some other condition )
                {
                    using (Pen myPen2 = new Pen(Color.Lime, 3))
                    {
                        // use myPen2
                    }
                    // here myPen2 is disposed
                 }
            }
            // here myBrush2 is disposed
        }
        else
        {
            // use myPen1 and myBrush1 for drawing, myPen2 and myBrush2 are never used
        }
    }
    // here myPen1 and myBrush1 are disposed
}

到目前为止,一切顺利。但是想象一下,如果存在 6 个条件和 12 个绘图资源,其中一些在所有控制路径中使用,一些仅在一个路径中使用,一些在其中三个路径中使用,依此类推,我们最终会得到什么样的代码。由此产生的嵌套 using 语句结构难以阅读,甚至更难维护。如果绘图代码发生变化,并且以前仅在一个控制路径中使用的资源在新控制路径中也使用了,我们必须重新排列整个代码。

在方法退出时释放绘图资源的辅助类

我有很强的 C++ 背景,在 C++ 中,将资源封装在对象中以便在它们超出范围时立即释放是一种最佳实践。这对于使代码异常安全尤其重要。因此,我将 C++ 方法移植到了 C#:使用一个辅助类来释放资源。

using System;
using System.Collections.Generic;

class Disposer : IDisposable
{
    private List<IDisposable> m_disposableList = 
            new List<IDisposable>();
    private bool m_bDisposed = false;

    // default ctor
    public Disposer()
    {
    }

    public void Add(IDisposable disposable)
    {
        if (m_bDisposed)
        {
            // its not allowed to add additional items
            // to dispose if Dispose() already called
            throw new InvalidOperationException(
              "Disposer: tried to add items after call to Disposer.Dispose()");
        }
        m_disposableList.Add(disposable);
    }

    #region IDisposable members
    public void Dispose()
    {
        if (!m_bDisposed)
        {
            foreach (IDisposable disposable in m_disposableList)
            {
                disposable.Dispose();
            } 
            m_disposableList.Clear(); 
            m_bDisposed = true;
        }
    } 
    #endregion
}

System.Drawing 命名空间中的所有绘图资源类都实现了 IDisposable 接口。它声明了一个方法 Dispose()。有关详细信息,请参阅 MSDN 文档中关于 IDisposable 的内容。我们将所有要释放的对象实例添加到 Disposer 的实例中,该实例会遍历内部列表并对所有条目调用 Dispose()。请注意,此代码不是线程安全的。为此,m_disposableListm_bDisposed 需要使用 lock() 进行保护。此辅助类的预期用例是在用户控件内部,其中所有成员都必须从最初创建控件的线程访问。

Disposer 辅助类的用法

现在,OnPaint() 方法已更改为使用 Disposer 辅助类

protected override void OnPaint(PaintEventArgs e)
{
    using (Disposer drawResDisposer = new Disposer())
    {
        Pen myPen1 = new Pen(Color.Black, 3);
        drawResDisposer.Add(myPen1);
        SolidBrush myBrush1 = new SolidBrush(Color.Red);
        drawResDisposer.Add(myBrush1);

        if ( some condition )
        {
            SolidBrush myBrush2 = new SolidBrush(Color.Red);
            drawResDisposer.Add(myBrush2);
            // use myPen1, myBrush1 and myBrush2
            if ( some other condition )
            {
                Pen myPen2 = new Pen(Color.Yellow, 3);
                drawResDisposer.Add(myPen2);
                // draw using myPen2
            }
        }
        else
        {
            // use myPen1 amyBrush1 for drawing,
            // myPen2 and myBrush2 is never used
        }
        // here all resources added to drawResDisposer
        // in any control path are disposed
    }

只需将每个实例化的绘图对象添加到 Disposer,即 drawResDisposer,并在 using {} 环绕的代码块退出时,drawResDisposer.Dispose() 会自动调用,它会调用添加到其中的所有实例的 Dispose()。无需知道哪些绘图资源是实际需要和创建的。只需将它们添加到释放器,使用它们,然后忘记它们。我认为这是一种更清晰、更容易维护的代码结构。

Disposer 类也可以用于封装原生和非托管资源的其他任何类。只要该类实现 IDisposable 接口即可。

我只喜欢一种程序员的“懒惰”:不要手动做编译器或运行时可以自动完成的事情。

使用 WeakReference 改进 Disposer

在高级用户控件中,我们不应该在每次调用 OnPaint() 时都创建所有绘图资源。在诸如调整大小等情况下,绘图会频繁发生。每次使用画笔就创建一个,然后直接交给垃圾回收器销毁,这是资源的浪费。每个绘图资源都应该在第一次使用时创建,并在控件的 Dispose() 方法中释放。换句话说,我们为用于绘图的每种不同绘图资源类型创建一个单例。这样,资源在控件的生命周期内由控件持有,但这仍然比一遍又一遍地构造和销毁相同的绘图资源要好。但是,这种方法使问题更加糟糕:资源现在在控件类的不同方法中创建和销毁,因此在控件的 Dispose() 方法中调用每个资源的 Dispose() 变得更加困难。如果我们在控件销毁时未能释放释放器实例,则内部存在的对 IDisposable 实例的引用会阻止垃圾回收器释放它们。这不如不处理...

为了使释放器辅助类在这种情况下更有用,它被更改为使用 .NET System.WeakReference 类。它允许垃圾回收器释放对象,即使仍然存在引用,但这是一个弱引用。有关详细信息,请参阅 MSDN 文档。此类正是为此类情况而设计的:一个对象的引用,可以用来释放它,但它本身不被垃圾回收器计为引用。

using System;
using System.Collections.Generic;

class Disposer : IDisposable
{
    private List<WeakReference> m_disposableList = 
            new List<WeakReference>();
    private bool m_bDisposed = false;

    // default ctor
    public Disposer()
    {
    }

    public void Add(IDisposable disposable)
    {
        if (m_bDisposed)
        {
            // its not allowed to add additional items
            // to dispose if Dispose() already called
            throw new InvalidOperationException(
              "Disposer: tried to add items after call to Disposer.Dispose()");
        }
        m_disposableList.Add(new WeakReference(disposable));
    }

    #region IDisposable members
    public void Dispose()
    {
        if (!m_bDisposed)
        {
            foreach (WeakReference weakRef in m_disposableList)
            {
                try
                {
                    if (weakRef.IsAlive)
                    {
                        // sadly there is no generic version of WeakReference
                        // WeakReference<IDisposable> would be nice...
                        IDisposable strongRef = (IDisposable)weakRef.Target;
                        // strongRef is null if weakRef.Target already disposed
                        if (strongRef != null)
                        {
                            strongRef.Dispose();
                        }
                    }
                } 
                catch (System.InvalidOperationException ex)
                {
                    // weakRef.Target already finalized
                }
            }
        } 
        m_disposableList.Clear(); 
        m_bDisposed = true;
    }
    #endregion
}

这是改进后的类在控件内部的用法

class MyControl : UserControl
{
    private Disposer m_Disposer = new Disposer();

    private Pen m_Pen1 = null;
    private Pen1
    {
        get
        {
            if (m_Pen1 == null)
            {
                m_Pen1 = new Pen(Color.Lime, 5);
                m_Disposer.Add(m_Pen1);
            }
            return m_Pen1;
        }
    }

    private SolidBrush m_Brush1 = null;
    private Brush1
    {
        get
        {
            if (m_Brush1 == null)
            {
                m_Brush1 = new SolidBrush(Color.Red);
                m_Disposer.Add(m_Brush1);
            }
            return m_Brush1;
        }
    }

    //
    // ... many other pens, brushes, fonts, we draw a copmlicated control
    //

    protected override void OnPaint(PaintEventArgs e)
    {
        // ...
        // use the same singleton on every call
        e.Graphics.DrawLine(this.Pen1, point1, point2);
        e.Graphics.FillRectangle(this.Brush1, rect);
        // ...
    }

    #region IDisposable members
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (components != null)
            {
                components.Dispose();
            }
            m_Disposer.Dispose()
            base.Dispose(disposing);
        }
    }
    #endregion

    //
    // ...
    //
}

您所要做的就是添加**粗体**显示的行。如果您忘记调用 m_Disposer.Dispose(),垃圾回收器会处理绘图资源。释放器类中持有的引用不再计入垃圾回收器。这正是我们想要的。

关注点

using 关键字的这种可能用法经常被忽视

// myType is any type implementing IDisposable
using (myType myVar = new myType())
{

    // ...
    // code here making use of myVar
    // ...
}

如果 using() 后面的代码块在**任何**路径中被离开,包括异常、中间的 return 等,myVar.Dispose() 都会被隐式调用。所需的调用由 C# 编译器插入。在 using() 中使用的类型的唯一限制是它们必须实现 IDisposable

using 语句实际上只是语法糖,它被 C# 编译器扩展为以下代码。如果您使用 Reflector 检查您的二进制程序集,您会看到这一点

try
{
    // myType is any type implementing IDisposable
    myType myVar = new myType();

    // ...
    // code here making use of myVar
    // ...

}
catch (System.Exception ex)
{

}
finally
{
    myVar.Dispose();
}

结论

尽管不调用 GDI 包装类上的 Dispose() 不会真正导致资源泄漏,但这样做是个好习惯。这样,它们在不再需要时就会被释放,而不是在垃圾回收器下次运行时才被释放。

有更好的主意吗?

我知道这是一种 C++ 方法。我对 C# 相对较新,但在之前十多年里一直使用 C 和 C++。所以,如果您有更“C# 化”的方法来做这件事,请告诉我。

历史

  • 2009 年 7 月 7 日 - 初始版本。
  • 2009 年 10 月 13 日 - 添加了 WeakReference 版本,并做了一些小的文本补充。
  • 2009 年 10 月 22 日 - 重新编写了代码示例,以更好地展示预期的用例。
© . All rights reserved.