2017-10-20 4 views
1

Ich weiß, diese Frage vor gefragt wurde, aber meine Frage bezieht sich direkt ‚Stretch‘ (oder etwas anderes als ‚Normal‘) mit dem picturebox zu verwenden, ‚Normal‘ funktioniert perfekt.C# Picturebox Strecke Speicherprobleme

Ich habe eine einfache Anwendung, die im Wesentlichen einem Screenshot 60 mal pro Sekunde _timer.Interval = (1.0/60.0) * 1000.0; mit Timer nimmt. Unglücklicherweise verursacht die Verwendung von "Stretch" mehr und mehr Speicher, der schließlich die Anwendung zum Absturz bringt.

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; 
      } 
     } 
    } 
} 

Wie Sie sehen können, ich entsorgen beiden Grafiken IDisposable und dem vorhergehenden Bild von der picturebox verwendet. Der Speicher erhöht sich jedoch immer noch.

Gibt es etwas, was ich falsch mache?

+1

Wo ist 'bmp' angeordnet? –

+0

Wenn ich versuche, bmp zu disponentieren, wird eine Ausnahme für 'ungültiger Parameter' auf .CopyFromScreen verursacht. – Daniel

+0

Die Bitmap muss entsorgt werden - Sie könnten versuchen, einen anderen Verweis auf die Bitmap zu erhalten, kurz bevor Sie pbCapture.Image zuweisen und dann löschen. Ich sehe @PatrickArtner mich schlagen, dies zu sagen. – PaulF

Antwort

3

Das Erstellen großer Mengen großer Bitmaps mit einer so hohen Rate ist im Allgemeinen ein schlechter Ansatz. Ich würde vorschlagen, dass Sie einmal eine Cache-Bitmap erstellen, um den Speicherdruck zu reduzieren. Sie können auch alle GDI-Funktionen verwenden, um die Bitmap-Daten direkt zu kopieren.

Der Speicherleckgrund ist in Ihrem Fall ein Multithreading-Problem. Die Standard-Zeitauflösung für Windows beträgt 15,6 ms. Bei einer Frequenz von 60 Hz (16,67 ms) besteht eine hohe Wahrscheinlichkeit, dass mindestens zwei Ereignisse fast gleichzeitig ausgelöst werden.

Es ist offensichtlich, dass Sie in einem solchen Fall eine Race-Bedingung erhalten werden, so dass Ihre Bitmaps nicht ordnungsgemäß entsorgt werden und wird im Speicher (in der Finalizer-Warteschlange) hängen. Und da Sie Ihre CPU stark belasten, kann der Garbage Collector die Zombie-Objekte nicht effektiv entfernen.

Um das zu überprüfen, Setup nur einen einfachen lock Abschnitt in Ihren Event-Handler so dass nur ein Thread die Bitmap zu einem Zeitpunkt zugreifen kann. Sie werden feststellen, dass kein Speicherleck mehr auftritt.

Sie könnten natürlich die Timer-Auflösung zu z. 1 ms. Aber dann müssen Sie garantieren, dass Ihre Bitmap-Verarbeitung abgeschlossen ist, bis das nächste Ereignis eintrifft. In der .NET-Welt ist das schwierig. Eine andere Lösung ist, die System.Timers.Timer zu System.Windows.Forms.Timer zu ändern. Der letztere wird Ereignisse auf dem GUI-Thread anstelle der Thread-Pool-Threads auslösen. Sie müssen jedoch bedenken, dass die gesamte Verarbeitung im GUI-Thread erfolgt, sodass Ihre Benutzeroberfläche nicht mehr reagiert.

Vielleicht ist die beste Lösung, ein Drop-Frame-Verhalten zu implementieren. Wenn ein Frame ankommt, während der vorherige noch bearbeitet wird, lassen Sie einfach den neuen Frame fallen. Sie können dies mit z.B. a Semaphore.

Eine andere Lösung, wie von @ BradleyUffner vorgeschlagen, wird der Timer nicht wiederholbar zu machen und das nächste Event-Handler, nachdem ein Rahmen verarbeitet wurde zu ermöglichen.

+2

Eine andere Möglichkeit besteht darin, den Timer nicht automatisch wiederholen zu lassen. Legen Sie fest, dass es einmal ausgelöst wird, verarbeiten Sie die Bitmap und * dann * aktualisieren Sie den Timer, um erneut zu feuern. Dies verhindert, dass sie sich überschneiden und ermöglicht es ihnen, mit der maximalen Geschwindigkeit für die zu verarbeitende Arbeitslast zu arbeiten. –

2

Das Timer.Elapsed-Ereignis wird in einem Threadpool-Thread ausgeführt. Das ist ein Problem, weder die Image.Dispose() - Methode noch die PictureBox.Image-Eigenschaft sind Thread-sicher. Winforms hat eine Heuristik, um eine InvalidOperationException zu werfen, wenn Sie dies falsch erhalten, aber es ist nicht in der Lage, dies für jedes Mitglied zu erkennen.

Der Fehlermodus tritt auf, wenn die verstrichene Ereignishandler ruft Dispose() genau an der Zeit, dass der UI-Thread beschäftigt ist, das Bild neu zu streichen. Dies scheint ein undefiniertes Verhalten in der Bitmap-Klasse auszulösen. Ich kann nur sehen, dass der Dispose() -Aufruf keinerlei Auswirkungen hat. Der GDI-Objekt-Zähler wird aktiviert (im Task-Manager sichtbar) und entsprechend erhöht sich die Speichernutzung.Nur sehr selten löst es eine harte Ausnahme "Object in use woanders" aus. Eine Ausnahme, die Sie nicht leicht sehen können, da sowohl PictureBox.OnPaint() als auch Timer.OnElapsed() try/catch-em-all-Anweisungen haben.

Das Ändern der SizeMode-Eigenschaft in "Stretch" wirkt sich nur sekundär aus. Das Paint-Ereignis dauert länger, da die Größe des Bildes geändert werden muss. Es erhöht also die Wahrscheinlichkeit, dass Farbe und Entsorgung gleichzeitig auftreten. Der Intervall-Wert des Timers hat ebenfalls einen sehr großen Effekt: je niedriger er wird, desto höher ist die Wahrscheinlichkeit, dass die Farbe noch nicht vollständig ist, wenn der tp-Thread Dispose() aufruft.

So wie üblich gibt es keinen Weihnachtsmann, Threading mit unsicheren Objekten führt immer zu Problemen. Sie müssen dies richtig tun:

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

Mit der zusätzlichen Voraussetzung, dass Sie die Autoreset-Eigenschaft auf falsch des Timers ändern, so können Sie nicht einen anderen Threading Fehler verursachen, der auftritt, wenn der Timer vor dem vorherigen Abgelaufene Handler Zecken Aufruf ist abgeschlossen. Auch wenn Sie dies normalerweise früher bemerken, werden alle diese Aufrufe die Benutzeroberfläche katatonisch machen, wenn sie nicht mithalten kann. Etwas, was auf einer langsamen Maschine leicht passieren kann. Nur ein synchroner Timer, der in der Toolbox, kann sicherstellen, dass dies nie passieren wird. Welches ist der beste Rat.