如何解决 .NET Webbrowser 控件中的内存泄漏?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/8302933/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 16:03:18  来源:igfitidea点击:

How to get around the memory leak in the .NET Webbrowser control?

.netmemorymemory-leaksbrowserwebbrowser-control

提问by Azuvector

This is a widely-known, old issue with the .NET Webbrowser control.

这是 .NET Webbrowser 控件的一个广为人知的老问题。

Summary: Having the .NET webbrowser control Navigate to a page increases memory usage that is never freed.

摘要: 使用 .NET webbrowser 控件导航到页面会增加永远不会释放的内存使用量。

Reproduce the memory leak: Add a WebBrowser control to a form. Use it to Navigate to whatever pages you'd like. about:blank works, scrolling down on Google Images until your usage is 100MB+ and then browsing elsewhere to notice barely any of that memory is freed is a more dramatic demonstration.

重现内存泄漏: 向窗体添加 WebBrowser 控件。使用它导航到您想要的任何页面。about:blank 有效,向下滚动 Google 图片直到您的使用量超过 100MB,然后浏览其他地方发现几乎没有任何内存被释放,这是一个更生动的演示。

My current requirements for an application include running it for long periods of time, displaying a limited IE7 browser window. Running IE7 itself with some bastard setup of hooks, BHOs and group policies isn't desired either, although that's looking like the fallback at this time. Embedding a browser into a Windows Forms application is. Using a different browser base is not an available option for me. IE7 is required.

我目前对应用程序的要求包括长时间运行它,显示有限的 IE7 浏览器窗口。运行 IE7 本身并带有一些混蛋设置的钩子、BHO 和组策略也是不可取的,尽管目前这看起来像是后备。将浏览器嵌入到 Windows 窗体应用程序中。使用不同的浏览器库对我来说不是一个可用的选项。需要IE7。

Previous threads and articles relating to this known memory leak:

与此已知内存泄漏相关的先前线程和文章:

Often-proposed fixes that DO NOT WORK:

经常提出但不起作用的修复:

  • Going to different pages does not matter. about:blank triggers the leak. It does not require a page to have javascript, or any other extra technology.
  • Using different versions of Internet Explorer does not matter. 7, 8, and 9 all exhibit the same symptoms, and as far as I've heard, all versions have the same memory leak in the control.
  • Dispose()ing of the control does not help.
  • Garbage Collecting does not help. (In fact, research I've done into this indicates the leak is in unmanaged COM code that the Webbrowswer control wraps.)
  • Minimizing and setting process available memory to -1, -1(SetProcessWorkingSetSize() or simimlar.) only reduces physical memory usage, has no impact on virtual memory.
  • Calling WebBrowser.Stop() is not a solution, and breaks functionality for using anything but static webpages, without doing more than just minimizing the leak slightly.
  • Forcing a wait for a document to load completely before Navigating to another also doesn't help.
  • Loading the control in a separate appDomain does not fix the issue. (I've not done this myself, but research shows others having no success with this route.)
  • Using a different wrapper such as csexwb2 does not help, as this also suffers from the same issue.
  • Clearing the Temporary Internet Files cache does nothing. The problem is in active memory, not on disk.
  • 转到不同的页面并不重要。about:blank 触发泄漏。它不需要页面具有 javascript 或任何其他额外技术。
  • 使用不同版本的 Internet Explorer 没有关系。7、8 和 9 都表现出相同的症状,据我所知,所有版本在控件中都有相同的内存泄漏。
  • Dispose() 控件没有帮助。
  • 垃圾收集没有帮助。(事实上​​,我对此所做的研究表明泄漏发生在 Webbrowswer 控件包装的非托管 COM 代码中。)
  • 最小化进程可用内存并将其设置为 -1、-1(SetProcessWorkingSetSize() 或类似。)只会减少物理内存使用量,对虚拟内存没有影响。
  • 调用 WebBrowser.Stop() 不是一个解决方案,它会破坏使用静态网页以外的任何东西的功能,而不仅仅是稍微减少泄漏。
  • 在导航到另一个文档之前强制等待文档完全加载也无济于事。
  • 在单独的 appDomain 中加载控件并不能解决问题。(我自己没有这样做,但研究表明其他人在这条路线上没有成功。)
  • 使用不同的包装器(例如 csexwb2)无济于事,因为这也会遇到同样的问题。
  • 清除 Internet 临时文件缓存没有任何作用。问题出在活动内存中,而不是磁盘上。

Memory is cleared when the entire application is closed and restarted.

当整个应用程序关闭并重新启动时,内存会被清除。

I'm willing to write my own browser control in COM or Windows API directly, if that's a for-sure fix to the problem. Of course, I would prefer a less complicated fix; I'd rather avoid going down to lower levels to do things, because I don't want to be reinventing the wheel in terms of supported features of a browser. Letalone duplicating IE7 features and nonstandard behaviours in a roll-your-own style browser.

我愿意直接在 COM 或 Windows API 中编写我自己的浏览器控件,如果这确实可以解决问题。当然,我更喜欢不那么复杂的修复;我宁愿避免下到较低级别来做事,因为我不想在浏览器支持的功能方面重新发明轮子。更不用说在您自己的风格浏览器中复制 IE7 功能和非标准行为。

Help?

帮助?

回答by Ivan

This leak appears to be a leak in unmanaged memory, so nothing you do in your process is going to reclaim that memory. From your post I can see that you've tried avoid the leak quite extensively and without success.

这种泄漏似乎是非托管内存中的泄漏,因此您在进程中所做的任何事情都不会回收该内存。从您的帖子中,我可以看到您已经尝试过非常广泛地避免泄漏但没有成功。

I would suggest a different approach if feasible. Create a separate application that uses web browser control and start it from your application. Use the method described hereto embed newly created application within your own existing application. Communicate with that application using WCF or .NET remoting. Restart the child process from time to time to prevent it from taking to much memory.

如果可行,我会建议采用不同的方法。创建一个使用 Web 浏览器控件的单独应用程序并从您的应用程序启动它。使用此处描述的方法 将新创建的应用程序嵌入到您自己的现有应用程序中。使用 WCF 或 .NET 远程处理与该应用程序通信。不时重启子进程以防止它占用太多内存。

This of course is quite complicated solution and the restart process could probably look ugly. You might maybe resort to restarting the whole browser application every time user navigates to another page.

这当然是相当复杂的解决方案,重启过程可能看起来很难看。您可能会在每次用户导航到另一个页面时重新启动整个浏览器应用程序。

回答by Sergey Kostrukov

I took the udione's code (it worked for me, thanks!) and changed two small things:

我拿了udione的代码(它对我有用,谢谢!)并改变了两件小事:

  1. IKeyboardInputSiteis a public interface and has method Unregister(), so we don't need to use reflection after we received a reference to *_keyboardInputSinkChildren* collection.

  2. As view not always has a direct reference to its window class (especially in MVVM) I added a method GetWindowElement(DependencyObject element)which returns the required reference by traversing through visual tree.

  1. IKeyboardInputSite是一个公共接口并且有方法Unregister(),所以我们在收到对 *_keyboardInputSinkChildren* 集合的引用后不需要使用反射。

  2. 由于视图并不总是直接引用其窗口类(尤其是在 MVVM 中),因此我添加了一个方法GetWindowElement(DependencyObject element),它通过遍历可视化树返回所需的引用。

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

Thank you all!

谢谢你们!

回答by udione

There is a way to do clear memory leaks by using reflection and removing references from private fields on mainForm. This is not a good solution but for desperate people here is the code:

有一种方法可以通过使用反射和从 mainForm 上的私有字段中删除引用来清除内存泄漏。这不是一个好的解决方案,但对于绝望的人来说,这里是代码:

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

回答by SliverNinja - MSFT

Having battled this exact Out of Memoryissue from different directions (Win32 WorkingSet / COM SHDocVw interfaces, etc.) with the WPF WebBrowsercomponent, I discovered the problem for us was jqGridplugin holding onto unmanaged resources in the IE ActiveXHostand not releasing them after calling WebBrowser.Dispose(). This issue is often created by Javascript that is not behaving properly. The odd thing is that the Javascript works fine in regular IE - just not from within the WebBrowsercontrol. I surmise that the garbage collection is different between the two integration points as IE can never really be closed.

使用WPF组件从不同方向(Win32 WorkingSet / COM SHDocVw 接口等)解决了这个确切的内存不足问题后,我发现我们的问题是jqGrid插件保留了 IE 中的非托管资源,并且在调用. 此问题通常是由行为不正常的 Javascript 造成的。奇怪的是,Javascript 在常规 IE 中工作正常 - 只是不在控件内。我推测两个集成点之间的垃圾收集是不同的,因为 IE 永远不会真正关闭。WebBrowserActiveXHostWebBrowser.Dispose()WebBrowser

One thing I'd suggest if you are authoring the source pages is to remove all JS components and slowly adding them back in. Once you identify the offending JS plugin (like we did) - it should be easy to remedy the problem. In our case we just used $("#jqgrid").jqGrid('GridDestroy')to properly remove the events and associated DOM elements it created. This took care of the issue for us by invoking this when the browser is closed via WebBrowser.InvokeScript.

如果您正在创作源页面,我建议您删除所有 JS 组件并慢慢将它们重新添加回来。一旦您确定了有问题的 JS 插件(就像我们所做的那样)- 解决问题应该很容易。在我们的例子中,我们只是用来$("#jqgrid").jqGrid('GridDestroy')正确删除它创建的事件和关联的 DOM 元素。这通过在浏览器关闭时调用它为我们解决了这个问题WebBrowser.InvokeScript

If you don't have the ability to modify the source pages you navigate to - you would have to inject some JS into the page to clean up the DOM events and elements that are leaking memory. It would be nice if Microsoft found a resolution to this, but for now we are left probing for JS plugins that need cleansed.

如果您无法修改导航到的源页面 - 您必须将一些 JS 注入页面以清理正在泄漏内存的 DOM 事件和元素。如果 Microsoft 找到解决方案,那就太好了,但现在我们正在探索需要清理的JS 插件。

回答by nh53019

I think this question has gone unanswered for a long time now. So many threads with the same question but not conclusive answer.

我想这个问题已经很久没有答案了。这么多线程有相同的问题,但没有决定性的答案。

I have found a work around for this issue and wanted to share with you all who are still facing this issue.

我找到了解决此问题的方法,并希望与所有仍然面临此问题的人分享。

step1: Create a new form say form2 and add a web-browser control on it. step2: In the form1 where you have your webbrowser control, just remove it. step3: Now, go to Form2 and make the access modifier for this webbrowser control to be public so that it can be accessed in Form1 step4: Create a panel in form1 and create object of form2 and add this into panel. Form2 frm = new Form2 (); frm.TopLevel = false; frm.Show(); panel1.Controls.Add(frm); step5: Call the below code at regular intervals frm.Controls.Remove(frm.webBrowser1); frm.Dispose();

步骤 1:创建一个新表单,例如 form2 并在其上添加一个 Web 浏览器控件。步骤 2:在您拥有 webbrowser 控件的 form1 中,只需将其删除。step3:现在,转到Form2,将这个webbrowser控件的访问修饰符设置为public,以便在Form1中可以访问它 step4:在form1中创建一个面板并创建form2的对象并将其添加到面板中。Form2 frm = new Form2(); frm.TopLevel = 假;frm.Show(); panel1.Controls.Add(frm); step5:定期调用下面的代码 frm.Controls.Remove(frm.webBrowser1); frm.Dispose();

Thats it. Now when you run it, you can see that webbrowser control loaded and it will get disposed at regular intervals and there is no more hanging of the application.

就是这样。现在,当您运行它时,您可以看到已加载 webbrowser 控件,它将定期处理,并且应用程序不再挂起。

You can add the below code to make it more efficient.

您可以添加以下代码以提高效率。

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


        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

回答by Layla

The below solution worked for 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

Best of luck, Layla

祝你好运,莱拉

回答by Asish Sinha

Believe its more of an .Net Framework issue rather than Web Browser control. Hooking to the navigated event of the browser with a handler which will dispose the browser and then navigating to about:blank will be a good workaround. For example:

相信它更多的是 .Net Framework 问题,而不是 Web 浏览器控件。使用将处理浏览器的处理程序连接到浏览器的导航事件,然后导航到 about:blank 将是一个很好的解决方法。例如:

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

回答by Stefan Kruger

Try this solution, i know its not ideal. Paste the code after each page load

试试这个解决方案,我知道它并不理想。在每个页面加载后粘贴代码

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