ฉันได้อธิบายความสับสนนี้ในบล็อกที่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 ยังมีชีวิตอยู่ แต่ถ้าคุณไม่ต้องการการป้องกันที่มอบให้กับวัตถุสมาชิกของเหตุการณ์คุณสามารถพูดได้ว่าเกิดการรั่วไหลของหน่วยความจำ
คุณสามารถหลีกเลี่ยงการรั่วไหลดังกล่าวได้โดยถอดตัวจัดการเหตุการณ์
จะตัดสินใจได้อย่างไร?
แต่มีเหตุการณ์และตัวจัดการเหตุการณ์จำนวนมากในฐานรหัสทั้งหมดของคุณ หมายความว่าคุณต้องทำการแยกตัวจัดการเหตุการณ์ออกทุกที่หรือไม่ คำตอบคือไม่ถ้าคุณต้องทำเช่นนั้น codebase ของคุณจะน่าเกลียดด้วย verbose
คุณสามารถติดตามแผนภูมิการไหลแบบง่าย ๆ เพื่อพิจารณาว่าจำเป็นต้องใช้ตัวจัดการเหตุการณ์การถอดหรือไม่
เวลาส่วนใหญ่คุณอาจพบว่าวัตถุสมาชิกของเหตุการณ์มีความสำคัญเท่ากับวัตถุผู้เผยแพร่เหตุการณ์และทั้งสองควรจะมีชีวิตอยู่ในเวลาเดียวกัน
ตัวอย่างของสถานการณ์ที่คุณไม่ต้องกังวล
ตัวอย่างเช่นเหตุการณ์คลิกปุ่มของหน้าต่าง
ที่นี่ผู้เผยแพร่กิจกรรมคือปุ่มและผู้สมัครสมาชิกของเหตุการณ์คือ MainWindow การใช้ผังงานถามคำถามหน้าต่างหลัก (สมาชิกเหตุการณ์) ควรจะตายก่อนปุ่ม (ผู้เผยแพร่เหตุการณ์) หรือไม่ เห็นได้ชัดว่าใช่มั้ย แม้จะไม่สมเหตุสมผลก็ตาม จากนั้นทำไมต้องกังวลเกี่ยวกับการแยกตัวจัดการเหตุการณ์คลิก
ตัวอย่างเมื่อการแยกตัวจัดการเหตุการณ์เป็นสิ่งที่ต้องทำ
ฉันจะให้ตัวอย่างหนึ่งที่วัตถุสมาชิกควรจะตายก่อนวัตถุผู้เผยแพร่ สมมติว่า MainWindow ของคุณเผยแพร่กิจกรรมชื่อ "SomethingHappened" และคุณแสดงหน้าต่างลูกจากหน้าต่างหลักด้วยการคลิกปุ่ม หน้าต่างลูกสมัครสมาชิกเหตุการณ์นั้นของหน้าต่างหลัก
และหน้าต่างลูกสมัครรับข้อมูลเหตุการณ์ของหน้าต่างหลัก
จากรหัสนี้เราสามารถเข้าใจได้อย่างชัดเจนว่ามีปุ่มในหน้าต่างหลัก การคลิกปุ่มนั้นจะแสดงหน้าต่างลูก หน้าต่างลูกฟังเหตุการณ์จากหน้าต่างหลัก หลังจากทำบางสิ่งผู้ใช้ปิดหน้าต่างลูก
ตอนนี้ตามโฟลว์แผนภูมิที่ฉันให้ไว้ถ้าคุณถามคำถามว่า "หน้าต่างลูก (ผู้สมัครสมาชิกเหตุการณ์) ควรจะตายก่อนผู้เผยแพร่เหตุการณ์ (หน้าต่างหลัก) หรือไม่คำตอบควรเป็นใช่ใช่แล้วดังนั้นถอดตัวจัดการเหตุการณ์ ฉันมักจะทำเช่นนั้นจากเหตุการณ์ที่ไม่โหลดของหน้าต่าง
กฎง่ายๆ:ถ้ามุมมองของคุณ (เช่น WPF, WinForm, UWP, แบบฟอร์ม Xamarin เป็นต้น) สมัครรับข้อมูลเหตุการณ์ของ ViewModel อย่าลืมถอดตัวจัดการเหตุการณ์ออก เพราะโดยปกติแล้ว ViewModel จะใช้งานได้ยาวนานกว่ามุมมอง ดังนั้นหาก ViewModel ไม่ถูกทำลายมุมมองใด ๆ ที่เหตุการณ์ที่สมัครเป็นสมาชิกของ ViewModel นั้นจะยังคงอยู่ในหน่วยความจำซึ่งไม่ดี
พิสูจน์แนวคิดโดยใช้หน่วยความจำ profiler
มันจะไม่สนุกถ้าเราไม่สามารถตรวจสอบแนวคิดกับ profiler หน่วยความจำ ฉันใช้ JetBrain dotMemory profiler ในการทดลองนี้
ก่อนอื่นฉันเรียกใช้ MainWindow ซึ่งแสดงดังนี้:
จากนั้นฉันก็ถ่ายภาพหน่วยความจำ แล้วฉันคลิกปุ่ม3 ครั้ง หน้าต่างลูกสามลูกปรากฏขึ้น ฉันปิดหน้าต่างลูกเหล่านั้นทั้งหมดแล้วคลิกปุ่ม Force GC ในตัวสร้างโปรไฟล์ dotMemory เพื่อให้แน่ใจว่ามีการเรียก Garbage Collector จากนั้นฉันใช้สแนปชอตหน่วยความจำอื่นแล้วเปรียบเทียบ ดูเถิด! ความกลัวของเราเป็นจริง หน้าต่างลูกไม่ได้ถูกรวบรวมโดยตัวรวบรวมข้อมูลขยะแม้ว่าจะปิดไปแล้วก็ตาม ไม่เพียงแค่นั้น แต่จำนวนวัตถุที่รั่วไหลออกมาสำหรับวัตถุ ChildWindow ก็แสดงเป็น " 3 " ด้วย (ฉันคลิกปุ่ม 3 ครั้งเพื่อแสดงหน้าต่างลูก 3 ลูก)
ตกลงจากนั้นฉันแยกตัวจัดการเหตุการณ์ตามที่แสดงด้านล่าง
จากนั้นฉันได้ทำตามขั้นตอนเดียวกันและตรวจสอบตัวสร้างหน่วยความจำ คราวนี้ว้าว! ไม่มีการรั่วไหลของหน่วยความจำเพิ่มเติม