首页 文章

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

提问于
浏览
57

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

简介:使用.NET webbrowser控件导航到页面会增加从未释放的内存使用量 .

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

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

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

通常提出的修复不起作用:

  • 去不同的页面并不重要 . about:blank触发泄漏 . 它不需要页面具有javascript或任何其他额外技术 .

  • 使用不同版本的Internet Explorer无关紧要 . 7,8和9都表现出相同的症状,据我所知,所有版本在控件中都有相同的内存泄漏 .

  • Dispose()控件无效 .

  • 垃圾收集没有帮助 . (事实上,我对此进行的研究表明泄漏是在Webbrowswer控件包装的非托管COM代码中 . )

  • 最小化并将进程可用内存设置为-1,-1(SetProcessWorkingSetSize()或simimlar . )仅减少物理内存使用量,对虚拟内存没有影响 .

  • 调用WebBrowser.Stop()不是一个解决方案,并且打破了使用静态网页之外的任何功能的功能,而不仅仅是略微减少泄漏 .

  • 在导航到另一个文档之前强制等待文档完全加载也无济于事 .

  • 在单独的appDomain中加载控件无法解决问题 . (我自己没有这样做,但是研究显示其他人在这条路线上没有成功 . )

  • 使用不同的包装器(如csexwb2)无济于事,因为这也会遇到同样的问题 .

  • 清除Internet临时文件缓存不执行任何操作 . 问题出在活动内存中,而不是磁盘上 .

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

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

救命?

8 回答

  • 12

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

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

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

  • 4

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

    • IKeyboardInputSite 是一个公共接口,方法 Unregister() ,因此在收到对* _keyboardInputSinkChildren *集合的引用后,我们不需要使用反射 .

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

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

    谢谢你们!

  • 1

    有一种方法可以通过使用来清除内存泄漏反射和从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

    在使用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事件和元素 . 如果微软找到解决方案,那将是很好的,但是现在我们还在探索需要 cleansed 的JS插件 .

  • 7

    以下解决方案对我有用:

    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
    

    祝你好运,Layla

  • 1

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

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

    step1:创建一个新表单,例如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();

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

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

    IntPtr pHandle = GetCurrentProcess();
            SetProcessWorkingSetSize(pHandle, -1, -1);
    
    
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
    
  • 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"));
    }
    
  • -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);
    }
    

相关问题