¿Cómo evitar la pérdida de memoria en el control .NET Webbrowser?

Este es un viejo problema ampliamente conocido con el control .NET Webbrowser.

Resumen: Tener el control del navegador web .NET Navegar a una página aumenta el uso de la memoria que nunca se libera.

Reproducir la pérdida de memoria: agregue un control WebBrowser a un formulario. Úselo para navegar a las páginas que desee. sobre: ​​trabajos en blanco, desplazarse hacia abajo en Google Imágenes hasta que su uso sea de 100MB + y luego buscar en otro lado para notar que apenas se libera parte de esa memoria es una demostración más dramática.

Mis requisitos actuales para una aplicación incluyen ejecutarlo durante largos periodos de tiempo, mostrando una ventana de navegador IE7 limitada. No se desea ejecutar IE7 con una configuración bastarda de ganchos, BHO y políticas de grupo, aunque parece ser la alternativa en este momento. Incrustar un navegador en una aplicación de Windows Forms es. Usar una base de navegador diferente no es una opción disponible para mí. IE7 es obligatorio.

Hilos y artículos anteriores relacionados con esta pérdida de memoria conocida:

  • http://www.vbforums.com/showthread.php?t=644658
  • Cómo arreglar la fuga de memoria en IE WebBrowser Control?
  • Fuga de memoria cuando se utiliza el control WPF WebBrowser en varias ventanas
  • http://social.msdn.microsoft.com/Forums/en-US/ieextensiondevelopment/thread/88c21427-e765-46e8-833d-6021ef79e0c8/
  • http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/8a2efea4-5e75-4e3d-856f-b09a4e215ede
  • http://dotnetforum.net/topic/17400-appdomain-webbrowser-memory-leak/

Soluciones a menudo propuestas que NO FUNCIONAN:

  • Ir a diferentes páginas no importa. about: en blanco desencadena la fuga. No requiere una página para tener javascript o cualquier otra tecnología adicional.
  • Usar diferentes versiones de Internet Explorer no importa. 7, 8 y 9 muestran todos los mismos síntomas, y por lo que he escuchado, todas las versiones tienen la misma pérdida de memoria en el control.
  • Dispose () ing del control no ayuda.
  • Garbage Collecting no ayuda. (De hecho, la investigación que he hecho sobre esto indica que la fuga está en código COM no administrado que el control de Webbrowswer ajusta).
  • Minimizar y configurar el proceso de memoria disponible en -1, -1 (SetProcessWorkingSetSize () o simimlar) solo reduce el uso de la memoria física, no tiene impacto en la memoria virtual.
  • Llamar a WebBrowser.Stop () no es una solución, y rompe la funcionalidad para usar cualquier cosa que no sean páginas web estáticas, sin hacer más que simplemente minimizar la fuga.
  • Obligar a esperar que un documento se cargue por completo antes de navegar a otro también no ayuda.
  • Cargando el control en una aplicación separadaDominio no soluciona el problema. (No he hecho esto yo mismo, pero la investigación muestra que otros no tienen éxito con esta ruta).
  • Usar una envoltura diferente como csexwb2 no ayuda, ya que esto también tiene el mismo problema.
  • La eliminación de la memoria caché temporal de archivos de Internet no hace nada. El problema está en la memoria activa, no en el disco.

La memoria se borra cuando la aplicación completa se cierra y se reinicia.

Estoy dispuesto a escribir mi propio control de navegador en la API COM o Windows directamente, si eso es una solución segura para el problema. Por supuesto, preferiría una solución menos complicada; Prefiero evitar bajar a niveles inferiores para hacer cosas, porque no quiero reinventar la rueda en términos de funciones compatibles de un navegador. Letalone duplica las características de IE7 y los comportamientos no estándar en un navegador de estilo propio.

¿Ayuda?

Esta fuga parece ser una fuga en la memoria no administrada, por lo que nada de lo que hagas en tu proceso va a reclamar esa memoria. De su publicación, puedo ver que ha intentado evitar la fuga bastante extensamente y sin éxito.

Sugeriría un enfoque diferente si es factible. Cree una aplicación separada que use el control del navegador web y comience desde su aplicación. Utilice el método descrito aquí para incrustar la aplicación recién creada dentro de su propia aplicación existente. Comuníquese con esa aplicación usando WCF o .NET remoto. Reinicie el proceso secundario de vez en cuando para evitar que consum mucha memoria.

Esto, por supuesto, es una solución bastante complicada y el proceso de reinicio probablemente se vea feo. Tal vez pueda recurrir a reiniciar toda la aplicación del navegador cada vez que el usuario navega a otra página.

Tomé el código de udione (funcionó para mí, ¡gracias!) Y cambié dos cosas pequeñas:

  1. IKeyboardInputSite es una interfaz pública y tiene el método Unregister () , por lo que no es necesario utilizar el reflection después de recibir una referencia a la colección * _keyboardInputSinkChildren *.

  2. Como la vista no siempre tiene una referencia directa a su clase de ventana (especialmente en MVVM) agregué un método GetWindowElement (elemento DependencyObject) que devuelve la referencia requerida atravesando el árbol visual.

Gracias, udione

 public void Dispose() { _browser.Dispose(); var window = GetWindowElement(_browser); if (window == null) return; var field = typeof(Window).GetField("_swh", BindingFlags.NonPublic | BindingFlags.Instance); var valueSwh = field.GetValue(window); var valueSourceWindow = valueSwh.GetType().GetField("_sourceWindow", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(valueSwh); var valuekeyboardInput = valueSourceWindow.GetType().GetField("_keyboardInputSinkChildren", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(valueSourceWindow); var inputSites = valuekeyboardInput as IEnumerable; if (inputSites == null) return; var currentSite = inputSites.FirstOrDefault(s => ReferenceEquals(s.Sink, _browser)); if (currentSite != null) currentSite.Unregister(); } private static Window GetWindowElement(DependencyObject element) { while (element != null && !(element is Window)) { element = VisualTreeHelper.GetParent(element); } return element as Window; } 

¡Gracias a todos!

Hay una forma de hacer claras pérdidas de memoria mediante el uso de la reflexión y la eliminación de referencias de campos privados en mainForm. Esta no es una buena solución, pero para la gente desesperada, aquí está el código:

 //dispose to clear most of the references this.webbrowser.Dispose(); BindingOperations.ClearAllBindings(this.webbrowser); //using reflection to remove one reference that was not removed with the dispose var field = typeof(System.Windows.Window).GetField("_swh", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var valueSwh = field.GetValue(mainwindow); var valueSourceWindow = valueSwh.GetType().GetField("_sourceWindow", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(valueSwh); var valuekeyboardInput = valueSourceWindow.GetType().GetField("_keyboardInputSinkChildren", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(valueSourceWindow); System.Collections.IList ilist = valuekeyboardInput as System.Collections.IList; lock(ilist) { for (int i = ilist.Count-1; i >= 0; i--) { var entry = ilist[i]; var sinkObject = entry.GetType().GetField("_sink", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (object.ReferenceEquals(sinkObject.GetValue(entry), this.webbrowser.webBrowser)) { ilist.Remove(entry); } } } 

Habiendo combatido este problema exacto de Out of Memory desde diferentes direcciones ( interfaces Win32 WorkingSet / COM SHDocVw, etc. ) con el componente WPF WebBrowser , descubrí que el problema era que el plugin jqGrid se aferraba a recursos no administrados en IE ActiveXHost y no los liberaba después llamando a WebBrowser.Dispose() . Este problema a menudo es creado por Javascript que no se comporta correctamente. Lo curioso es que el Javascript funciona bien en IE normal, pero no desde el control WebBrowser . Supongo que la recolección de basura es diferente entre los dos puntos de integración, ya que IE nunca puede cerrarse.

Una cosa que sugeriría si está escribiendo las páginas de origen es eliminar todos los componentes de JS y volver a agregarlos lentamente. Una vez que identifique el complemento JS ofensivo ( como hicimos nosotros ), debería ser fácil solucionar el problema. En nuestro caso, simplemente usamos $("#jqgrid").jqGrid('GridDestroy') para eliminar correctamente los eventos y los elementos DOM asociados que creó. Esto nos ocupó del problema al invocar esto cuando el navegador se cierra a través de WebBrowser.InvokeScript .

Si no tiene la capacidad de modificar las páginas de origen a las que navega, deberá inyectar algunas JS en la página para limpiar los eventos DOM y los elementos que están perdiendo memoria. Sería bueno si Microsoft encontrara una resolución para esto, pero por ahora nos queda investigar los complementos de JS que necesitan ser limpiados .

La siguiente solución funcionó para mí:

 Protected Sub disposeBrowers() If debug Then debugTrace() If Me.InvokeRequired Then Me.Invoke(New simple(AddressOf disposeBrowers)) Else Dim webCliffNavigate As String = webCliff.Url.AbsoluteUri Me.DollarLogoutSub() If dollarLoggedIn Then Exit Sub End If 'Dim webdollarNavigate As String = webDollar.Url.AbsoluteUri Me.splContainerMain.SuspendLayout() Me.splCliffDwellers.Panel2.Controls.Remove(webCliff) Me.splDollars.Panel2.Controls.Remove(webDollar) RemoveHandler webCliff.DocumentCompleted, AddressOf webCliff_DocumentCompleted RemoveHandler webDollar.DocumentCompleted, AddressOf webDollar_DocumentCompleted RemoveHandler webCliff.GotFocus, AddressOf setDisposeEvent RemoveHandler webCliff.LostFocus, AddressOf setDisposeEvent RemoveHandler webDollar.GotFocus, AddressOf setDisposeEvent RemoveHandler webDollar.LostFocus, AddressOf setDisposeEvent webCliff.Stop() webDollar.Stop() Dim tmpWeb As SHDocVw.WebBrowser = webCliff.ActiveXInstance System.Runtime.InteropServices.Marshal.ReleaseComObject(tmpWeb) webCliff.Dispose() tmpWeb = webDollar.ActiveXInstance System.Runtime.InteropServices.Marshal.ReleaseComObject(tmpWeb) webDollar.Dispose() tmpWeb = Nothing webCliff = Nothing webDollar = Nothing GC.AddMemoryPressure(50000) GC.Collect() GC.WaitForPendingFinalizers() GC.Collect() GC.WaitForFullGCComplete() GC.Collect() GC.RemoveMemoryPressure(50000) webCliff = New WebBrowser() webDollar = New WebBrowser() webCliff.CausesValidation = False webCliff.Dock = DockStyle.Fill webDollar.CausesValidation = webCliff.CausesValidation webDollar.Dock = webCliff.Dock webDollar.ScriptErrorsSuppressed = True webDollar.Visible = True webCliff.Visible = True Me.splCliffDwellers.Panel2.Controls.Add(webCliff) Me.splDollars.Panel2.Controls.Add(webDollar) Me.splContainerMain.ResumeLayout() 'AddHandler webCliff.DocumentCompleted, AddressOf webCliff_DocumentCompleted 'AddHandler webDollar.DocumentCompleted, AddressOf webDollar_DocumentCompleted 'AddHandler webCliff.GotFocus, AddressOf setDisposeEvent 'AddHandler webCliff.LostFocus, AddressOf setDisposeEvent 'AddHandler webDollar.GotFocus, AddressOf setDisposeEvent 'AddHandler webDollar.LostFocus, AddressOf setDisposeEvent webCliff.Navigate(webCliffNavigate) disposeOfBrowsers = Now.AddMinutes(20) End If End Sub 

La mejor de las suertes, Layla

Creo que esta pregunta ha quedado sin respuesta durante mucho tiempo. Tantos hilos con la misma pregunta pero sin respuesta concluyente.

He encontrado una solución a este problema y quería compartir con ustedes todos los que aún enfrentan este problema.

Paso 1: cree un nuevo formulario, digamos form2 y agregue un control de navegador web en él. Paso 2: en el formulario 1 donde tienes el control de tu navegador web, solo quítalo. Paso 3: Ahora, vaya a Form2 y haga que el modificador de acceso para este control webbrowser sea público para que se pueda acceder en Form1 paso4: Cree un panel en form1 y cree el objeto de form2 y agréguelo al panel. Form2 frm = new Form2 (); frm.TopLevel = falso; frm.Show (); panel1.Controls.Add (frm); paso5: Llame al código siguiente a intervalos regulares frm.Controls.Remove (frm.webBrowser1); frm.Dispose ();

Eso es. Ahora cuando lo ejecutas, puedes ver que el control del webbrowser está cargado y se eliminará a intervalos regulares y ya no habrá más aplicaciones colgadas.

Puede agregar el código siguiente para hacerlo más eficiente.

  IntPtr pHandle = GetCurrentProcess(); SetProcessWorkingSetSize(pHandle, -1, -1); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); 

Creo que es más un problema de .NET Framework que de navegador web. Conectarse al evento navegado del navegador con un controlador que dispondrá del navegador y luego navegará a aproximadamente: en blanco será una buena solución. Por ejemplo:

 private void RemoveButton_Click(object sender, RoutedEventArgs e) { var browser = (WebBrowser) _stackPanel.Children[_stackPanel.Children.Count - 1]; _stackPanel.Children.RemoveAt(_stackPanel.Children.Count-1); NavigatedEventHandler dispose = null; dispose = (o, args) => { browser.Navigated -= dispose; browser.Dispose(); }; browser.Navigated += dispose; browser.Navigate(new Uri("about:blank")); } 

Prueba esta solución, sé que no es ideal. Pegue el código después de cada carga de página

 System.Diagnostics.Process loProcess = System.Diagnostics.Process.GetCurrentProcess(); try { loProcess.MaxWorkingSet = (IntPtr)((int)loProcess.MaxWorkingSet - 1); loProcess.MinWorkingSet = (IntPtr)((int)loProcess.MinWorkingSet - 1); } catch (System.Exception) { loProcess.MaxWorkingSet = (IntPtr)((int)1413120); loProcess.MinWorkingSet = (IntPtr)((int)204800); }