C#Picturebox拉伸内存问题

时间:2017-10-20 11:33:05

标签: c# picturebox

我之前已经问过这个问题,但我的问题直接与使用' Stretch' (或正常'以外的任何其他内容)使用图片框,'正常'效果很好。

我有一个简单的应用程序,基本上每秒使用计时器拍摄60次_timer.Interval = (1.0/60.0) * 1000.0;。不幸的是,使用' stretch'导致使用越来越多的内存,最终导致应用程序崩溃。

public partial class Form1 : Form
{

    System.Timers.Timer _timer = new System.Timers.Timer();

    public Form1()
    {
        InitializeComponent();

        _timer.Elapsed += new ElapsedEventHandler(OnElapsed);
        _timer.Interval = (1.0/60.0) * 1000.0;
    }

    private void btnCapture_Click(object sender, EventArgs e)
    {
       _timer.Enabled = true;
    }

    private void OnElapsed(object o, ElapsedEventArgs e)
    {
        Rectangle bounds = Screen.PrimaryScreen.Bounds;
        Bitmap bmp = new Bitmap(bounds.Width, bounds.Height);

        using (Graphics g = Graphics.FromImage(bmp))
        {
            g.CopyFromScreen(0, 0, 0, 0, new Size(bounds.Width, bounds.Height));

            using (Image prev = pbCapture.Image)
            {
                pbCapture.Image = bmp;
            }
        }
    }
}

如您所见,我处理了图形IDisposable和图片框使用的上一个图像。然而,记忆仍在增加。

有什么我做错了吗?

2 个答案:

答案 0 :(得分:3)

以如此高的速率创建大量大位图通常是一种糟糕的方法。我建议你创建一次缓存位图来减少内存压力。您还可以使用任何GDI函数直接复制位图数据。

您的情况下的内存泄漏原因是一个多线程问题。 Windows上的默认计时器分辨率为15.6毫秒。以60Hz(16.67ms)的速率,几乎同时发射至少两个事件的可能性很高。

很明显,在这种情况下你会遇到竞争条件,所以你的位图不会被正确处理并且会挂在内存中(在终结器队列中)。而且由于您的CPU负载很重,垃圾收集器无法有效地摆脱僵尸对象。

要检查这一点,只需在事件处理程序中设置一个简单的lock部分,这样一次只有一个线程可以访问位图。您会注意到不会再发生内存泄漏。

您当然可以将计时器分辨率更改为例如1毫秒但是,你必须保证你的位图处理将完成,直到下一个事件到来。在.NET世界中很难做到。

另一种解决方案是将System.Timers.Timer更改为System.Windows.Forms.Timer。后者将在GUI线程而不是线程池线程上引发事件。但您必须记住,所有处理都将在GUI线程上完成,使您的用户界面无响应。

也许最好的解决方案是实施一些“丢帧”'行为。如果帧在前一个帧仍在处理时到达,则只需删除新帧。您可以使用例如Semaphore

@ BradleyUffner建议的另一个解决方案是使计时器不可重复,并在处理完帧后启用下一个事件处理程序。

答案 1 :(得分:2)

Timer.Elapsed事件在线程池线程上运行。这是一个问题,Image.Dispose()方法和PictureBox.Image属性都不是线程安全的。 Winforms有一个启发式方法,当你弄错了它时会抛出一个InvalidOperationException,但是它无法为这两个成员检测到它。

当Elapsed事件处理程序在UI线程忙于重新绘制Image的确切时间调用Dispose()时,会发生失败模式。这似乎在Bitmap类中触发了未定义的行为,我只能看到Dispose()调用没有任何影响。 GDI对象计数器勾选(在任务管理器中可见)并且相应地增加了内存使用量。只是偶尔会触发一个在其他地方使用的硬对象"例外。您无法轻易看到的异常,因为PictureBox.OnPaint()和Timer.OnElapsed()都有try / catch-em-all语句。

将SizeMode属性更改为Stretch仅具有辅助效果,它会导致Paint事件花费更长时间,因为它需要更加努力地调整图像大小。因此,它增加了油漆和处理同时发生的可能性。计时器的间隔值也有很大的影响,它得到的越低,当tp线程调用Dispose()时,涂料没有完成的几率越高。

与往常一样,没有Santa Clause,使用线程不安全对象进行线程确实会导致麻烦。您必须正确执行此操作:

   this.BeginInvoke(new Action(() => {
        using (Image prev = pictureBox1.Image) {
            pictureBox1.Image = bmp;
        }
    }));

附加要求您将计时器的AutoReset属性更改为 false ,这样就不会导致在上一次Elapsed处理程序调用之前计时器停止时发生的另一个线程错误完成了。虽然你通常会注意到这一点,但是当它无法跟上时,所有这些调用都会让用户体验变得紧张。什么东西很容易发生在慢机上btw。只有同步计时器(工具箱中的计时器)才能确保永远不会发生。哪个是最好的建议。