Чому і як уникнути витоку пам'яті обробника подій?


154

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

Я багато разів використовував подібні обробники подій, і ніколи не розумів, що вони можуть спричинити або спричинити витоки пам'яті у моїх програмах.

Як це працює (це означає, чому це насправді спричиняє витік пам’яті)?
Як я можу виправити цю проблему? Чи -=достатньо використання одного і того ж обробника подій?
Чи існують загальні схеми дизайну чи найкращі практики для вирішення подібних ситуацій?
Приклад: Як я повинен обробляти програму, яка має багато різних потоків, використовуючи багато різних обробників подій, щоб підняти кілька подій в інтерфейсі?

Чи є якісь хороші та прості способи ефективно контролювати це у вже створеному великому додатку?

Відповіді:


188

Причину легко пояснити: поки підписується обробник подій, видавець події тримає посилання на підписника через делегата обробника події (припустимо, що делегат - це метод примірника).

Якщо видавець живе довше, ніж підписник, він буде тримати підписника в живих, навіть коли немає інших посилань на підписника.

Якщо ви скасуєте підписку на подію з рівним обробником, то так, це видалить обробник і можливий витік. Однак на моєму досвіді це рідко насправді є проблемою - тому що, як правило, я вважаю, що видавець та підписник у будь-якому випадку мають приблизно однаковий термін життя.

Це є можливою причиною ... але в моєму досвіді це досить побите. Звичайно, ваш пробіг може відрізнятися ... вам просто потрібно бути обережним.


... Я бачив, як деякі люди пишуть про це на відповіді на запитання на кшталт "що найчастіше протікає в пам'яті .net".
gillyb

32
Спосіб подолати це з боку видавця - встановити нуль події, як тільки ви впевнені, що більше не будете його знімати. Це неявно видалить усіх передплатників і може бути корисним, коли певні події випускаються лише протягом певних етапів життя об’єкта.
JSB ձոգչ

2
Метод розпорядження був би сприятливим моментом для встановлення
недійсної

6
@DaviFiamenghi: Ну, якщо щось розміщують, то це, принаймні, ймовірний показник того, що він скоро матиме право на вивезення сміття, і тоді не має значення, які підписники є.
Джон Скіт

1
@ BrainSlugs83: "і типовий шаблон події все одно включає відправника" - так, але це виробник події . Зазвичай примірник абонента події є релевантним, а відправник - ні. Так, так, якщо ви можете підписатися за допомогою статичного методу, це не проблема - але це рідко варіант, на мій досвід.
Джон Скіт

13

Так, -=достатньо, однак, можна було б важко відслідковувати кожну призначену подію, будь-коли. (детальніше див. пост Джона). Що стосується дизайнерського шаблону, подивіться на схему слабких подій .



Якщо я знаю, що видавець буде жити довше, ніж підписник, я змушу підписатися IDisposableі скасувати підписку на події.
Шиммі Вайцхандлер

9

Я пояснив цю плутанину в своєму блозі за адресою 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 живий. Але, якщо ви не хотіли, щоб захист, наданий об’єкту абонента події, тоді, можна сказати, сталася витік пам'яті.

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

Ви можете уникнути такого витоку, від'єднавши обробника події.

Як прийняти рішення?

Але у всій вашій кодовій базі є маса подій та обробників подій. Це означає, що вам потрібно тримати відокремлення обробників подій скрізь? Відповідь - Ні. Якщо вам довелося це зробити, ваша кодова база буде дійсно некрасивою з багатослівними.

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

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

Більшу частину часу ви можете виявити, що об’єкт, який підписався на подію, є таким же важливим, як об’єкт видавця події, і обидва, як передбачається, живуть одночасно.

Приклад сценарію, коли вам не потрібно хвилюватися

Наприклад, подія натискання кнопки вікна.

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

Тут видавцем події є кнопка, а підписником події є MainWindow. Застосовуючи цю схему потоку, задайте питання, чи не повинно Головне вікно (абонент події) бути мертвим перед кнопкою (видавець подій)? Очевидно, ні. Це навіть не має сенсу. Тоді навіщо турбуватися про від'єднання обробника подій клацання?

Приклад, коли загін обробника подій ОБОВ'ЯЗКОВО.

Я наведу один приклад, коли передплатний об’єкт повинен бути мертвим перед видавчим об’єктом. Скажімо, ваше головне вікно публікує подію під назвою "SomethingHappened", і ви показуєте дочірнє вікно з головного вікна натисканням кнопки. Дочірнє вікно підписується на цю подію головного вікна.

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

І дочірнє вікно підписується на подію Головного вікна.

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

З цього коду ми можемо чітко зрозуміти, що в головному вікні є кнопка. При натисканні на цю кнопку відображається дочірнє вікно. Дочірнє вікно слухає подію з головного вікна. Зробивши щось, користувач закриває вікно дитини.

Тепер, згідно з графіком поданих мною таблиць, який я надав, якщо ви ставите запитання "Чи повинно дочірнє вікно (абонент події) бути мертвим перед видавцем події (головне вікно)? Відповідь має бути ТАК. Так? Так, від'єднайте обробник події Я зазвичай роблю це з події Unloaded Window.

Правило: Якщо ваш погляд (наприклад, WPF, WinForm, UWP, форма Xamarin тощо) передплачує подію ViewModel, завжди пам’ятайте, що потрібно від'єднати обробник подій. Тому що ViewModel зазвичай живе довше, ніж представлення. Отже, якщо ViewModel не буде знищений, будь-який погляд, який підписався на подію ViewModel, залишиться в пам'яті, що не добре.

Доведення концепції за допомогою профайлера пам'яті.

Це буде не дуже весело, якщо ми не зможемо затвердити концепцію з профілером пам'яті. У цьому експерименті я використав JetBrain dotMemory профілер.

По-перше, я запустив MainWindow, яке відображається так:

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

Потім я зробив знімок пам’яті. Тоді я натискав кнопку 3 рази . З’явилися три дитячі вікна. Я закрив усі ці дочірні вікна і натиснув кнопку Force GC у профілі dotMemory, щоб переконатися, що викликається збирач сміття. Потім я зробив ще один знімок пам'яті і порівняв його. Ось! наш страх був правдивим. Дитяче вікно збирач сміття не збирався навіть після їх закриття. Не тільки це, але і кількість протікаючих об'єктів для об'єкта ChildWindow також показана " 3 " (я натиснув кнопку 3 рази, щоб показати 3 дочірні вікна).

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

Добре, тоді я відключив обробник подій, як показано нижче.

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

Потім я виконав ті ж дії і перевірив профайлер пам'яті. Цього разу, вау! більше немає витоку пам'яті.

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


3

Подія дійсно пов'язаний список обробників подій

Якщо ви зробите + = новий EventHandler для події, це насправді не має значення, якщо ця конкретна функція була додана як слухач раніше, вона буде додана один раз на + =.

Коли подія піднімається, вона переходить через пов'язаний список, пункт за пунктом і викликає всі методи (обробники подій), додані до цього списку, ось чому обробники подій все ще викликаються, навіть коли сторінки більше не працюють, поки вони живі (вкорінені), і будуть живі, доки вони підчеплені. Тож вони будуть дзвонити до тих пір, поки eventhandler не розв’яжеться з - = новим EventHandler.

Дивіться тут

та MSDN ТУТ


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.