Почему и как избежать утечек памяти в обработчике событий?

Я только что понял, прочитав некоторые вопросы и ответы на Stackru, что добавление обработчиков событий с использованием += в C# (или я думаю, другие языки.net) может вызвать общие утечки памяти...

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

Как это работает (имеется в виду, почему это на самом деле вызывает утечку памяти)?
Как я могу исправить эту проблему? Использует -= достаточно того же обработчика событий?
Существуют ли общие шаблоны проектирования или лучшие практики для обработки подобных ситуаций?
Пример: как я должен обрабатывать приложение, которое имеет много разных потоков, используя много разных обработчиков событий, чтобы вызвать несколько событий в пользовательском интерфейсе?

Есть ли хорошие и простые способы эффективно контролировать это в уже построенном большом приложении?

3 ответа

Решение

Причина проста для объяснения: в то время как обработчик события подписан, издатель события содержит ссылку на подписчика через делегат обработчика события (при условии, что делегат является методом экземпляра).

Если издатель живет дольше, чем подписчик, он будет поддерживать подписчика живым, даже если нет никаких других ссылок на подписчика.

Если вы отмените подписку на событие с равным обработчиком, то да, это удалит обработчик и возможную утечку. Однако, по моему опыту, это на самом деле редко является проблемой - потому что обычно я нахожу, что издатель и подписчик в любом случае имеют примерно одинаковые времена жизни.

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

Я объяснил эту путаницу в блоге по адресу https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16. Я постараюсь резюмировать это здесь, чтобы вы имели четкое представление.

Ссылка означает "Потребность":

Прежде всего, вам нужно понять, что если объект A содержит ссылку на объект B, то это будет означать, что объекту A нужен объект B для функционирования, верно? Таким образом, сборщик мусора не будет собирать объект B, пока объект A находится в памяти.

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

+ = Означает, что ссылка объекта Right side вставляется в левый объект:

Но путаница возникает из-за оператора C# +=. Этот оператор четко не сообщает разработчику, что правая часть этого оператора фактически вводит ссылку на левый объект.

Поступая так, объект A думает, что ему нужен объект B, хотя, с вашей точки зрения, объект A не должен заботиться о том, живёт объект B или нет. Поскольку объект A считает, что объект B необходим, объект A защищает объект B от сборщика мусора, пока объект A жив. Но если вы не хотите, чтобы эта защита предоставлялась объекту подписчика события, то, можно сказать, произошла утечка памяти.

Вы можете избежать такой утечки, отсоединив обработчик событий.

Как принять решение?

Но во всей кодовой базе есть множество событий и обработчиков событий. Означает ли это, что вам нужно повсюду отсоединять обработчики событий? Ответ - нет. Если бы вам пришлось так поступить, ваша кодовая база была бы ужасно многословной.

Вы можете скорее следовать простой блок-схеме, чтобы определить, нужен ли отсоединяющий обработчик событий или нет.

В большинстве случаев вы можете обнаружить, что объект подписчика на событие не менее важен, чем объект публикатора события, и оба должны существовать одновременно.

Пример сценария, когда не о чем беспокоиться

Например, событие нажатия кнопки окна.

Here, the event publisher is the Button, and the event subscriber is the MainWindow. Applying that flow chart, ask a question, does the Main Window (event subscriber) supposed to be dead before the Button (event publisher)? Obviously No. Right? That won't even make sense. Then, why worry about detaching the click event handler?

An example when an event handler detachment is a MUST.

I will provide one example where the subscriber object is supposed to be dead before the publisher object. Say, your MainWindow publishes an event named "SomethingHappened" and you show a child window from the main window by a button click. The child window subscribes to that event of the main window.

And, the child window subscribes to an event of the Main Window.

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

Теперь, согласно предоставленной мною блок-схеме, если вы зададите вопрос "Предполагается ли, что дочернее окно (подписчик событий) не работает до того, как публикатор события (главное окно)?" Ответ должен быть ДА. Верно? Итак, отсоедините обработчик событий Я обычно делаю это из события Unloaded окна.

Практическое правило: если ваше представление (например, WPF, WinForm, UWP, Xamarin Form и т. Д.) Подписывается на событие ViewModel, всегда не забудьте отсоединить обработчик событий. Потому что ViewModel обычно живет дольше, чем представление. Итак, если ViewModel не уничтожен, любое представление, на которое подписано событие этой ViewModel, останется в памяти, что плохо.

Подтверждение концепции с помощью профилировщика памяти.

Будет не очень весело, если мы не сможем проверить концепцию с помощью профилировщика памяти. В этом эксперименте я использовал профилировщик JetBrain dotMemory.

Сначала я запустил MainWindow, который выглядит так:

Затем я сделал снимок памяти. Затем я нажал кнопку 3 раза. Появились три дочерних окна. Я закрыл все эти дочерние окна и нажал кнопку Force GC в профилировщике dotMemory, чтобы убедиться, что вызывается сборщик мусора. Затем я сделал еще один снимок памяти и сравнил его. Вот! наш страх был правдой. Дочернее окно не собиралось сборщиком мусора даже после того, как оно было закрыто. Не только это, но и счетчик просочившихся объектов для объекта Child Window также отображается " 3 " (я щелкнул кнопку 3 раза, чтобы отобразить 3 дочерних окна).

Хорошо, тогда я отсоединил обработчик событий, как показано ниже.

Затем я выполнил те же действия и проверил профилировщик памяти. На этот раз вау! больше никакой утечки памяти.

Да, -= достаточно, однако, может быть довольно сложно отслеживать каждое назначенное событие, когда-либо. (подробнее см. пост Джона). Что касается шаблона проектирования, взгляните на шаблон слабых событий.

Событие - это действительно связанный список обработчиков событий

Когда вы делаете += новый EventHandler для события, на самом деле не имеет значения, была ли эта конкретная функция ранее добавлена ​​в качестве прослушивателя, она будет добавлена ​​один раз за +=.

Когда событие вызывается, оно проходит по связанному списку, элемент за элементом и вызывает все методы (обработчики событий), добавленные в этот список, поэтому обработчики событий по-прежнему вызываются, даже если страницы больше не работают, пока они живы (укоренены), и они будут живы до тех пор, пока они подключены. Поэтому они будут вызываться до тех пор, пока обработчик событий не отсоединится с -= new EventHandler.

Посмотреть здесь

и MSDN ЗДЕСЬ

Я могу сказать вам, что это может стать проблемой в Blazor. У вас может быть компонент, подписывающийся на события, используя += синтаксис и, в конечном итоге, это вызовет утечки.

Единственное решение (о котором я знаю) - не использовать анонимные методы, унаследовать компонент от IDisposable и использовать Dispose() чтобы отказаться от подписки на обработчик событий.

Другие вопросы по тегам