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

时间:2011-11-28 21:56:22

标签: .net memory memory-leaks browser webbrowser-control

这是.NET Webbrowser控件中一个众所周知的旧问题。

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

重现内存泄漏:将WebBrowser控件添加到表单。用它来导航到你想要的任何页面。关于:空白工作,向下滚动谷歌图像,直到你的使用量为100MB +然后在其他地方浏览,注意几乎没有任何内存被释放是一个更戏剧性的演示。

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

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

经常提出的不起作用的修补程序:

  • 去不同的页面并不重要。 about:blank触发泄漏。它不需要页面具有javascript或任何其他额外技术。
  • 使用不同版本的Internet Explorer无关紧要。 7,8和9都表现出相同的症状,据我所知,所有版本在控件中都有相同的内存泄漏。
  • 处理()控件无效。
  • 垃圾收集没有帮助。 (事实上​​,我对此进行的研究表明泄漏是在Webbrowswer控件包含的非托管COM代码中。)
  • 最小化并将进程可用内存设置为-1,-1(SetProcessWorkingSetSize()或simimlar。)仅减少物理内存使用量,对虚拟内存没有影响。
  • 调用WebBrowser.Stop()不是一个解决方案,并且打破了使用静态网页之外的任何功能的功能,而不仅仅是略微减少泄漏。
  • 在导航到另一个文档之前强制等待文档完全加载也无济于事。
  • 在单独的appDomain中加载控件无法解决问题。 (我自己并没有这样做,但研究显示其他人在这条路线上没有成功。)
  • 使用不同的包装器(如csexwb2)无济于事,因为这也会遇到同样的问题。
  • 清除Internet临时文件缓存不执行任何操作。问题出在活动内存中,而不是磁盘上。

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

我愿意直接在COM或Windows API中编写自己的浏览器控件,如果这是对问题的肯定修复。当然,我更喜欢一个不太复杂的修复;我宁愿避免降低级别来做事情,因为我不想在浏览器支持的功能方面重新发明轮子。让我们在自己动手的浏览器中复制IE7功能和非标准行为。

帮助?

8 个答案:

答案 0 :(得分:12)

此泄漏似乎是非托管内存中的泄漏,因此您在进程中执行的任何操作都不会回收该内存。从您的帖子中我可以看到您已经尝试过相当广泛地避免泄漏并且没有成功。

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

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

答案 1 :(得分:7)

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

  1. IKeyboardInputSite 是一个公共接口,其方法为取消注册(),因此在收到对* _keyboardInputSinkChildren *的引用后,我们不需要使用反射集合。

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

  3. 谢谢,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;
    }
    

    谢谢大家!

答案 2 :(得分:4)

有一种方法可以通过使用反射和从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);
        }
    }
} 

答案 3 :(得分:3)

我从WPF WebBrowser组件的不同方向( Win32 WorkingSet / COM SHDocVw接口等)与这个确切的 Out of Memory 问题进行了斗争,我发现了对我们来说,问题是 jqGrid 插件保留在IE ActiveXHost中的非托管资源,而在调用WebBrowser.Dispose()后不释放它们。此问题通常由行为不正常的Javascript创建。奇怪的是,Javascript在常规IE中运行良好 - 只是不在WebBrowser控件内。我猜测两个集成点之间的垃圾收集是不同的,因为IE永远不会真正关闭。

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

如果您无法修改导航到的源页面,则必须在页面中注入一些JS来清理DOM事件和泄漏内存的元素。如果微软找到解决方案会很好,但是现在我们还在探索需要清理的JS插件。

答案 4 :(得分:1)

以下解决方案对我有用:

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

祝你好运, 蕾拉

答案 5 :(得分:1)

我认为这个问题在很长一段时间内都没有得到答复。这么多线程有相同的问题,但没有确定的答案。

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

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

多数民众赞成。现在,当您运行它时,您可以看到加载了Web浏览器控件,它将定期处理,并且不再挂起应用程序。

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

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


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

答案 6 :(得分:0)

相信它更像是一个.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"));
}

答案 7 :(得分:-3)

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

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