2011-11-28 5 views
57

Questo è un problema vecchio e ampiamente noto con il controllo .NET Webbrowser.Come aggirare la perdita di memoria nel controllo .NET Webbrowser?

Riepilogo: Avere il controllo del browser Web .NET Passare a una pagina aumenta l'utilizzo della memoria che non viene mai liberato.

Riprodurre la perdita di memoria: aggiungere un controllo WebBrowser a un modulo. Usalo per navigare verso le pagine che desideri. a proposito di: lavori vuoti, scorrendo verso il basso su Google Immagini fino a quando il tuo utilizzo è di 100 MB + e quindi sfogliare altrove per notare che quasi tutta la memoria viene liberata è una dimostrazione più drammatica.

I miei requisiti attuali per un'applicazione includono l'esecuzione per lunghi periodi di tempo, visualizzando una finestra del browser IE7 limitata. Anche l'esecuzione di IE7 con una configurazione bastarda di hook, BHO e criteri di gruppo non è desiderata, anche se in questo momento sembra che sia il fallback. L'incorporamento di un browser in un'applicazione Windows Form è. L'utilizzo di una diversa base di browser non è un'opzione disponibile per me. È richiesto IE7.

discussioni precedenti ed articoli relativi a questo noto perdita di memoria:

correzioni Spesso-proposte che non funzionano:

  • Andando a diverse pagine non importa. about: blank fa scattare la perdita. Non richiede una pagina per avere javascript o qualsiasi altra tecnologia aggiuntiva.
  • L'utilizzo di versioni diverse di Internet Explorer non è importante. 7, 8 e 9 mostrano tutti gli stessi sintomi e, per quanto ho sentito, tutte le versioni hanno la stessa perdita di memoria nel controllo.
  • L'eliminazione del controllo non è d'aiuto.
  • La raccolta dei rifiuti non aiuta. (In effetti, la ricerca su cui ho fatto riferimento indica che la perdita è nel codice COM non gestito che include il controllo Webbrowswer.)
  • Ridurre al minimo e impostare la memoria disponibile del processo su -1, -1 (SetProcessWorkingSetSize() o simimlar.) Solo riduce l'utilizzo della memoria fisica, non ha alcun impatto sulla memoria virtuale.
  • Chiamare WebBrowser.Stop() non è una soluzione e interrompe la funzionalità per l'utilizzo di qualsiasi cosa tranne pagine Web statiche, senza fare altro che ridurre al minimo la perdita.
  • Forzare l'attesa del caricamento completo di un documento prima che la navigazione in un'altra non sia di aiuto.
  • Caricamento del controllo in un'app separataDomain non risolve il problema. (Non l'ho fatto da solo, ma la ricerca mostra che altri non hanno avuto successo con questa rotta.)
  • L'utilizzo di un wrapper diverso come csexwb2 non aiuta, poiché anche questo ha lo stesso problema.
  • Cancellare la cache dei file temporanei Internet non fa nulla. Il problema è nella memoria attiva, non sul disco.

La memoria viene cancellata quando l'intera applicazione viene chiusa e riavviata.

Sono disposto a scrivere direttamente il mio controllo del browser in API COM o Windows, se questa è una correzione sicura del problema. Certo, preferirei una soluzione meno complicata; Preferisco evitare di scendere ai livelli più bassi per fare le cose, perché non voglio reinventare la ruota in termini di funzionalità supportate di un browser. Letalone duplicazione di funzionalità IE7 e comportamenti non standard in un browser in stile roll-your-own.

Aiuto?

+1

Potrebbe essere possibile avviare la parte del browser in un'altra applicazione e comunicare con esso tramite .Net remote o WCF o qualcosa del genere? È possibile monitorare l'utilizzo della memoria e quindi chiuderla e riavviarla quando raggiunge una determinata soglia. – pstrjds

+0

@pstrjds: Penso che sia l'unica soluzione effettiva che sarà un enorme problema da implementare e limiterà definitivamente la funzionalità. – Juan

+1

È un comune malinteso che il rilascio di memoria ridurrà istantaneamente la dimensione VM di un processo. Non è così che funziona il gestore della memoria di Windows. –

risposta

12

Questa perdita sembra essere una perdita nella memoria non gestita, quindi nulla che si esegue nel processo sta per recuperare quella memoria. Dal tuo post vedo che hai cercato di evitare la fuga abbastanza estensivamente e senza successo.

Suggerirei un approccio diverso se possibile. Creare un'applicazione separata che utilizza il controllo del browser Web e avviarla dall'applicazione. Utilizzare il metodo descritto here per incorporare un'applicazione appena creata nella propria applicazione esistente. Comunicare con l'applicazione tramite WCF o .NET remoting. Riavviare il processo figlio di volta in volta per evitare che assuma molta memoria.

Questo ovviamente è una soluzione piuttosto complicata e il processo di riavvio potrebbe sembrare brutto. Potresti forse ricorrere al riavvio dell'intera applicazione del browser ogni volta che l'utente naviga su un'altra pagina.

+0

@Doc, il controllo 'WebBrowser' fa parte dell'interfaccia utente? O viene nascosto in modo nascosto per l'automazione del web? – Noseratio

+0

@Noseratio Nel mio caso fa parte dell'interfaccia utente. – Daniel

+0

Guardando perfmon ho visto che il browser IE 11 fa qualcosa di simile (probabilmente per motivi diversi). Sembrava creare un nuovo processo, spostare lo stato in quel processo, quindi distruggere il processo originale. Abbiamo "risolto" questa perdita con una tecnica simile. – RaoulRubin

4

C'è un modo per cancellare le perdite di memoria usando la riflessione e rimuovendo i riferimenti dai campi privati ​​su mainForm. Questa non è una buona soluzione, ma per le persone disperate ecco il codice:

//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); 
     } 
    } 
} 
+1

Sembra essere la stessa soluzione di questo thread del forum WPF: http://social.msdn.microsoft.com/Forums/en/wpf/thread/368755dd-0047-4a88-9951-ba0539266410 – bzlm

+0

In realtà funziona. Qualcuno si preoccupa di spiegare questa magia nera? @ ___ @ – Edza

+2

@udione, cosa fare per l'applicazione WinForm? –

1

La soluzione qui di seguito ha lavorato per me:

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 

Buona fortuna, Layla

+0

ho provato a implementare questa soluzione (anche se ho solo bisogno di disporre di 1 browser invece di 2) ma stavo ottenendo un errore di runtime da questa riga: 'Dim tmpWeb As WebBrowser = wbDebugMain.ActiveXInstance' Il messaggio di errore era' Impossibile lanciare l'oggetto COM di tipo 'System .__ ComObject' nel tipo di classe 'System.Windows.Forms.WebBrowser'. Le istanze che rappresentano componenti COM non possono essere convertite in tipi che non rappresentano componenti COM; tuttavia possono essere trasmessi alle interfacce fintanto che il componente COM sottostante supporta chiamate QueryInterface per l'IID dell'interfaccia. Qualche idea? – Allen

+0

btw, il mio oggetto System.Windows.Forms.WebBrowser si chiama 'wbDebugMain' – Allen

+1

Sono stato in grado di aggirare l'errore dichiarandolo come un "Oggetto" invece di "WebBrowser" (cosa è "SHDocVw.WebBrowser"?) Ma la memoria non è stata rilasciata quando ho distrutto i controlli e li ho rimossi dal modulo ... li ho impostati per essere ricreati ogni 10 minuti, ma la memoria è gradualmente aumentata fino a quando non ho ottenuto un'eccezione di memoria insufficiente. Ho analizzato la mia perdita di memoria con debugdiag e ha detto che jscript.dll era responsabile per oltre un gig di allocazioni di memoria, wow! Forse ci sono diversi tipi di perdite? Mi sembra di andare bene, a meno che non ci sia javascript sul sito ... – Allen

-3

Prova questa soluzione, so che non è l'ideale. Incollare il codice dopo ogni caricamento della pagina

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); 
} 
+1

Hai davvero bisogno di spiegarlo (da dove sono venuti i numeri magici?) –

+2

Impostare MaxWorkingSet (nel migliore dei casi) finirebbe semplicemente a sfogliare la memoria sul disco , rallentando le prestazioni e migliorando nulla. – EricLaw

7

ho preso il codice udione s' (ha funzionato per me, grazie!) E ha cambiato due piccole cose:

  1. IKeyboardInputSite è un interfaccia pubblica e metodo Unregister(), quindi non è necessario utilizzare il reflection dopo aver ricevuto un riferimento alla raccolta * _keyboardInputSinkChildren *.

  2. Poiché vista non ha sempre un riferimento diretto alla sua classe di finestra (soprattutto in MVVM) ho aggiunto un metodo (elemento DependencyObject) GetWindowElement che restituisce il riferimento richiesto attraversando attraverso albero visuale.

Grazie, 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<IKeyboardInputSite>; 

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

Grazie a tutti!

+0

ho provato a convertire questo in VB, ma sto avendo alcuni problemi; ecco il mio codice GetWindowElement: 'Funzione condivisa privata GetWindowElement (elemento As System.Windows.DependencyObject) As System.Windows.Window While element IsNot Nothing AndAlso Not (TypeOf element System.Windows.Window) element = Windows.Media.VisualTreeHelper. GetParent (elemento) End While Return TryCast (element, System.Windows.Window) End Function 'ma nel codice di dispatch, sto ricevendo un errore da questa riga:' Dim window = GetWindowElement (wbDebugMain) 'dicendo che il tipo è sbagliato. – Allen

+0

, l'errore specifico è 'Il valore del tipo 'System.Windows.Forms.WebBrowser' non può essere convertito in 'System.Windows.DependencyObject''. – Allen

3

Dopo aver combattuto questo esatto Memoria problema da diverse direzioni (Win32 WorkingSet/COM SHDocVw interfacce, ecc) con la componente WPF WebBrowser, ho scoperto il problema per noi era jqGrid plug trattenendo risorse non gestite nello IE ActiveXHost e non li rilascia dopo aver chiamato WebBrowser.Dispose(). Questo problema è spesso creato da Javascript che non si comporta correttamente. La cosa strana è che Javascript funziona bene in IE regolare - non solo dal controllo WebBrowser. Suppongo che la raccolta dei rifiuti sia diversa tra i due punti di integrazione in quanto IE non può mai essere veramente chiuso.

Una cosa che suggerirei, se si sta creando le pagine di origine è quello di rimuovere tutti i componenti JS e lentamente l'aggiunta di nuovo in volta identificato il plugin incriminato JS (come abbiamo fatto) -. Dovrebbe essere facile da rimediare al problema. Nel nostro caso abbiamo semplicemente utilizzato $("#jqgrid").jqGrid('GridDestroy') per rimuovere correttamente gli eventi e gli elementi DOM associati creati. Ci siamo presi cura del problema invocandolo quando il browser è stato chiuso tramite WebBrowser.InvokeScript.

Se non si ha la possibilità di modificare le pagine di origine a cui si è diretti, è necessario inserire del JS nella pagina per pulire gli eventi e gli elementi DOM che stanno perdendo memoria. Sarebbe bello se Microsoft avesse trovato una soluzione a questo problema, ma per ora non sono disponibili i plug-in JS che necessitano di puliti.

1

Penso che questa domanda sia rimasta senza risposta per molto tempo. Così tanti thread con la stessa domanda ma non la risposta definitiva.

Ho trovato un modo per ovviare a questo problema e ho voluto condividere con voi tutti coloro che stanno ancora affrontando questo problema.

step1: Creare un nuovo modulo, digitare form2 e aggiungere un controllo browser Web su di esso. step2: Nel form1 in cui è presente il controllo del webbrowser, è sufficiente rimuoverlo. step3: Ora, andare a Form2 e rendere pubblico il modificatore di accesso per questo controllo del browser Web in modo che sia accessibile in Form1 step4: Creare un pannello in form1 e creare l'oggetto di form2 e aggiungerlo nel pannello. Form2 frm = new Form2(); frm.TopLevel = falso; frm.Show(); panel1.Controls.Add (frm); step5: Chiamare il seguente codice a intervalli regolari frm.Controls.Remove (frm.webBrowser1); frm.Dispose();

Questo è tutto. Ora, quando lo esegui, puoi vedere che il controllo del browser è stato caricato e verrà eliminato a intervalli regolari e non sarà più possibile appendere l'applicazione.

È possibile aggiungere il codice seguente per renderlo più efficiente.

 IntPtr pHandle = GetCurrentProcess(); 
     SetProcessWorkingSetSize(pHandle, -1, -1); 


     GC.Collect(); 
     GC.WaitForPendingFinalizers(); 
     GC.Collect(); 
+2

L'impostazione di MaxWorkingSet potrebbe (nel migliore dei casi) solo terminare il paging della memoria su disco, rallentando le prestazioni e migliorando nulla – EricLaw

0

Credere di più di un problema di .NET Framework piuttosto che di controllo del browser web. Collegandosi all'evento navigato del browser con un gestore che disporrà il browser e quindi navigando su about: blank sarà una buona soluzione. Ad esempio:

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