WPF 界面卡顿但 CPU 不高,大概率是内存泄漏导致 GPU 资源未释放;需重点检查 UIElement、BitmapImage 等对象的托管引用未断开,尤其是 DataContext 绑定、事件监听未解注册及视觉树未彻底清理。

WPF 界面卡顿但 CPU 不高,大概率是内存没释放
WPF 的 UIElement、BitmapImage、Brush 等对象会隐式持有对渲染资源(如 D3D 纹理、GPU 内存)的引用,即使控件已从视觉树移除,只要。NET 对象还在 GC 堆上,底层资源就可能不释放。典型表现是:任务管理器里进程的“专用工作集”持续上涨,滚动列表或频繁切换 Tab 时越来越卡。
- 别只盯着
GC.Collect()——它不触发 WPF 的资源清理,得先断开所有托管引用链 - 重点检查
DataContext是否还绑着旧 ViewModel,尤其用了INotifyPropertyChanged且没手动Unregister事件 - 用
VisualTreeHelper.GetChildrenCount()快速确认控件是否真被移除了(返回 0 才安全) -
Image.Source设为null后,记得再调一次BitmapImage.CacheOption = BitmapCacheOption.OnLoad(如果之前是OnDemand)
Binding 泄漏:DataContext 没清,后台线程还在发通知
最常见的内存泄漏源头。比如页面 A 绑定了 ViewModelA,跳转到页面 B 后,ViewModelA 的 PropertyChanged 事件仍被页面 A 的 TextBox 等控件监听,而页面 A 的实例因事件引用未被回收,连带整个 ViewModel 和其持有的集合、图片流都悬在内存里。
- 不用手写
INotifyPropertyChanged时,优先选ObservableObject(CommunityToolkit.Mvvm),它自带WeakEventManager支持 - 手动实现时,
PropertyChanged事件必须用WeakReference包装监听器,或在OnDetachedFromVisualTree里显式-= - 避免在 ViewModel 里直接订阅
DispatcherTimer.Tick或Task.ContinueWith——这些回调会强引用 ViewModel - 调试时打开
Debug.WriteLine打点,在 ViewModel 构造 / 析构里输出,看析构函数是否被调用
BitmapImage 加载大图后内存不降,不是缓存问题而是解码没完成
BitmapImage默认异步解码,设 Source 后立刻返回,但像素数据实际还在后台线程加载。如果页面关闭太快,解码线程可能卡住,导致 BitmapImage 对象无法被 GC,GPU 内存也一直占着。
- 强制同步加载:
BitmapImage.BeginInit(); BitmapImage.UriSource = uri; BitmapImage.CacheOption = BitmapCacheOption.OnLoad; BitmapImage.EndInit(); - 加载前先用
BitmapDecoder.Create()读头信息判断尺寸,超 2000×2000 的图建议预缩放再加载 - 别反复给同一个
Image控件赋新Source——每次都会新建BitmapImage,旧的未必及时释放;改用Image.Source = null+GC.Collect(2, GCCollectionMode.Forced)(仅调试用) - 注意
BitmapImage.StreamSource必须是可重读流(如MemoryStream),文件流关了就解码失败,错误信息是"Cannot access a closed Stream"
自定义控件里用 Canvas 或 DrawingVisual,忘了 Detach
继承 UIElement 写绘图控件时,如果用 DrawingVisual 或Canvas.Children.Add()动态加元素,离开页面后不清理,这些对象会持续占用渲染资源,且不会出现在常规对象引用分析里。
- 重写
OnVisualParentChanged,当visualParent为null时,调用ClearValue(DrawingVisual.DrawingProperty)或Children.Clear() - 用
RenderTargetBitmap截图后,记得Freeze()再赋值给Image.Source,否则它会持续监听源控件变化 - 避免在
OnRender里反复new GeometryDrawing——Geometry 对象本身不轻量,应缓存复用 - 检查
VisualTreeHelper.GetParent()返回null时是否执行了资源释放逻辑,这是判断控件是否脱离视觉树的最准依据
WPF 的内存问题往往藏在“看起来已经删了”的地方——比如一个没 Dispose() 的MediaPlayer、一个没 Stop() 的Storyboard、甚至一个 ToolTip 还在偷偷挂着。工具上,用 PerfView 抓 GCHeapAlloc 事件比看任务管理器更准,重点关注 System.Windows.Media 命名空间下的类型实例数。