Mengapa dan Bagaimana cara menghindari kebocoran memori Event Handler?

154

Saya baru saja menyadari, dengan membaca beberapa pertanyaan dan jawaban di StackOverflow, bahwa menambahkan pengendali event menggunakan +=C # (atau saya kira, bahasa .net lainnya) dapat menyebabkan kebocoran memori umum ...

Saya telah menggunakan event handler seperti ini di masa lalu berkali-kali, dan tidak pernah menyadari bahwa mereka dapat menyebabkan, atau menyebabkan, kebocoran memori pada aplikasi saya.

Bagaimana cara kerjanya (artinya, mengapa ini sebenarnya menyebabkan kebocoran memori)?
Bagaimana saya bisa memperbaiki masalah ini? Apakah cukup menggunakan -=event handler yang sama?
Apakah ada pola desain umum atau praktik terbaik untuk menangani situasi seperti ini?
Contoh: Bagaimana saya seharusnya menangani aplikasi yang memiliki banyak utas berbeda, menggunakan banyak penangan acara yang berbeda untuk mengangkat beberapa acara di UI?

Apakah ada cara yang baik dan sederhana untuk memonitor ini secara efisien dalam aplikasi besar yang sudah dibangun?

gillyb
sumber

Jawaban:

188

Penyebabnya mudah untuk dijelaskan: ketika pengendali acara berlangganan, penerbit acara memegang referensi ke pelanggan melalui delegasi pengendali acara (dengan asumsi delegasi adalah metode contoh).

Jika penerbit hidup lebih lama dari pelanggan, maka itu akan membuat pelanggan tetap hidup bahkan ketika tidak ada referensi lain kepada pelanggan.

Jika Anda berhenti berlangganan dari acara dengan penangan yang sama, maka ya, itu akan menghapus penangan dan kemungkinan kebocoran. Namun, dalam pengalaman saya ini jarang benar-benar masalah - karena biasanya saya menemukan bahwa penerbit dan pelanggan memiliki umur yang kira-kira sama.

Ini adalah penyebab yang mungkin ... tapi menurut pengalaman saya agak berlebihan. Jarak tempuh Anda mungkin berbeda, tentu saja ... Anda hanya perlu berhati-hati.

Jon Skeet
sumber
... Saya telah melihat beberapa orang menulis tentang ini pada jawaban atas pertanyaan seperti "apa kebocoran memori yang paling umum di .net".
gillyb
32
Cara untuk menyiasatinya dari sisi penerbit adalah mengatur acara menjadi nol setelah Anda yakin tidak akan memecatnya lagi. Ini secara implisit akan menghapus semua pelanggan, dan dapat berguna ketika peristiwa tertentu hanya dipecat selama tahap tertentu dari masa hidup objek.
JSB ձոգչ
2
Metode dipose akan menjadi momen yang baik untuk menyetel acara menjadi nol
Davi Fiamenghi
6
@DaviFiamenghi: Ya, jika ada sesuatu yang dibuang, itu setidaknya indikasi yang mungkin akan memenuhi syarat untuk pengumpulan sampah segera, pada titik mana tidak masalah pelanggan apa yang ada.
Jon Skeet
1
@ BrainSlugs83: "dan pola acara tipikal termasuk pengirim" - ya, tapi itulah produser acara . Biasanya instance pelanggan acara relevan, dan pengirimnya tidak. Jadi ya, jika Anda dapat berlangganan menggunakan metode statis, ini bukan masalah - tapi itu jarang menjadi pilihan dalam pengalaman saya.
Jon Skeet
13

Ya, -=sudah cukup, Namun, bisa sangat sulit untuk melacak setiap acara yang ditugaskan, selamanya. (untuk detail, lihat posting Jon). Mengenai pola desain, lihat pola acara yang lemah .

Femaref
sumber
1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx versi 4.0 masih memilikinya.
Femaref
Jika saya tahu penerbit akan hidup lebih lama dari pelanggan, saya membuat pelanggan IDisposabledan berhenti berlangganan dari acara.
Shimmy Weitzhandler
9

Saya telah menjelaskan kebingungan ini dalam sebuah blog di https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Saya akan mencoba merangkumnya di sini sehingga Anda dapat memiliki gagasan yang jelas.

Referensi berarti, "Perlu":

Pertama-tama, Anda perlu memahami bahwa, jika objek A memiliki referensi ke objek B, maka, itu berarti, objek A membutuhkan objek B agar berfungsi, bukan? Jadi, pengumpul sampah tidak akan mengumpulkan objek B selama objek A masih hidup dalam memori.

Saya pikir bagian ini harus jelas bagi pengembang.

+ = Berarti, menyuntikkan referensi objek sisi kanan ke objek kiri:

Tetapi, kebingungan berasal dari operator C # + =. Operator ini tidak dengan jelas memberi tahu pengembang bahwa, sisi kanan operator ini sebenarnya menyuntikkan referensi ke objek sisi kiri.

masukkan deskripsi gambar di sini

Dan dengan melakukan itu, objek A berpikir, perlu objek B, meskipun, dari sudut pandang Anda, objek A tidak peduli jika objek B hidup atau tidak. Karena objek A menganggap objek B diperlukan, objek A melindungi objek B dari pengumpul sampah selama objek A masih hidup. Tetapi, jika Anda tidak ingin perlindungan diberikan kepada objek pelanggan acara, maka, Anda dapat mengatakan, kebocoran memori terjadi.

masukkan deskripsi gambar di sini

Anda dapat menghindari kebocoran seperti itu dengan melepaskan event handler.

Bagaimana cara mengambil keputusan?

Tapi, ada banyak acara dan penangan acara di seluruh basis kode Anda. Apakah itu berarti, Anda harus terus melepaskan penangan acara di mana-mana? Jawabannya adalah Tidak. Jika Anda harus melakukannya, basis kode Anda akan sangat jelek dengan verbose.

Anda bisa mengikuti bagan alur sederhana untuk menentukan apakah penangan kejadian yang memisahkan diperlukan atau tidak.

masukkan deskripsi gambar di sini

Sebagian besar waktu, Anda mungkin menemukan objek acara pelanggan sama pentingnya dengan objek penerbit acara dan keduanya seharusnya hidup pada waktu yang sama.

Contoh skenario di mana Anda tidak perlu khawatir

Misalnya, acara klik tombol dari sebuah jendela.

masukkan deskripsi gambar di sini

Di sini, penerbit acara adalah Tombol, dan pelanggan acara adalah MainWindow. Menerapkan bagan alur itu, ajukan pertanyaan, apakah Jendela Utama (pelanggan acara) seharusnya sudah mati sebelum Tombol (penerbit acara)? Jelas Tidak. Benar? Itu bahkan tidak masuk akal. Lalu, mengapa khawatir tentang melepaskan event handler klik?

Contoh ketika pelepasan event handler adalah HARUS.

Saya akan memberikan satu contoh di mana objek pelanggan seharusnya sudah mati sebelum objek penerbit. Katakan, MainWindow Anda menerbitkan acara yang bernama "SomethingHappened" dan Anda menampilkan jendela anak dari jendela utama dengan mengklik tombol. Jendela anak berlangganan acara jendela utama itu.

masukkan deskripsi gambar di sini

Dan, jendela anak berlangganan acara Jendela Utama.

masukkan deskripsi gambar di sini

Dari kode ini, kita dapat dengan jelas memahami bahwa ada tombol di Jendela Utama. Mengklik tombol itu menunjukkan Window Anak. Jendela anak mendengarkan acara dari jendela utama. Setelah melakukan sesuatu, pengguna menutup jendela anak.

Sekarang, sesuai dengan bagan alur yang saya berikan jika Anda mengajukan pertanyaan "Apakah jendela anak (pelanggan acara) seharusnya sudah mati sebelum penerbit acara (jendela utama)? Jawabannya harus YA. Benar? Jadi, lepaskan penyelenggara acara Saya biasanya melakukan itu dari event Window yang tidak diturunkan.

Aturan praktis: Jika tampilan Anda (yaitu WPF, WinForm, UWP, Formulir Xamarin, dll.) Berlangganan ke acara ViewModel, selalu ingat untuk melepaskan pengendali acara. Karena ViewModel biasanya hidup lebih lama daripada tampilan. Jadi, jika ViewModel tidak dihancurkan, setiap tampilan yang berlangganan acara dari ViewModel itu akan tetap tersimpan dalam memori, yang tidak baik.

Bukti konsep menggunakan memory profiler.

Tidak akan terlalu menyenangkan jika kita tidak dapat memvalidasi konsep dengan profiler memori. Saya telah menggunakan JetBrain dotMemory profiler dalam percobaan ini.

Pertama, saya telah menjalankan MainWindow, yang muncul seperti ini:

masukkan deskripsi gambar di sini

Kemudian, saya mengambil snapshot memori. Lalu saya mengklik tombol 3 kali . Tiga jendela anak muncul. Saya telah menutup semua jendela anak dan mengklik tombol Force GC di profiler dotMemory untuk memastikan bahwa Pengumpul Sampah dipanggil. Kemudian, saya mengambil snapshot memori lain dan membandingkannya. Melihat! Ketakutan kami benar. Jendela Anak tidak dikumpulkan oleh pengumpul Sampah bahkan setelah mereka ditutup. Tidak hanya itu, tetapi jumlah objek bocor untuk objek ChildWindow juga ditampilkan " 3 " (Saya mengklik tombol 3 kali untuk menampilkan 3 jendela anak).

masukkan deskripsi gambar di sini

Ok, kalau begitu, saya melepaskan event handler seperti yang ditunjukkan di bawah ini.

masukkan deskripsi gambar di sini

Kemudian, saya telah melakukan langkah yang sama dan memeriksa memori profiler. Kali ini, wow! tidak ada lagi kebocoran memori.

masukkan deskripsi gambar di sini

Emran Hussain
sumber
3

Suatu acara adalah benar-benar daftar tertaut dari penangan acara

Ketika Anda melakukan + = EventHandler baru pada acara tersebut, tidak masalah jika fungsi khusus ini telah ditambahkan sebagai pendengar sebelumnya, itu akan ditambahkan sekali per + =.

Ketika acara dinaikkan itu melalui daftar yang ditautkan, item demi item dan memanggil semua metode (event handler) yang ditambahkan ke daftar ini, inilah mengapa penangan event masih dipanggil bahkan ketika halaman tidak lagi berjalan selama mereka hidup (berakar), dan mereka akan hidup selama mereka terhubung. Jadi mereka akan dipanggil sampai eventhandler tidak terkait dengan EventHandler - = baru.

Lihat disini

dan MSDN DI SINI

TalentTuner
sumber