GDI-Leck (oder einfach die Verwendung von zu vielen GDI-Objekten) ist eines der häufigsten Probleme. Es verursacht schließlich Renderprobleme, Fehler und/oder Leistungsprobleme. Der Artikel beschreibt, wie wir dieses Problem beheben.
Im Jahr 2016, wenn die meisten Programme in Sandboxen ausgeführt werden, wodurch selbst der inkompetenteste Entwickler dem System keinen Schaden zufügen kann, bin ich erstaunt über das Problem, über das ich in diesem Artikel sprechen werde. Ehrlich gesagt hoffte ich, dass dieses Problem zusammen mit Win32Api für immer verschwunden wäre. Trotzdem habe ich mich damit auseinandergesetzt. Davor habe ich von erfahrenen Entwicklern nur Horrorgeschichten darüber gehört.
Das Problem
Leck oder Nutzung der enormen Menge an GDI-Objekten.
Symptome
- Die Spalte GDI-Objekte auf der Registerkarte Details des Task-Managers zeigt kritisch 10000 (wenn diese Spalte nicht vorhanden ist, können Sie sie hinzufügen, indem Sie mit der rechten Maustaste auf den Tabellenkopf klicken und Spalten auswählen auswählen).
- Beim Entwickeln in C# oder in anderen Sprachen, die von CLR ausgeführt werden, tritt der folgende wenig informative Fehler auf:
Meldung:In GDI+ ist ein allgemeiner Fehler aufgetreten.
Quelle:System.Drawing
TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
Typ:System.Runtime.InteropServices.ExternalException
Der Fehler tritt möglicherweise bei bestimmten Einstellungen oder in bestimmten Systemversionen nicht auf, aber Ihre Anwendung kann kein einzelnes Objekt darstellen: - Während der Entwicklung in С/С++ begannen alle GDI-Methoden, wie Create%SOME_GDI_OBJECT%, NULL zurückzugeben.
Warum?
Windows-Systeme erlauben nicht das Erstellen von mehr als 65535 GDI-Objekte. Diese Zahl ist in der Tat beeindruckend und ich kann mir kaum vorstellen, dass ein normales Szenario eine so große Menge an Objekten erfordert. Es gibt eine Begrenzung für Prozesse – 10.000 pro Prozess, der geändert werden kann (durch Ändern von HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota Wert im Bereich von 256 bis 65535), aber Microsoft empfiehlt nicht, diese Einschränkung zu erhöhen. Wenn Sie es dennoch tun, kann ein Prozess das System einfrieren, so dass es nicht einmal die Fehlermeldung ausgeben kann. In diesem Fall kann das System erst nach einem Neustart wiederbelebt werden.
Wie beheben?
Wenn Sie in einer komfortablen und verwalteten CLR-Welt leben, besteht eine hohe Wahrscheinlichkeit, dass Ihre Anwendung ein übliches Speicherleck aufweist. Das Problem ist unangenehm, aber es ist ein ganz gewöhnlicher Fall. Es gibt mindestens ein Dutzend großartiger Tools, um dies zu erkennen. Sie müssen einen beliebigen Profiler verwenden, um anzuzeigen, ob die Anzahl der Objekte, die GDI-Ressourcen umschließen (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics), zunimmt. Wenn dies der Fall ist, können Sie aufhören, diesen Artikel zu lesen. Wenn das Leck von Wrapper-Objekten nicht erkannt wurde, verwendet Ihr Code direkt die GDI-API und es gibt ein Szenario, in dem sie nicht gelöscht werden
Was empfehlen andere?
Der offizielle Microsoft-Leitfaden oder andere Artikel zu diesem Thema empfehlen Ihnen Folgendes:
Finden Sie alle Erstellen %SOME_GDI_OBJECT% und erkennen, ob das entsprechende DeleteObject (oder ReleaseDC für HDC-Objekte) existiert. Wenn ein solches DeleteObject existiert, kann es ein Szenario geben, das es nicht aufruft.
Es gibt eine leicht verbesserte Version dieser Methode, die einen zusätzlichen Schritt enthält:
Laden Sie das GDIView-Dienstprogramm herunter. Es kann die genaue Anzahl von GDI-Objekten nach Typ anzeigen. Beachten Sie, dass die Gesamtzahl der Objekte nicht dem Wert in der letzten Spalte entspricht. Aber wir können die Augen davor verschließen, wenn es hilft, das Suchfeld einzugrenzen.
Das Projekt, an dem ich arbeite, hat eine Codebasis von 9 Millionen Datensätzen, ungefähr die gleiche Menge an Datensätzen befindet sich in den Bibliotheken von Drittanbietern, Hunderte von Aufrufen der GDI-Funktion, die über Dutzende von Dateien verteilt sind. Ich hatte viel Zeit und Energie verschwendet, bevor ich begriff, dass eine manuelle Analyse ohne Fehler unmöglich ist.
Was kann ich anbieten?
Wenn Ihnen diese Methode zu langwierig und lästig erscheint, haben Sie mit der vorherigen nicht alle Phasen der Verzweiflung überstanden. Sie können versuchen, die vorherigen Schritte zu befolgen, aber wenn es nicht hilft, vergessen Sie diese Lösung nicht.
Bei der Suche nach dem Leck stellte ich mir die Frage:Wo werden die undichten Objekte erstellt? Es war unmöglich, an allen Stellen, an denen die API-Funktion aufgerufen wird, Breakpoints zu setzen. Außerdem war ich mir nicht sicher, ob es nicht im .NET Framework oder in einer der von uns verwendeten Bibliotheken von Drittanbietern passiert. Ein paar Minuten Googeln führten mich zum API-Monitor-Dienstprogramm, mit dem Aufrufe aller Systemfunktionen protokolliert und verfolgt werden konnten. Ich habe die Liste aller Funktionen, die GDI-Objekte generieren, leicht gefunden, gefunden und in API Monitor ausgewählt. Dann setze ich Breakpoints.
Danach habe ich den Debugging-Prozess ausgeführt Visual Studio und wählte es in der Struktur „Prozesse“ aus. Der fünfte Haltepunkt hat sofort geklappt:
Mir wurde klar, dass ich in diesem Strom ertrinken würde und dass ich etwas anderes brauchte. Ich habe Haltepunkte aus Funktionen gelöscht und mich entschieden, das Protokoll anzuzeigen. Es zeigte Tausende von Anrufen. Es wurde klar, dass ich sie nicht manuell analysieren kann.
Die Aufgabe besteht darin, die Aufrufe der GDI-Funktionen zu finden, die nicht zum Löschen führen . Das Protokoll enthielt alles, was ich brauchte:die Liste der Funktionsaufrufe in chronologischer Reihenfolge, ihre zurückgegebenen Werte und Parameter. Daher musste ich einen zurückgegebenen Wert der Funktion Create%SOME_GDI_OBJECT% erhalten und den Aufruf von DeleteObject mit diesem Wert als Argument finden. Ich habe alle Datensätze im API Monitor ausgewählt, in eine Textdatei eingefügt und so etwas wie CSV mit dem TAB-Trennzeichen erhalten. Ich habe VS ausgeführt, wo ich vorhatte, ein kleines Programm zum Parsen zu schreiben, aber bevor es geladen werden konnte, kam mir eine bessere Idee:Daten in eine Datenbank zu exportieren und eine Abfrage zu schreiben, um das zu finden, was ich brauche. Es war die richtige Wahl, da ich schnell Fragen stellen und Antworten erhalten konnte.
Es gibt viele Tools zum Importieren von Daten aus CSV in eine Datenbank, daher werde ich nicht weiter auf dieses Thema eingehen (mysql, mssql, sqlite).
Ich habe die folgende Tabelle:
CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
Ich habe die folgende MySQL-Funktion geschrieben, um den Deskriptor des gelöschten Objekts aus dem API-Aufruf abzurufen:
CREATE FUNCTION getHandle(api varchar(1000)) RETURNS varchar(100) CHARSET utf8 BEGIN DECLARE start int(11); DECLARE result varchar(100); SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )' IF start = 0 THEN SET start := INSTR(api, '('); END IF; SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1); RETURN TRIM(result); END
Und schließlich habe ich eine Abfrage geschrieben, um alle aktuellen Objekte zu finden:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates LEFT JOIN (SELECT d.id, d.API, getHandle(d.API) handle FROM apicalls d WHERE API LIKE 'DeleteObject%' OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels ON dels.handle = creates.handle WHERE creates.API LIKE 'Create%';
(Im Grunde findet es einfach alle Delete-Aufrufe für alle Create-Aufrufe).
Wie Sie im obigen Bild sehen, wurden alle Aufrufe ohne ein einziges Löschen auf einmal gefunden.
Bleibt noch die letzte Frage:Wie kann ich feststellen, woher diese Methoden im Kontext meines Codes aufgerufen werden? Und hier hat mir ein raffinierter Trick geholfen:
- Führen Sie die Anwendung zum Debuggen in VS aus
- Finden Sie es in Api Monitor und wählen Sie es aus.
- Wählen Sie eine erforderliche Funktion in der API aus und platzieren Sie einen Haltepunkt.
- Klicken Sie weiter auf „Weiter“, bis es mit den fraglichen Parametern aufgerufen wird (ich habe bedingte Haltepunkte von VS wirklich vermisst)
- Wenn Sie zum gewünschten Anruf kommen, wechseln Sie zu CS und klicken Sie auf Alle unterbrechen .
- VS Debugger wird direkt an der Stelle angehalten, an der das undichte Objekt erstellt wurde, und Sie müssen lediglich herausfinden, warum es nicht gelöscht wurde.
Hinweis:Der Code dient der Veranschaulichung.
Zusammenfassung:
Der beschriebene Algorithmus ist kompliziert und erfordert viele Tools, aber er lieferte das Ergebnis viel schneller im Vergleich zu einer dummen Suche durch die riesige Codebasis.
Hier ist eine Zusammenfassung aller Schritte:
- Suche nach Speicherlecks von GDI-Wrapper-Objekten.
- Wenn sie vorhanden sind, entfernen Sie sie und wiederholen Sie Schritt 1.
- Wenn es keine Lecks gibt, suchen Sie explizit nach Aufrufen der API-Funktionen.
- Wenn ihre Menge nicht groß ist, suchen Sie nach einem Skript, in dem ein Objekt nicht gelöscht wird.
- Wenn ihre Menge groß ist oder sie kaum verfolgt werden können, laden Sie API Monitor herunter und richten Sie ihn für die Protokollierung von Aufrufen der GDI-Funktionen ein.
- Führen Sie die Anwendung zum Debuggen in VS aus.
- Reproduzieren Sie das Leck (es wird das Programm initialisieren, um die kassierten Objekte zu verstecken).
- Verbinden Sie sich mit API Monitor.
- Reproduzieren Sie das Leck.
- Kopieren Sie das Protokoll in eine Textdatei und importieren Sie es in eine beliebige Datenbank (die in diesem Artikel vorgestellten Skripte sind für MySQL, können jedoch problemlos für jedes relationale Datenbankverwaltungssystem übernommen werden).
- Vergleichen Sie die Create- und Delete-Methoden (Sie finden das SQL-Skript oben in diesem Artikel) und finden Sie die Methoden ohne die Delete-Aufrufe.
- Setzen Sie einen Haltepunkt im API-Monitor beim Aufruf der erforderlichen Methode.
- Klicken Sie weiter auf Weiter, bis die Methode mit neu erfassten Parametern aufgerufen wird.
- Wenn die Methode mit den erforderlichen Parametern aufgerufen wird, klicken Sie auf Break All in VS.
- Finden Sie heraus, warum dieses Objekt nicht gelöscht wird.
Ich hoffe, dass dieser Artikel nützlich ist und Ihnen hilft, Zeit zu sparen.