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

154

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

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

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

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

gillyb
источник

Ответы:

188

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

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

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

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

Джон Скит
источник
... Я видел, как некоторые люди писали об этом в ответах на такие вопросы, как "что является наиболее распространенной утечкой памяти в .net".
gillyb
32
Способ обойти это со стороны издателя - установить событие на null, если вы уверены, что больше не будете его запускать. Это неявно удалит всех подписчиков и может быть полезно, когда определенные события запускаются только на определенных этапах жизненного цикла объекта.
JSB ձոգչ
2
Дипозный метод был бы хорошим моментом для установки события на нуль
Дэви Фьяменги,
6
@DaviFiamenghi: Ну, если что-то удаляется, это, по крайней мере, вероятный признак того, что он скоро будет иметь право на сборку мусора, и в этот момент не имеет значения, какие есть подписчики.
Джон Скит
1
@ BrainSlugs83: «и типичная картина событий включает в себя отправитель в любом случае» - да, но это событие производитель . Обычно экземпляр подписчика на событие является релевантным, а отправитель - нет. Так что да, если вы можете подписаться, используя статический метод, это не проблема, но это редко встречается в моем опыте.
Джон Скит
13

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

Femaref
источник
1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx в версии 4.0 все еще есть.
Femaref
Если я знаю, что издатель будет жить дольше, чем подписчик, я делаю подписку IDisposableи отписываюсь от события.
Shimmy Weitzhandler
10

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

Справочное обозначение «Потребность»:

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

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

+ = Означает, что вставка ссылки правого объекта на левый объект:

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

введите описание изображения здесь

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

введите описание изображения здесь

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

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

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

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

введите описание изображения здесь

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

Пример сценария, в котором вам не нужно беспокоиться

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

введите описание изображения здесь

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

Пример, когда отсоединение обработчика события является ОБЯЗАТЕЛЬНЫМ.

Я приведу один пример, когда объект подписчика должен быть мертвым перед объектом издателя. Скажем, ваше MainWindow публикует событие с именем SomethingHappened, и вы показываете дочернее окно из главного окна нажатием кнопки. Дочернее окно подписывается на это событие главного окна.

введите описание изображения здесь

И дочернее окно подписывается на событие Главного окна.

введите описание изображения здесь

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

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

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

Доказательство концепции с использованием профилировщика памяти.

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

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

введите описание изображения здесь

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

введите описание изображения здесь

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

введите описание изображения здесь

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

введите описание изображения здесь

Эмран Хуссейн
источник
3

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

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

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

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

и MSDN ЗДЕСЬ

TalentTuner
источник
Также смотрите: blogs.msdn.com/b/tess/archive/2006/01/23/…
Коди Грей