Как обойти утечку памяти в элементе управления.NET Webbrowser?
Это широко известная старая проблема с элементом управления.NET Webbrowser.
Описание: Наличие веб-браузера.NET. Переход на страницу увеличивает использование памяти, которое никогда не освобождается.
Воспроизведите утечку памяти: добавьте элемент управления WebBrowser в форму. Используйте его для перехода на любые страницы, которые вы хотите. about: blank работает, прокручивая изображения Google до тех пор, пока вы не используете более 100 МБ +, а затем просматривая другие места, чтобы заметить, что почти вся эта память освобождена, является более драматичной демонстрацией.
Мои текущие требования к приложению включают запуск его в течение длительных периодов времени с отображением ограниченного окна браузера IE7. Сам по себе запуск IE7 с какой-то неестественной настройкой хуков, BHO и групповых политик также нежелателен, хотя сейчас это выглядит как запасной вариант. Встраивание браузера в приложение Windows Forms. Использование другой базы браузера для меня недоступно. IE7 требуется.
Предыдущие темы и статьи, касающиеся этой известной утечки памяти:
- http://www.vbforums.com/showthread.php?t=644658
- Как исправить утечку памяти в элементе управления IE WebBrowser?
- Утечка памяти при использовании элемента управления WPF WebBrowser в нескольких окнах
- http://social.msdn.microsoft.com/Forums/en-US/ieextensiondevelopment/thread/88c21427-e765-46e8-833d-6021ef79e0c8/
- http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/8a2efea4-5e75-4e3d-856f-b09a4e215ede
- http://dotnetforum.net/topic/17400-appdomain-webbrowser-memory-leak/
Часто предлагаемые исправления, которые НЕ РАБОТАЮТ:
- Переход на разные страницы не имеет значения. 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(он работал для меня, спасибо!) И изменил две маленькие вещи:
IKeyboardInputSite является общедоступным интерфейсом и имеет метод Unregister (), поэтому нам не нужно использовать отражение после получения ссылки на коллекцию *_keyboardInputSinkChildren*.
Поскольку представление не всегда имеет прямую ссылку на свой класс окна (особенно в 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);
}