Как обойти утечку памяти в элементе управления .NET Webbrowser?

Это широко известная старая проблема с элементом управления .NET Webbrowser.

Описание: Наличие элемента управления веб-браузера .NET «Переход на страницу» увеличивает использование памяти, которая никогда не освобождается.

Воспроизведите утечку памяти: добавьте в форму элемент управления WebBrowser. Используйте его для перехода к любым нужным страницам. about: blank работает, прокрутка изображений Google вниз до тех пор, пока ваше использование не станет более 100 МБ, а затем просмотр в другом месте, чтобы заметить, что почти вся память не освобождена, - более яркая демонстрация.

Мои текущие требования к приложению включают запуск его в течение длительного времени с отображением ограниченного окна браузера IE7. Запуск самого IE7 с какой-то ублюдочной настройкой хуков, BHO и групповых политик тоже нежелателен, хотя в настоящее время это выглядит как запасной вариант. Встраивание браузера в приложение Windows Forms - это. Я не могу использовать другую базу браузера. Требуется IE7.

Предыдущие темы и статьи, относящиеся к этой известной утечке памяти:

Часто предлагаемые исправления, которые НЕ РАБОТАЮТ:

  • Переход на разные страницы не имеет значения. about: blank вызывает утечку. Для этого не требуется, чтобы на странице был javascript или какие-либо другие дополнительные технологии.
  • Использование разных версий Internet Explorer не имеет значения. 7, 8 и 9 демонстрируют одни и те же симптомы, и, насколько я слышал, все версии имеют одинаковую утечку памяти в элементе управления.
  • Dispose () элемента управления не помогает.
  • Сбор мусора не помогает. (Фактически, проведенное мной исследование показывает, что утечка происходит в неуправляемом COM-коде, который оборачивается элементом управления Webbrowswer.)
  • Сведение к минимуму и установка доступной памяти процесса на -1, -1 (SetProcessWorkingSetSize () или аналог.) Только сокращает использование физической памяти, не влияет на виртуальную память.
  • Вызов WebBrowser.Stop () не является решением и нарушает функциональность использования чего-либо, кроме статических веб-страниц, не делая больше, чем просто минимизируя утечку.
  • Принудительное ожидание полной загрузки документа перед переходом к другому также не помогает.
  • Загрузка элемента управления в отдельный домен приложения не решает проблему. (Сам я этого не делал, но исследования показывают, что другие не добились успеха с этим путем.)
  • Использование другой оболочки, такой как csexwb2, не помогает, поскольку она также страдает той же проблемой.
  • Очистка кеша временных файлов Интернета ничего не делает. Проблема в активной памяти, а не на диске.

Память очищается при закрытии и перезапуске всего приложения.

Я готов написать собственный элемент управления браузером напрямую в COM или Windows API, если это наверняка решит проблему. Конечно, я бы предпочел менее сложное исправление; Я бы предпочел не спускаться на более низкие уровни, чтобы что-то сделать, потому что я не хочу изобретать колесо с точки зрения поддерживаемых функций браузера. Избавьтесь от дублирования функций IE7 и нестандартного поведения в браузере с индивидуальным дизайном.

Помощь?


person Azuvector    schedule 28.11.2011    source источник
comment
Можно ли запустить часть браузера в другом приложении и общаться с ним через удаленное взаимодействие .Net, WCF или что-то в этом роде? Вы можете контролировать использование памяти, а затем закрыть и перезапустить его, когда он достигнет некоторого порога.   -  person pstrjds    schedule 29.11.2011
comment
@pstrjds: я думаю, что это единственный реальный обходной путь, который будет очень сложно реализовать и определенно ограничит функциональность.   -  person Juan    schedule 01.12.2011
comment
Распространенное заблуждение, что освобождение памяти мгновенно уменьшает размер виртуальной машины процесса. Это не то, как работает диспетчер памяти Windows.   -  person Hans Passant    schedule 01.12.2011
comment
@HansPassant: Если вы имеете в виду, что память может быть освобождена Windows позже, например, если бы вообще не было утечки, тогда приложения не получали бы исключения Out Of Memory или сбой из-за этого. Я также пробовал большинство обходных путей, упомянутых OP, и мой фактический способ измерения потребления памяти - это сбой приложения.   -  person Juan    schedule 01.12.2011
comment
@pstrjds: В моем случае использования не особо много для меня. На данный момент лучший обходной путь - немного свести к минимуму проблему, постоянно просматривая страницы на диск (что ничего не делает, кроме как немного улучшает это в диспетчере задач, хотя, предположительно, это делает сборщик мусора потенциально более вероятным для сбора вещей?) И явным вызовом GC и предоставляет пользователям функцию быстрого перезапуска приложения. Этот контроль кажется довольно неработающим. То, что я сейчас исследую в качестве решения, - это использование самого IE и написание нескольких плагинов BHO для ограничения / обеспечения необходимой функциональности для приложения.   -  person Azuvector    schedule 01.01.2012
comment
IExplore.exe (99% его реализовано в DLL, которая используется элементом управления WebBrowser COM) также ведет себя аналогичным образом, он сохраняет как минимум ~ 50 МБ выделенных частных байтов. Я не думаю, что это связано конкретно с .NET. И я не думаю, что это утечка памяти (разве что с точки зрения многих людей, потому что им это не нравится). Вряд ли это можно изменить каким-либо образом. Кстати, мой процесс FF остается на уровне 130M, и хотя у меня только одна вкладка открыта только в Chrome, у меня есть 3 процесса chrome.exe в общей сложности ~ 200M.   -  person Simon Mourier    schedule 22.10.2014


Ответы (8)


Эта утечка кажется утечкой в ​​неуправляемой памяти, поэтому никакие ваши действия в процессе не будут возвращать эту память. Из вашего поста я вижу, что вы довольно часто и безуспешно пытались избежать утечки.

Я бы предложил другой подход, если это возможно. Создайте отдельное приложение, использующее элемент управления веб-браузера, и запустите его из своего приложения. Используйте описанный метод здесь, чтобы встроить вновь созданное приложение в существующее. Общайтесь с этим приложением с помощью удаленного взаимодействия WCF или .NET. Время от времени перезапускайте дочерний процесс, чтобы он не занимал много памяти.

Это, конечно, довольно сложное решение, и процесс перезапуска, вероятно, может выглядеть некрасиво. Вы можете прибегать к перезапуску всего приложения браузера каждый раз, когда пользователь переходит на другую страницу.

person Ivan    schedule 01.12.2011
comment
@Doc, WebBrowser элемент управления является частью пользовательского интерфейса? Или он скрытно используется для веб-автоматизации? - person noseratio; 28.10.2014
comment
@Noseratio В моем случае это часть пользовательского интерфейса. - person Daniel; 28.10.2014
comment
Наблюдая за perfmon, я увидел, что браузер IE 11 делает нечто подобное (вероятно, по другим причинам). Казалось, что он создает новый процесс, перемещает состояние в этот процесс, а затем уничтожает исходный процесс. Мы исправили эту утечку аналогичным способом. - person RaoulRubin; 31.12.2015

Я взял код udione (у меня он сработал, спасибо!) И изменил две мелочи:

  1. IKeyboardInputSite является общедоступным интерфейсом и имеет метод Unregister (), поэтому нам не нужно использовать отражение после того, как мы получили ссылку на коллекцию * _keyboardInputSinkChildren *.

  2. Поскольку представление не всегда имеет прямую ссылку на свой класс окна (особенно в MVVM), я добавил метод GetWindowElement (элемент DependencyObject), который возвращает требуемую ссылку путем обхода визуального дерева.

Спасибо 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;
}

Спасибо вам всем!

person Sergey Kostrukov    schedule 12.11.2012
comment
Я пытался преобразовать это в VB, но у меня возникли проблемы; вот мой код GetWindowElement: `Private Shared Function GetWindowElement (element As System.Windows.DependencyObject) As System.Windows.Window While element IsNot Nothing AndAlso Not (TypeOf element Is System.Windows.Window) element = Windows.Media.VisualTreeHelper. GetParent (element) End While Return TryCast (element, System.Windows.Window) End Function `но в коде удаления я получаю сообщение об ошибке из этой строки: Dim window = GetWindowElement(wbDebugMain) говорит, что тип неправильный. - person Allen; 08.03.2013
comment
на самом деле конкретная ошибка Value of type 'System.Windows.Forms.WebBrowser' cannot be converted to 'System.Windows.DependencyObject'. - person Allen; 08.03.2013

Есть способ устранить утечки памяти с помощью отражения и удаления ссылок из частных полей в 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);
        }
    }
} 
person udione    schedule 19.02.2012
comment
Похоже, это то же решение, что и в этой ветке форума WPF: social.msdn.microsoft.com/Forums/en/wpf/thread/ - person bzlm; 19.02.2012
comment
Это действительно работает. Кто-нибудь хочет объяснить эту черную магию? @ ___ @ - person Edza; 05.05.2015
comment
@udione, что делать с приложением WinForm? - person Olexiy Pyvovarov; 14.08.2015
comment
Как сделать то же самое в WinForms? - person Kosmo零; 15.01.2018

Решив эту точную проблему нехватки памяти с разных сторон (Win32 WorkingSet / COM SHDocVw интерфейсы и т. Д.) с помощью WPF WebBrowser, я обнаружил, что проблема заключается в том, что плагин jqGrid удерживает на неуправляемые ресурсы в IE ActiveXHost и не освобождает их после вызова WebBrowser.Dispose(). Эта проблема часто возникает из-за некорректного поведения Javascript. Странно то, что Javascript отлично работает в обычном IE, но не в элементе управления WebBrowser. Я предполагаю, что сборка мусора в двух точках интеграции различается, поскольку IE никогда не может быть закрыт.

Одна вещь, которую я бы посоветовал, если вы создаете исходные страницы, - это удалить все компоненты JS и медленно добавить их обратно. Как только вы определите проблемный плагин JS (как мы) - это должно быть легко для устранения проблемы. В нашем случае мы просто использовали _5 _, чтобы правильно удалить события и связанные элементы DOM, которые он создал. Это решило проблему, вызвав это при закрытии браузера через WebBrowser.InvokeScript < / а>.

Если у вас нет возможности изменять исходные страницы, к которым вы переходите, вам придется добавить на страницу некоторый JS, чтобы очистить DOM-события и элементы, вызывающие утечку памяти. Было бы неплохо, если бы Microsoft нашла решение этой проблемы, но пока нам остается исследовать плагины JS, которые необходимо очистить.

person SliverNinja - MSFT    schedule 13.03.2013

Приведенное ниже решение сработало для меня:

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

Удачи, Лейла

person Layla    schedule 21.02.2012
comment
Я попытался реализовать это решение (хотя мне нужно избавиться только от 1 веб-браузера, а не от 2), но я получал ошибку времени выполнения из этой строки: Dim tmpWeb As WebBrowser = wbDebugMain.ActiveXInstance Сообщение об ошибке было Unable to cast COM object of type 'System.__ComObject' to class type 'System.Windows.Forms.WebBrowser'. Instances that represent COM components cannot be cast to types that do not represent COM components; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface. Любые идеи? - person Allen; 08.03.2013
comment
кстати, мой объект System.Windows.Forms.WebBrowser называется wbDebugMain - person Allen; 08.03.2013
comment
Мне удалось обойти ошибку, объявив ее как Object вместо WebBrowser (что такое SHDocVw.WebBrowser?) Но память не была освобождена, когда я уничтожил элементы управления и удалил их из формы ... Я установил их для воссоздания каждые 10 минут, но память все равно постепенно увеличивалась, пока я не получил исключение нехватки памяти. Я проанализировал утечку памяти с помощью debugdiag, и он сказал, что jscript.dll отвечает за более гигабайт распределения памяти, ага! Может быть, есть несколько разных видов утечек? Кажется, у меня все в порядке, если на сайте нет javascript ... - person Allen; 11.03.2013

Думаю, этот вопрос уже давно остается без ответа. Так много обсуждений с одним и тем же вопросом, но нет окончательного ответа.

Я нашел способ решить эту проблему и хотел бы поделиться с вами всеми, кто все еще сталкивается с этой проблемой.

step1: Создайте новую форму, скажем, form2, и добавьте на нее элемент управления в веб-браузере. step2: В форме1, где у вас есть элемент управления веб-браузером, просто удалите его. step3: Теперь перейдите к Form2 и сделайте модификатор доступа для этого элемента управления webbrowser общедоступным, чтобы к нему можно было получить доступ в Form1 step4: Создайте панель в form1 и создайте объект form2 и добавьте его в панель. Form2 frm = новый Form2 (); frm.TopLevel = false; frm.Show (); панель1.Controls.Add (frm); step5: Регулярно вызывайте приведенный ниже код frm.Controls.Remove (frm.webBrowser1); frm.Dispose ();

Вот и все. Теперь, когда вы запустите его, вы увидите, что элемент управления веб-браузера загружен, и он будет удаляться через определенные промежутки времени, и приложение больше не будет зависать.

Вы можете добавить приведенный ниже код, чтобы сделать его более эффективным.

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


        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
person nh53019    schedule 09.05.2013
comment
Установка MaxWorkingSet (в лучшем случае) просто приведет к выгружению памяти на диск, что снизит производительность и ничего не улучшит - person EricLaw; 29.07.2013

Поверьте, это скорее проблема .Net Framework, а не элемент управления веб-браузером. Подключение к навигационному событию браузера с помощью обработчика, который удаляет браузер, а затем переход к 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"));
}
person Asish Sinha    schedule 06.11.2014

Попробуйте это решение, я знаю, что оно не идеально. Вставляйте код после каждой загрузки страницы

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);
}
person Stefan Kruger    schedule 22.06.2012
comment
Вам действительно нужно это объяснить (откуда взялись магические числа?) - person Emond Erno; 20.10.2012
comment
Установка MaxWorkingSet (в лучшем случае) просто приведет к выгружению памяти на диск, что снизит производительность и ничего не улучшит. - person EricLaw; 29.07.2013