为什么我的C#绘图方法会耗尽内存?

4

我是c#的新手,尝试通过编写一些简单的应用程序来熟悉语法和.NET库。我最近承担的小项目是一个极坐标钟(类似于这里找到的那个)

我早期注意到的一个问题是应用程序会不断“闪烁”,这真的影响了呈现效果,所以我在网上阅读了如何实现双缓冲区的资料,这样可以消除这个问题,但也可能与问题无关。 这是我的onPaint方法; 它由定时器控件每33ms(约30FPS)调用。 大部分其余的应用程序只是处理拖动应用程序(因为它是无框架且具有透明背景),双击退出等操作。

    protected override void OnPaint(PaintEventArgs e) {
        DateTime now = DateTime.Now;

        float secondAngle = now.Second / 60F;
        secondAngle += (now.Millisecond / 1000F) * (1F / 60F);

        float minuteAngle = now.Minute / 60F;
        minuteAngle += secondAngle / 60F;

        float hourAngle = now.Hour / 24F;
        hourAngle += minuteAngle / 60F;

        float dayOfYearAngle = now.DayOfYear / (365F + (now.Year % 4 == 0 ? 1F : 0F));
        dayOfYearAngle += hourAngle / 24F;

        float dayOfWeekAngle = (float)(now.DayOfWeek + 1) / 7F;
        dayOfWeekAngle += hourAngle / 24F;

        float dayOfMonthAngle = (float)now.Day / (float)DateTime.DaysInMonth(now.Year, now.Month);
        dayOfMonthAngle += hourAngle / 24F;

        float monthAngle = now.Month / 12F;
        monthAngle += dayOfMonthAngle / (float)DateTime.DaysInMonth(now.Year, now.Month);

        float currentPos = brushWidth / 2F;

        float[] angles = {
            secondAngle, minuteAngle,
            hourAngle, dayOfYearAngle,
            dayOfWeekAngle, dayOfMonthAngle,
            monthAngle
        };

        SolidBrush DateInfo = new SolidBrush(Color.Black);
        SolidBrush background = new SolidBrush(Color.Gray);
        Pen lineColor = new Pen(Color.Blue, brushWidth);
        Font DateFont = new Font("Arial", 12);

        if (_backBuffer == null) {
            _backBuffer = new Bitmap(this.Width, this.Height);
        }

        Graphics g = Graphics.FromImage(_backBuffer);
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        try {                
            g.Clear(Color.White);
            if (_mouseIsOver) {
                g.FillEllipse(background, new Rectangle(0, 0, this.Width, this.Height));
            }
            foreach (float angle in angles) {
                g.DrawArc(
                    lineColor,
                    currentPos, currentPos,
                    this.Height - currentPos * 2, this.Width - currentPos * 2,
                    startAngle, angle * 360F
                );

                currentPos += brushWidth + spaceStep;
            }

            // Text - Seconds

            g.DrawString(String.Format("{0:D2} s", now.Second), DateFont, DateInfo, new PointF(115F, 0F));
            g.DrawString(String.Format("{0:D2} m", now.Minute), DateFont, DateInfo, new PointF(115F, 20F));
            g.DrawString(String.Format("{0:D2} h", now.Hour), DateFont, DateInfo, new PointF(115F, 40F));
            g.DrawString(String.Format("{0:D3}", now.DayOfYear), DateFont, DateInfo, new PointF(115F, 60F));
            g.DrawString(now.ToString("ddd"), DateFont, DateInfo, new PointF(115F, 80F));
            g.DrawString(String.Format("{0:D2} d", now.Day), DateFont, DateInfo, new PointF(115F, 100F));
            g.DrawString(now.ToString("MMM"), DateFont, DateInfo, new PointF(115F, 120F));
            g.DrawString(now.ToString("yyyy"), DateFont, DateInfo, new PointF(115F, 140F));

            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);
        }
        finally {
            g.Dispose();
            DateInfo.Dispose();
            background.Dispose();
            DateFont.Dispose();
            lineColor.Dispose();
        }
        //base.OnPaint(e);
    }

    protected override void OnPaintBackground(PaintEventArgs e) {
        //base.OnPaintBackground(e);
    }

    protected override void OnResize(EventArgs e) {
        if (_backBuffer != null) {
            _backBuffer.Dispose();
            _backBuffer = null;
        }
        base.OnResize(e);
    }

我曾以为在方法结束时清除所有内容就可以安全了,但似乎并没有帮助。此外,运行时和OutOfMemoryException之间的间隔不是恒定的;有时只需要几秒钟,但通常需要一两分钟。以下是一些类范围内的变量声明。

    private Bitmap _backBuffer;

    private float startAngle = -91F;
    private float brushWidth = 14;
    private float spaceStep = 6;

And a screenshot (edit: screenshot links to a view with some code present):

屏幕截图
(来源: ggot.org)

编辑: 堆栈跟踪!

System.OutOfMemoryException: Out of memory.
   at System.Drawing.Graphics.CheckErrorStatus(Int32 status)
   at System.Drawing.Graphics.DrawArc(Pen pen, Single x, Single y, Single width, Single height, Single startAngle, Single sweepAngle)
   at PolarClock.clockActual.OnPaint(PaintEventArgs e) in C:\Redacted\PolarClock\clockActual.cs:line 111
   at System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs)
   at System.Windows.Forms.Control.WmPaint(Message& m)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

似乎是上次崩溃时相同的行,循环内部的主要drawArc


1
请裁剪您的屏幕截图,使其尺寸不要超过必要范围。 - Gabe
@Gabe:完成了;我使用了较大的那一个来显示一些额外的代码,所以我已经把新的截图链接到了旧的截图上。 - Dereleased
根据http://msdn.microsoft.com/en-us/library/system.drawing.graphics.drawarc(VS.90).aspx中的评论,当弧小于2像素或3.5扫描度时,drawArc存在一个错误。 - Andreas Paulsson
我刚刚增加了一个1.5度的扫描范围选项,因为崩溃似乎发生在非常小的值上,比如0.02;让我们看看这样会怎么样。 - Dereleased
提供的代码没有任何泄漏,可以在我的机器上稳定运行。 - Hans Passant
6个回答

8

确保您也处理 Pen 和 Brush 对象,并使用 using 块来确保即使存在异常也要处理对象。

顺便提一下:避免每次绘制时重新创建和处理 _backBuffer。可以捕获 resize 事件并在那里处理 _backBuffer,或者只需检查每个 Paint 事件中的 _backBuffer 是否具有正确的尺寸,如果尺寸不匹配,则进行处理和重新创建。


正如Fredrik Mörk所说,不要忘记字体。当涉及到GDI对象时,Windows(和.NET)非常挑剔。始终释放所有可以被释放的GDI对象。 - Andreas Paulsson
2
不要处理 e。这是调用该方法的代码的责任。 - Guffa
我已经实现了所有这些建议,并更新了原始问题中的代码以反映这一点。我只是让它运行,看看现在是否更好。五分钟后,我会把它称为暂时的成功,如果它一整晚都持续下去,我就会接受这个答案。 - Dereleased
1
如果您添加我在上面评论中提到的检查:不要绘制小于3.5度的弧,会怎样呢?此外,还有两个关于此错误的Connect报告:http://connect.microsoft.com/VisualStudio/feedback/details/253886/graphics-drawarc-throws-outofmemoryexception-for-no-good-reason和http://connect.microsoft.com/VisualStudio/feedback/details/121532/drawarc-out-of-memory-exception-on-small-arcs。 - Andreas Paulsson
我会在获取异常信息后尝试实现该检查。 - Dereleased
显示剩余2条评论

8

谢谢,这有效地给了我提示,如何修复一个 LinearGradientBrush 的 Out of Memory 异常,当它的距离小于 2 像素时。 - Alexandre TryHard Leblanc

3
我并没有发现您的代码有什么严重的问题。请问您能提供 OutOfMemoryException 出现的具体行数吗?
只是让您知道,我花了几个月才理解: OutOfMemoryException 并不意味着内存用尽。;-) 当 GDI+ 发生一些简单的错误时(在我的看法中,这表明 GDI+ 内部存在糟糕的编码风格),例如尝试加载无效图像或具有无效像素格式的图像等,它就会发生。

看起来很奇怪。这可能与您每33毫秒创建新对象有关,或者弧的某些计算产生无效结果(如0)。尝试调试项目,等待再次崩溃并检查传递给DrawArc的值!除此之外,我无法给您任何建议 :-( - Michael
由于某种原因,当在IDE中运行时崩溃时,我没有选择查看异常详细信息的选项,我只能得到无效位图渲染,然后就这样了,没有停止,我的其他方法(如拖放和双击)都正常工作。 - Dereleased
有没有想过为什么我在IDE中没有收到异常通知?我很想在程序崩溃时检查变量的值... - Dereleased
根据我的经验,有时候在IDE中异常的处理方式会非常不同。虽然我不确定为什么会这样...通常情况下,你应该能够检查值。你可以尝试使用try-catch语句捕获异常和/或自己跟踪值。 - Michael
这个答案为我节省了几个小时的调试时间。在我的情况下,我使用的是具有错误坐标的矩形的FillRectangle()。我从不知道“内存不足”可能是那样的结果。 - DarenW
显示剩余3条评论

2

虽然不是回答“为什么”的答案,但这是一个可能的解决方案:

你不应该每次都创建一个新位图。 只需在绘制新帧时清除它即可。

但是,当你的尺寸改变时,应该创建一个新的位图。


在这一点上,大小不应该改变,但还是谢谢你的建议! - Dereleased
哦,如果你把它改成那样工作,请小心最小化应用程序,你的控件可能会收到一个0x0的大小,而位图不喜欢用那个大小创建 ;) - Stormenet

1
为什么每次想要用OnPaint绘制东西时都需要一个新的位图?你只需要一个。尝试像这样做:
private Bitmap _backBuffer = new Bitmap(this.Width, this.Height);

protected override void OnPaint(PaintEventArgs e) { 

    Graphics g = Graphics.FromImage(_backBuffer);

    //Clear back buffer with white color...
    g.Clear(Color.White);

    //Draw all new stuff...
}

我最初这样做是为了确保我没有保留任何不必要的东西,尽管绝大多数人认为这是一个坏主意;另一方面,如果画布大小发生变化,它可以重新创建,这是未来可能发生的一种情况,一旦我进行了一些自定义设置;在那种情况下,每次调整大小都需要重新制作它。 - Dereleased

0

这不是对你的问题的答案,也许你这样做有很好的理由(我可能会学到一些东西),但为什么要先创建一个位图,然后在位图上绘制,最后再在窗体上绘制位图呢? 直接在窗体上绘制不是更有效率吗? 可以尝试类似于以下代码:

protected override void OnPaint(PaintEventArgs e) {
    base.OnPaint(e);
    //_backBuffer = new Bitmap(this.Width, this.Height);
    Graphics g = Graphics.FromImage(_backBuffer);

    //Rest of your code
    //e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);

    //g.Dispose();
    //e.Dispose();
    //base.OnPaint(e);

    //_backBuffer.Dispose();
    //_backBuffer = null;
}

同样根据MSDN的说明

在派生类中重写 OnPaint 方法时,请确保调用基类的 OnPaint 方法,以便已注册的委托接收事件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接