你好,欢迎来到第六章。经过前五章的学习,我们已经能够识别内存泄漏、理解其原理、使用工具定位问题。现在,是时候给出系统的解决方案了。
这一章,我们将针对第三章列举的所有泄漏场景,逐一给出可落地的修复方案 。同时,我会重点讲解WPF开发中最重要的两个防泄漏武器:弱事件模式(Weak Event Pattern) 和 IDisposable的正确实现。
1. 解决方案速查表
在深入细节之前,先给你一张快速对照表,方便日常开发时查阅:
| 泄漏场景 | 核心解决方案 | 代码示例关键词 |
|---|---|---|
| 事件订阅未取消 | 在 Dispose/OnClosed/Unloaded 中取消订阅 |
-= 操作符 |
| 静态事件 | 使用弱事件模式;或在适当时机取消订阅 | WeakEventManager |
| 静态集合 | 限制容量;使用弱引用集合 | ConditionalWeakTable |
| Lambda捕获this | 改用命名方法订阅 | 不使用 => 订阅事件 |
| DispatcherTimer | 停止定时器并取消订阅 | timer.Stop() + -= |
| 数据绑定泄漏 | 清理绑定;使用弱事件模式 | ClearAllBindings |
| 非托管资源 | 实现 IDisposable + 使用 using |
Dispose() 方法 |
| x:Name注册 | 动态移除时调用 UnregisterName |
UnregisterName() |
| 大对象碎片 | 使用数组池;启用LOH压缩 | ArrayPool<T> |
2. 武器一:弱事件模式(Weak Event Pattern)
这是WPF中最重要的防泄漏机制。在深入之前,我们先理解它为什么是必要的。
2.1 问题的本质
回顾一下泄漏的核心:长生命周期对象持有短生命周期对象的引用。
text
[长生命事件源] --强引用--> [订阅者对象] ❌ 导致泄漏
如果事件源的生命周期你无法控制(比如它是静态的、系统级的),那么唯一的办法就是让事件源通过弱引用来持有订阅者。
2.2 WeakEventManager 的工作原理
WPF提供了一个内置的解决方案:WeakEventManager。它的工作方式如下:
-
事件源不直接持有订阅者的强引用。
-
取而代之,
WeakEventManager使用WeakReference来跟踪订阅者。 -
当订阅者被GC回收时,
WeakEventManager自动清理对应的引用。 -
事件触发时,
WeakEventManager只通知那些"还活着"的订阅者。
2.3 如何使用 WeakEventManager
场景:假设你有一个自定义事件源,需要支持弱事件订阅。
步骤1:创建自定义的 WeakEventManager
csharp
public class MyEventSource
{
public event EventHandler<MyEventArgs> MyEvent;
// 提供一个弱事件订阅方法
public void AddWeakEventListener(IWeakEventListener listener)
{
MyEventWeakEventManager.AddListener(this, listener);
}
public void RemoveWeakEventListener(IWeakEventListener listener)
{
MyEventWeakEventManager.RemoveListener(this, listener);
}
}
// 自定义WeakEventManager
public class MyEventWeakEventManager : WeakEventManager
{
private static MyEventWeakEventManager _currentManager;
public static void AddListener(MyEventSource source, IWeakEventListener listener)
{
CurrentManager.ProtectedAddListener(source, listener);
}
public static void RemoveListener(MyEventSource source, IWeakEventListener listener)
{
CurrentManager.ProtectedRemoveListener(source, listener);
}
private static MyEventWeakEventManager CurrentManager
{
get
{
if (_currentManager == null)
{
_currentManager = new MyEventWeakEventManager();
_currentManager.ProtectedInit();
}
return _currentManager;
}
}
protected override void StartListening(object source)
{
var eventSource = source as MyEventSource;
eventSource.MyEvent += OnMyEvent;
}
protected override void StopListening(object source)
{
var eventSource = source as MyEventSource;
eventSource.MyEvent -= OnMyEvent;
}
private void OnMyEvent(object sender, MyEventArgs e)
{
// 通知所有仍然存活的监听者
DeliverEvent(sender, e);
}
}
步骤2:订阅者实现 IWeakEventListener 接口
csharp
public class MySubscriber : IWeakEventListener
{
public MySubscriber(MyEventSource source)
{
// 使用弱事件方式订阅
source.AddWeakEventListener(this);
}
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(MyEventWeakEventManager))
{
// 处理事件
var args = e as MyEventArgs;
// ... 处理逻辑
return true;
}
return false;
}
}
2.4 简化的方式:使用 WeakEvent 辅助类
如果觉得上面的方式太繁琐,可以使用 .NET 提供的 WeakEvent<TEventSource, TEventArgs> 辅助类(.NET Core 3.0+):
csharp
public class MyEventSource
{
// 使用弱事件替代普通事件
public WeakEvent<MyEventSource, MyEventArgs> MyWeakEvent = new WeakEvent<MyEventSource, MyEventArgs>();
public void RaiseEvent()
{
MyWeakEvent.Raise(this, new MyEventArgs());
}
}
// 订阅
var source = new MyEventSource();
source.MyWeakEvent.Subscribe((s, e) =>
{
// 处理事件
});
// 不需要取消订阅,因为它是弱引用
3. 武器二:IDisposable 的正确实现
对于需要释放资源的对象,正确实现 IDisposable 是基本功,也是防止资源泄漏的关键。
3.1 标准实现模板
这是一个经过实践检验的 IDisposable 实现模板,适用于包含托管和非托管资源的类:
csharp
public class MyResourceHolder : IDisposable
{
// 非托管资源
private IntPtr _nativeResource;
// 托管资源(实现了IDisposable的对象)
private FileStream _fileStream;
private EventHandler _someEventHandler;
private MyEventSource _eventSource;
// 是否已释放的标志
private bool _disposed = false;
public MyResourceHolder()
{
_nativeResource = NativeMethods.Allocate();
_fileStream = new FileStream("data.bin", FileMode.Open);
_eventSource = new MyEventSource();
_someEventHandler = OnSomeEvent;
_eventSource.SomeEvent += _someEventHandler;
}
// 公开的Dispose方法
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 告知GC不需要再调用终结器
}
// 受保护的Dispose方法,供子类调用
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// ✅ 释放托管资源
// 1. 取消事件订阅
if (_eventSource != null)
{
_eventSource.SomeEvent -= _someEventHandler;
_eventSource = null;
}
// 2. 释放IDisposable对象
_fileStream?.Dispose();
_fileStream = null;
}
// ✅ 释放非托管资源
if (_nativeResource != IntPtr.Zero)
{
NativeMethods.Free(_nativeResource);
_nativeResource = IntPtr.Zero;
}
_disposed = true;
}
// 终结器(仅作为后备方案)
~MyResourceHolder()
{
Dispose(false);
}
private void OnSomeEvent(object sender, EventArgs e)
{
// 处理事件
}
}
3.2 使用模式
使用 using 语句(推荐):
csharp
using (var holder = new MyResourceHolder())
{
// 使用holder
} // 自动调用Dispose
手动调用 Dispose:
csharp
var holder = new MyResourceHolder();
try
{
// 使用holder
}
finally
{
holder.Dispose();
}
4. 各场景详细解决方案
4.1 事件订阅未取消
最佳实践:在订阅时,就规划好取消订阅的位置。
csharp
public class MyViewModel : INotifyPropertyChanged, IDisposable
{
private EventHandler _handler;
public MyViewModel()
{
_handler = OnSomethingChanged;
GlobalEventSource.SomethingChanged += _handler;
}
public void Dispose()
{
GlobalEventSource.SomethingChanged -= _handler;
}
}
设计模式 :如果对象是UI元素,在 Unloaded 事件中取消订阅:
csharp
public partial class MyControl : UserControl
{
public MyControl()
{
InitializeComponent();
this.Unloaded += OnUnloaded;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// 订阅事件
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
// 取消所有订阅
}
}
4.2 静态事件
静态事件是"根"级别的,最容易导致泄漏。三种应对策略:
| 策略 | 适用场景 | 代码示例 |
|---|---|---|
| 使用弱事件模式 | 通用,推荐 | SystemEvents.DisplaySettingsChanged 用 WeakEventManager |
| 取消订阅 | 能够确定对象生命周期结束点 | 在 Dispose 中取消 |
| 避免使用静态事件 | 设计阶段 | 改用实例事件 + 单例模式 |
4.3 静态集合
原则 :如果必须使用静态集合,务必实现容量上限 和清理策略。
csharp
public class SafeCache<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
private readonly int _maxSize;
private readonly Queue<TKey> _accessOrder = new Queue<TKey>();
public SafeCache(int maxSize = 100)
{
_maxSize = maxSize;
}
public void Add(TKey key, TValue value)
{
if (_cache.Count >= _maxSize)
{
// LRU淘汰策略
var oldest = _accessOrder.Dequeue();
_cache.Remove(oldest);
}
_cache[key] = value;
_accessOrder.Enqueue(key);
}
public TValue Get(TKey key)
{
if (_cache.TryGetValue(key, out var value))
{
// 更新访问顺序
_accessOrder = new Queue<TKey>(_accessOrder.Where(k => !k.Equals(key)));
_accessOrder.Enqueue(key);
return value;
}
return default;
}
}
4.4 Lambda捕获this
规则 :除非你非常清楚自己在做什么,否则永远不要用Lambda订阅需要在未来取消的事件。
csharp
// ❌ 错误 - 无法取消
timer.Tick += (s, e) => UpdateClock();
// ✅ 正确 - 可以取消
timer.Tick += OnTimerTick;
// 取消时:timer.Tick -= OnTimerTick;
private void OnTimerTick(object sender, EventArgs e) { UpdateClock(); }
4.5 DispatcherTimer
原则 :每个 DispatcherTimer 必须在所有者销毁前停止并清理。
csharp
public partial class MyPage : Page
{
private DispatcherTimer _timer;
public MyPage()
{
InitializeComponent();
_timer = new DispatcherTimer();
_timer.Tick += OnTick;
_timer.Start();
}
// 在Page卸载时清理
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
if (_timer != null)
{
_timer.Stop();
_timer.Tick -= OnTick;
_timer = null;
}
}
}
4.6 数据绑定清理
当动态创建绑定时,记得清理:
csharp
public void CleanupBindings(DependencyObject element)
{
// 清除所有本地绑定
BindingOperations.ClearAllBindings(element);
// 如果有子元素,递归清理
var children = VisualTreeHelper.GetChildrenCount(element);
for (int i = 0; i < children; i++)
{
var child = VisualTreeHelper.GetChild(element, i);
CleanupBindings(child);
}
}
4.7 非托管资源
始终遵循:谁分配,谁释放。
csharp
public class BitmapHandler : IDisposable
{
private Bitmap _bitmap; // 托管资源,但内部使用非托管GDI资源
public BitmapHandler(string path)
{
_bitmap = new Bitmap(path);
}
public void Dispose()
{
_bitmap?.Dispose();
_bitmap = null;
}
}
5. 设计层面的最佳实践
5.1 使用"弱引用容器"管理子对象
如果你的类持有大量子对象,可以使用弱引用来管理它们:
csharp
public class Parent
{
// 使用ConditionalWeakTable管理子对象,不会阻止子对象被GC
private ConditionalWeakTable<Child, object> _children
= new ConditionalWeakTable<Child, object>();
public void RegisterChild(Child child)
{
_children.Add(child, null);
}
}
5.2 实现"可取消订阅"的辅助类
封装一个辅助类,管理所有需要取消的订阅:
csharp
public class SubscriptionManager : IDisposable
{
private readonly List<Action> _unsubscribeActions = new List<Action>();
public void Add(Action subscribe, Action unsubscribe)
{
subscribe();
_unsubscribeActions.Add(unsubscribe);
}
public void Dispose()
{
foreach (var action in _unsubscribeActions)
{
action?.Invoke();
}
_unsubscribeActions.Clear();
}
}
// 使用示例
public class MyViewModel : IDisposable
{
private readonly SubscriptionManager _subscriptions = new SubscriptionManager();
public MyViewModel()
{
_subscriptions.Add(
() => GlobalEvent.Something += OnSomething,
() => GlobalEvent.Something -= OnSomething
);
}
public void Dispose() => _subscriptions.Dispose();
}
6. 本章小结
| 核心武器 | 使用场景 | 关键代码 |
|---|---|---|
| 弱事件模式 | 长生命周期事件源 | WeakEventManager / WeakEvent<T> |
| IDisposable | 释放托管+非托管资源 | 标准模板 + using |
| 事件取消 | 短生命周期订阅者 | -= 操作符 |
| SubscriptionManager | 管理多个订阅 | 批量取消的辅助类 |
三个黄金法则:
-
谁订阅,谁取消 ------ 确保每个
+=都有一个对应的-=。 -
长生命周期事件源,用弱事件 ------ 静态事件、系统事件必须使用弱引用方式订阅。
-
所有资源持有者,实现IDisposable ------ 包括事件订阅、定时器、文件句柄等。