托管代碼也會洩漏記憶體——**事件發佈者持有訂閱者的強參考**,導致訂閱者無法被 GC 回收。本文以 SystemEvents.DisplaySettingsChanged 為例實測洩漏,並示範 IDisposable 手動解除與 WeakEvent 弱參考封裝兩種解法。
class DisplaySettingsListener
{
byte[] m_ExtraMemory = new byte[1_000_000]; // 1MB 壓力
public DisplaySettingsListener()
{
SystemEvents.DisplaySettingsChanged += ehDisplaySettingsChanged;
}
void ehDisplaySettingsChanged(object s, EventArgs e) { }
}
for (int i=0; i<5; i++)
{
var listener = new DisplaySettingsListener();
listener = null;
GC.Collect();
Console.WriteLine(GC.GetTotalMemory(true)); // 記憶體持續增長
}
結果:每次迴圈記憶體增加 ~1MB,證實 listener 未被回收,因靜態事件仍持有委托參考。
class DisplaySettingsListener : IDisposable
{
byte[] m_ExtraMemory = new byte[1_000_000];
public DisplaySettingsListener()
{
SystemEvents.DisplaySettingsChanged += ehDisplaySettingsChanged;
}
void ehDisplaySettingsChanged(object s, EventArgs e) { }
public void Dispose()
{
SystemEvents.DisplaySettingsChanged -= ehDisplaySettingsChanged;
}
}
// 使用
using (var listener = new DisplaySettingsListener()) { /* ... */ }
// 或
listener.Dispose(); listener = null; GC.Collect(); // 記憶體穩定
原理:事件存儲 WeakReference 指向目標物件,觸發時檢查目標是否存活,若已回收則自動移除。
public class WeakEvent
{
private readonly List _handlers = new List();
public void Add(EventHandler handler) => _handlers.Add(new WeakReference(handler.Target));
public void Remove(EventHandler handler) { /* 略 */ }
public void Raise(object sender, EventArgs e)
{
var alive = new List();
foreach (var wr in _handlers)
if (wr.Target is EventHandler h) h(sender, e); else alive.Add(wr);
_handlers.Clear(); _handlers.AddRange(alive);
}
}
優點:訂閱者無需實作 IDisposable,生命週期完全由 GC 掌控。缺點:Raise 需遍歷清理失效引用,高頻事件有效能開銷。
Dispose/Unloaded/OnNavigatedFrom 中 -=;WeakEventManager(WPF)或自訂 WeakEvent 基礎設施;