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

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

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

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

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

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

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

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

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

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

Помогите?

9 ответов

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

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

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

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

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

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

Спасибо, Удионе

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

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

Есть способ устранить утечки памяти, используя отражение и удаляя ссылки из приватных полей в 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);
        }
    }
} 

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

Одна вещь, которую я бы посоветовал, если вы создаете исходные страницы, это удалить все компоненты JS и медленно добавить их обратно. После того, как вы определили нарушающий плагин JS (как мы это сделали), проблема должна быть легко решена. В нашем случае мы просто использовали $("#jqgrid").jqGrid('GridDestroy') правильно удалить события и связанные элементы DOM, которые он создал. Это позаботилось о проблеме для нас, вызвав это, когда браузер закрыт через WebBrowser.InvokeScript,

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

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

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

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

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

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

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


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

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

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

Удачи, Лейла

Поверьте, это скорее проблема.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"));
}

ЭТО РАБОТАЕТ, особенно если у вас приложение Winforms! Ручки будут освобождены сразу после закрытия веб-браузера.

ИСПОЛЬЗУЙТЕ КОД:

      using system.windows.forms;
[DllImport("KERNEL32.DLL", EntryPoint = "SetProcessWorkingSetSize", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
internal static extern bool SetProcessWorkingSetSize(IntPtr pProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize);

[DllImport("KERNEL32.DLL", EntryPoint = "GetCurrentProcess", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr GetCurrentProcess();

// **Release handles invoking the above**
IntPtr pHandle = GetCurrentProcess();
SetProcessWorkingSetSize(pHandle, -1, -1);

Удачного кодирования!

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

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);
}
Другие вопросы по тегам