ich arbeite seit einiger Zeit mit WPF/Vb.NET und nutze hierfür das .NET Framework 4.0. Beim Arbeiten mit dem WPF TreeView habe ich des öfteren Probleme mit dem reservierten Speicher, der teilweise exorbitant ansteigt. Ich arbeite mit Databinding und HierarchicalDataTemplates um Daten darzustellen. Nach dem Durcharbeiten einiger interessanter Artikel (z.B. diesem hier) vermutete ich nun, dass die Datenbindung das Problem ist.
Also habe ich mir eine kleine Testanwendung geschrieben, um dem Problem weiter auf den Grund zu gehen. Zunächst habe ich erst einmal keinerlei Data Binding verwendet. Ich habe ein Window erstellt und diesem ein TreeView hinzugefügt:
Im Loaded-Event des Fensters rufe ich nun folgende Methode auf:
Private Sub TreeViewWindow_Loaded() LoadDummies(Nothing, 0) End Sub
Private Sub LoadDummies(ByRef Parent As TreeViewItem, ByVal Ebene As Integer) If Ebene = 4 Then Exit Sub
If Parent Is Nothing Then For i = 0 To 10 Dim p As New TreeViewItem p.Header = "Dummy Ebene " & Ebene MyTreeView.Items.Add(p) LoadDummies(p, Ebene + 1) Next Else For i = 0 To 10 Dim p As New TreeViewItem p.Header = "Dummy Ebene " & Ebene Parent.Items.Add(p) LoadDummies(p, Ebene + 1) Next End If End Sub
Kurzum: Ich füge dem TreeView also ein paar tausend TreeViewItems hinzu.
Dieses obige Fenster rufe ich dann von meinem Hauptfenster aus 20 Mal in einer Schleife auf und schließe es gleich wieder:
Private Sub Button1_Click() For i = 0 To 20 Dim wnd As New TreeViewWindow() wnd.Show() wnd.Close() Next End Sub
Der verbrauchte Arbeitsspeicher rauscht in dieser Zeit von ca. 25.000 K auf ca. 750.000 K hoch (Quelle: Taskmanager und GC.GetTotalMemory(true)).
Nun endlich zu meiner Frage: Ich bin eigentlich der Meinung, dass die Garbage Collection nach dem Schließen des Fensters den Speicher wenigstens annähernd vollständig bereinigen sollte. Das tut sie allerdings überhaupt nicht. Wo liegt also mein Denkfehler? Ich habe auch versucht in meinem Window IDisposable zu implementieren und das TreeView = Nothing zu setzen. Allerdings Fehlanzeige. Keine Änderung. Eine Verbesserung habe ich nur erreicht, indem ich in der Dispose()-Methode alle TreeViewItems durchlaufen habe und aus jedem TreeViewItem die Child-Items manuell gelöscht habe. Aber das kann ja nich der Weißheit letzter Schluss sein...
Ich hoffe jemand kann mir hier weiterhelfen bzw. mir das Verhalten erklären. Besten Dank einstweilen.
Irgendwo hab ich mal gelesen, dass Phänomene (ähnlich denen, die Du schilderst), in diesem Zusammenhang auftreten, wenn man an eine List statt an eine ObservableCollection bindet.
Du könntest außerdem in Deiner TestApp noch ein GC.Collect() einbauen, um den GC selbst zu triggern. Auch wenn man diese Methode selbst eigentlich nicht aufrufen soll, gibt es Ausnahmen. Und diese sind GROSSE Objekte, und Dein TreeView ist letzten Endes ein solches.
Wenn das alles nicht weiterhilft, dann rate ich Dir, einen anständigen Profiler anzuschaffen und das Problem damit näher zu analysieren. Mich begleitet seit Jahren der ANTS Memory Profiler (bzw. alle Profiling Produkte von ANTS, die sind einfach spitze).
Bevor Dich das Problem (im kommerziellen Umfeld) aber zuviel kostet, erwäge noch, auf einen Komponentenhersteller zu setzen, der ein TreeView anbietet (Telerik, DevExpress, C1, Infragsitics, ...). Mit den Trials kannst Du Deinen UseCase schnell testen...
Den ANTS Memory Profiler kann ich auch nur empfehlen. Davon gibt es auch eine Testversion. Damit habe ich mein letztes Memory Leak finden können (und dann auch eine Lizenz gekauft).
Danke für Deine Hinweise. Es ist schon traurig, dass die GC diese einfache Aufgabe nicht hinbekommt. Mag ja sein, dass es einige tausend Items sind. Aber es handelt sich hier um Standard-Controls der WPF ohne Data Binding etc. Wie einfach kann man's denn noch machen? Ist ja schön, dass man sich um die Speicherverwaltung nicht mehr selbst kümmern muss, aber so...
Nun, ich habe noch ein wenig probiert und festgestellt, dass der verwendete Memory ab einem gewissen Level konstant bleibt, wenn ich das Fenster nicht mit Window.Show, sondern mit ShowDialog aufrufe und das Fenster immer manuell schließe. Kann es sein, dass die GC immer ein bisschen Verschnaufpause benötigt? Jedoch kehrt der Speicher nie in seinen ursprünglichen Zustand zurück.
Ich bin Deinen Hinweisen nachgegangen. Der Hotfix hat leider nichts bewirkt und der Aufruf von GC.Collect nach jedem Fensteraufruf auch nicht.
Ich werde mir dann mal ANTS Memory Profiler ansehen. Ich habe bisher noch nicht mit einem solchen Tool gearbeitet. Wie könnte denn das Ergebnis in oben genannten Fall aussehen? Werden von dem Tool auch hinweise zur Codebereinigung gegeben (vermutlich nicht).
Welche Vorteile könnten die Steuerelemente von Komponentenanbietern bieten? Bauen diese nicht auch auf den grundlegenden WPF-Steuerelementen auf, oder ist es so, dass das Verhalten komplett anders sein kann? Ich setze zwar bisher auch einige Komponenten von Drittherstellern ein (Report-Generator und andere), aber z.B. ein TreeView eines Drittherstellers zu verwenden hielt ich bisher für überflüssig. Wie sind Eure Erfahrungen?
3rdParty Komponentenherst. bauen IMHO nach nicht auf den Standard Controls auf, sondern implementieren die Controls selbst. Die Vorteile ihrere Controls preisen diese selbst auf ihren Websites zu Genüge an (dafür mache ich keine Werbung). Unabh. davon könntest Du auch versuchen, die darzustellenden Daten in der UI möglichst gering zu halten. Stichwort: Data Virtualization. Der TreeView legt für jedes Datum, dass Du hinzufügst eine Node an. Wenn Du x Tausend Nodes anlegst, summiert sich das (merklich). Wenn Du aber nur die darzustellenden Nodes im Speicher hast u. dich der anderen entledigst...