Olay İşleyici bellek sızıntılarını neden ve nasıl önleyebilirim?


154

StackOverflow'daki bazı soruları ve cevapları okuyarak +=, C # (veya sanırım diğer .net dilleri) kullanarak olay işleyicileri eklemenin yaygın bellek sızıntılarına neden olabileceğini fark ettim ...

Geçmişte bu gibi olay işleyicilerini birçok kez kullandım ve uygulamalarımda bellek sızıntılarına neden olabileceğini veya neden olabileceğini hiç fark etmedim.

Bu nasıl çalışıyor (yani, bu aslında bir bellek sızıntısına neden oluyor)?
Bu sorunu nasıl düzeltebilirim? Kullanarak mı -=aynı olay işleyicisi, yeterli uzunlukta için?
Bunun gibi durumlarla başa çıkmak için ortak tasarım modelleri veya en iyi uygulamalar var mı?
Örnek: Kullanıcı arayüzünde birkaç olay oluşturmak için birçok farklı olay işleyicisi kullanarak, çok farklı iş parçacığı olan bir uygulamayı nasıl ele almam gerekir?

Önceden oluşturulmuş büyük bir uygulamada bunu verimli bir şekilde izlemenin iyi ve basit yolları var mı?

Yanıtlar:


188

Nedeni açıklamak kolaydır: Bir olay işleyicisi abone olurken, olayın yayıncısı , olay işleyicisi temsilcisi aracılığıyla aboneye bir referans tutar (temsilci bir örnek yöntem olduğu varsayılarak).

Yayıncı aboneden daha uzun yaşıyorsa, aboneye başka referans olmasa bile aboneyi canlı tutacaktır.

Eşit bir işleyici ile olaydan aboneliğinizi iptal ederseniz, evet, işleyiciyi ve olası sızıntıyı kaldıracaktır. Ancak, tecrübelerime göre bu nadiren bir problem - çünkü tipik olarak yayıncının ve abonenin kabaca eşit ömürlere sahip olduğunu düşünüyorum.

Bu ise olası bir nedeni ... ama benim deneyim oldukça sinirli bu. Kilometreniz değişebilir, elbette ... sadece dikkatli olmanız gerekiyor.


... ".net'te en yaygın bellek sızıntısı nedir?"
gillyb

32
Yayıncı tarafından bunun üstesinden gelmenin bir yolu, artık tetiklemeyeceğinizden emin olduktan sonra etkinliği null değerine ayarlamaktır. Bu, tüm aboneleri dolaylı olarak kaldıracaktır ve belirli olaylar yalnızca nesnenin ömrünün belirli aşamalarında tetiklendiğinde yararlı olabilir.
JSB ձոգչ

2
Dipoz yöntemi, etkinliği null olarak ayarlamak için iyi bir zaman olurdu
Davi Fiamenghi

6
@DaviFiamenghi: Eğer bir şey elden çıkarılıyorsa, bu en azından yakında çöp toplama için uygun olacağının olası bir göstergesidir, bu noktada hangi abonelerin olduğu önemli değildir.
Jon Skeet

1
@ BrainSlugs83: "ve tipik etkinlik düzeni yine de bir gönderen içeriyor" - evet, ancak etkinlik üreticisi . Genellikle etkinlik abone örneği alakalı olur ve gönderen ilgili değildir. Yani evet, statik bir yöntem kullanarak abone olabilirseniz, bu bir sorun değildir - ancak bu benim deneyimimde nadiren bir seçenektir.
Jon Skeet


10

Bu karışıklığı https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 adresindeki bir blogda açıkladım . Açık bir fikre sahip olabilmek için burada özetlemeye çalışacağım.

Referans, "İhtiyaç" anlamına gelir:

Her şeyden önce, eğer A nesnesi B nesnesine bir başvuru içeriyorsa, bunun A nesnesinin çalışması için B nesnesine ihtiyaç duyduğunu anlamalısınız. Bu nedenle, çöp toplayıcı A nesnesi bellekte olduğu sürece B nesnesini toplamaz.

Bence bu bölüm bir geliştirici için açık olmalı.

+ = Sağ taraftaki nesnenin sol nesneye referansını enjekte etmek anlamına gelir:

Ancak, karışıklık C # + = operatöründen gelir. Bu operatör geliştiriciye, bu operatörün sağ tarafının aslında sol taraftaki nesneye bir referans enjekte ettiğini açıkça söylemez.

resim açıklamasını buraya girin

Ve bunu yaparak, A nesnesinin B nesnesine ihtiyacı olduğunu düşünür, ancak bakış açınızdan, A nesnesinin B nesnesinin yaşayıp yaşamadığını umursamaması gerekir. A nesnesi B nesnesinin gerekli olduğunu düşündüğü için A nesnesi, A nesnesi canlı olduğu sürece B nesnesini çöp toplayıcıdan korur. Ancak, olay abone nesnesine bu korumanın verilmesini istemediyseniz, bir bellek sızıntısı meydana geldiğini söyleyebilirsiniz.

resim açıklamasını buraya girin

Olay gidericiyi ayırarak böyle bir sızıntıyı önleyebilirsiniz.

Nasıl karar verilir?

Ancak, kod tabanınızın tamamında çok sayıda olay ve olay işleyicisi vardır. Bu, olay işleyicilerini her yerde ayırmaya devam etmeniz gerektiği anlamına mı geliyor? Cevap hayır. Bunu yapmak zorunda olsaydınız, kod tabanınız ayrıntılı bir şekilde çirkin olacaktır.

Bir ayırma olayı işleyicisinin gerekli olup olmadığını belirlemek için basit bir akış şemasını takip edebilirsiniz.

resim açıklamasını buraya girin

Çoğu zaman, olay abone nesnesinin olay yayıncı nesnesi kadar önemli olduğunu ve her ikisinin de aynı anda yaşadığı düşünülebilir.

Endişelenmenize gerek olmayan bir senaryo örneği

Örneğin, bir pencerenin düğme tıklama olayı.

resim açıklamasını buraya girin

Burada, olay yayıncısı Düğme ve olay abonesi MainWindow'dur. Bu akış şemasını uygulayarak bir soru sorun, Ana Pencerenin (olay abonesi) Düğme'den (olay yayıncısı) önce ölmüş olması gerekiyor mu? Açıkçası Hayır. Bu hiç mantıklı değil. Öyleyse, tıklama etkinliği işleyicisini ayırma konusunda neden endişeleniyorsunuz?

Bir olay işleyicisi ayrılması GEREKİR olduğunda bir örnek.

Abone nesnesinin yayıncı nesnesinden önce ölmüş olması gereken bir örnek vereceğim. Diyelim ki MainWindow'unuz "SomethingHappened" adlı bir olay yayınlıyor ve ana pencereden bir düğmeyi tıklatarak bir alt pencere görüntülüyorsunuz. Alt pencere, ana pencerenin o olayına abone olur.

resim açıklamasını buraya girin

Ve alt pencere, Ana Pencerenin bir olayına abone olur.

resim açıklamasını buraya girin

Bu koddan, Ana Pencerede bir düğme olduğunu açıkça anlayabiliriz. Bu düğmeye tıklandığında bir Çocuk Penceresi gösterilir. Alt pencere, ana pencereden bir olay dinler. Bir şey yaptıktan sonra kullanıcı alt pencereyi kapatır.

Şimdi, akış şemasına göre, "Alt pencere (olay abonesi) olay yayıncısından (ana pencere) önce ölmüş olmalı mı?" Sorusunu sorarsanız, yanıt EVET olmalıdır. Doğru mu? Bunu genellikle Pencerenin Yüksüz olayından yaparım.

Temel kural: Görünümünüz (örn. WPF, WinForm, UWP, Xamarin Formu vb.) Bir ViewModel olayına abone olursa, her zaman olay işleyiciyi ayırmayı unutmayın. Çünkü bir ViewModel genellikle bir görünümden daha uzun yaşar. Bu nedenle, ViewModel imha edilmezse, o ViewModel'e abone olan herhangi bir görünüm bellekte kalır ve bu iyi değildir.

Bir bellek profiler kullanarak kavramın kanıtı.

Bir bellek profili oluşturucu ile konsepti doğrulayamazsak çok eğlenceli olmaz. Bu deneyde JetBrain dotMemory profiler kullandım.

İlk olarak, MainWindow, bu şekilde ortaya çıktı:

resim açıklamasını buraya girin

Sonra bir anı fotoğrafı çektim. Sonra düğmeyi 3 kez tıkladım . Üç çocuk penceresi geldi. Tüm bu alt pencereleri kapattım ve Çöp Toplayıcı'nın çağrıldığından emin olmak için dotMemory profilindeki GC'yi Zorla düğmesini tıklattım. Sonra başka bir bellek fotoğrafı aldım ve karşılaştırdım. Seyretmek! korkumuz doğruydu. Çocuk Penceresi, kapatıldıktan sonra bile Çöp toplayıcı tarafından toplanmadı. Sadece bu değil, ChildWindow nesnesi için sızan nesne sayısı da " 3 " olarak gösterilir (3 alt pencereyi göstermek için düğmeye 3 kez tıkladım).

resim açıklamasını buraya girin

Tamam, sonra olay işleyicisini aşağıda gösterildiği gibi ayırdım.

resim açıklamasını buraya girin

Sonra aynı adımları uyguladım ve bellek profilini kontrol ettim. Bu sefer, vay! daha fazla bellek sızıntısı yok.

resim açıklamasını buraya girin


3

Bir olay gerçekten bağlantılı olay işleyicileri listesidir

Olayda + = yeni EventHandler yaptığınızda, bu özel işlevin daha önce bir dinleyici olarak eklenip eklenmediği önemli değildir, + = başına bir kez eklenir.

Olay yükseltildiğinde, bağlantılı listeye, öğeye göre ilerler ve bu listeye eklenen tüm yöntemleri (olay işleyicileri) çağırır, bu yüzden sayfalar artık çalışmadıkları sürece bile olay işleyicileri çağrılır. yaşıyorlar (köklü) ve bağlandıkları sürece hayatta olacaklar. Böylece olay işleyici bir - = yeni EventHandler ile çağrılıncaya kadar çağrılırlar.

Buraya Bakın

ve MSDN HERE


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.