为什么WPF中触发NotifyCollectionChanged会引发内存泄漏?
我使用WPF作为UI框架开发游戏,游戏引擎本身不会自行变更状态,仅当UI应用主动调用通知其更新到下一帧时才会发生变化。
以下是我的核心代码:
public class GameWorldViewModel : ViewModel { public GameWorldViewModel(GameWorld gameWorld) { this.gameWorld = gameWorld; GameBodyCollectionViewModel = new GameBodyCollectionViewModel(gameWorld); CopyData(); Proceed(); } public GameBodyCollectionViewModel GameBodyCollectionViewModel { get; init; } private readonly GameWorld gameWorld; private readonly Stopwatch stopwatch = new Stopwatch(); private bool isProceed = false; public void Pause() { if (!isProceed) throw new InvalidOperationException("The GameWorld is already paused."); isProceed = false; stopwatch.Reset(); } public void Proceed() { if (isProceed) throw new InvalidOperationException("The GameWorld is already proceeding."); isProceed = true; Action action = () => DispatherLoopCallback(Task.CompletedTask); stopwatch.Start(); Application.Current.Dispatcher.BeginInvoke(action, DispatcherPriority.Background); } private void DispatherLoopCallback(Task task) { if (!isProceed) return; if (task.IsCompleted) {//检查后台更新是否完成 CopyData();//复制数据但不更新UI double deltaTime = stopwatch.Elapsed.TotalSeconds; stopwatch.Restart(); task = gameWorld.BeginUpdate(deltaTime);//让后台游戏引擎计算下一帧游戏数据 NotifyChange();//更新UI,与后台游戏引擎线程并发运行,移除该行后内存泄漏不再发生 } Task task_forLambda = task; Action action = () => DispatherLoopCallback(task_forLambda); Application.Current.Dispatcher.BeginInvoke(action, DispatcherPriority.Background);//将该方法的下一次调用发送到调度器,为其他WPF进程留出资源 } private void CopyData() { GameBodyCollectionViewModel.CopyData(); } private void NotifyChange() { GameBodyCollectionViewModel.NotifyChange(); } }
但游戏运行时即便没有任何渲染内容,内存占用也会持续升高,游戏暂停后升高趋势就会停止,可以确定存在内存泄漏问题。排查后发现问题出在NotifyChange()方法,但我定位不到这些ViewModel类的问题点。
public class GameBodyCollectionViewModel : CollectionViewModel<GameBodyViewModel> { public GameBodyCollectionViewModel(GameWorld gameWorld) { this.gameWorld = gameWorld; } private readonly GameWorld gameWorld; public override IEnumerator<GameBodyViewModel> GetEnumerator() => copiedData.GetEnumerator(); internal void CopyData() { copiedData.Clear(); copiedData.AddRange(from gb in gameWorld.GetGameBodies() select new GameBodyViewModel(gb)); } private readonly List<GameBodyViewModel> copiedData = new List<GameBodyViewModel>(); internal void NotifyChange() { NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));//移除该行后内存泄漏不再发生 } }
public class GameBodyViewModel : ViewModel { public GameBodyViewModel(GameBody gameBody) { AABBLowerX = gameBody.AABB.LowerBound.X; AABBLowerY = gameBody.AABB.LowerBound.Y; AABBWidth = gameBody.AABB.Width; AABBHeight = gameBody.AABB.Height; } public double AABBLowerX { get; } public double AABBLowerY { get; } public double AABBWidth { get; } public double AABBHeight { get; } }
public abstract class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged([CallerMemberName] String propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public abstract class CollectionViewModel<T> : ViewModel, INotifyCollectionChanged, IEnumerable<T> { public abstract IEnumerator<T> GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public event NotifyCollectionChangedEventHandler CollectionChanged; protected void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); } }
我原本认为视图的XAML代码和问题无关就没有附上,如果需要更多信息可以告知。
更新0
我之前忽略XAML代码是错误的,实测它和问题相关,代码如下:
<v:View x:TypeArguments="local:GameBodyCollectionViewModel" x:Name="view" x:Class="Enigma.GameWPF.Visual.Game.GameBodyCollectionView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:v ="clr-namespace:Enigma.GameWPF.Visual" xmlns:local="clr-namespace:Enigma.GameWPF.Visual.Game" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <ItemsControl ItemsSource="{Binding ViewModel,ElementName=view}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas></Canvas> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Left" Value="{Binding AABBLowerX}"/> <Setter Property="Canvas.Bottom" Value="{Binding AABBLowerY}"/> </Style> </ItemsControl.ItemContainerStyle> <ItemsControl.ItemTemplate> <DataTemplate> <local:GameBodyView ViewModel="{Binding}" Width="{Binding AABBWidth}" Height="{Binding AABBHeight}"></local:GameBodyView> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </v:View>
移除整个ItemsControl后内存泄漏就不再出现。
更新1
基于观测结果我制作了可复现问题的演示项目。
更新2
基于已有的测试结果,我尝试改用.NET自带的ObservableCollection替代自行实现的INotifyCollectionChanged,修改后的DemoCollectionViewModel代码如下:
public class DemoCollectionViewModel : ViewModel { public DemoCollectionViewModel() { DemoItemViewModels = new ObservableCollection<DemoItemViewModel>(); } public ObservableCollection<DemoItemViewModel> DemoItemViewModels { get; } private readonly List<DemoItemViewModel> copiedData = new List<DemoItemViewModel>(); internal void CopyData() { copiedData.Clear(); copiedData.AddRange(from SimulatedModelItem smi in SimulatedModel.GetItems select new DemoItemViewModel(smi)); } internal void NotifyChange() { DemoItemViewModels.Clear(); foreach (DemoItemViewModel vm in copiedData) { DemoItemViewModels.Add(vm); } } }
同时我将视图中ItemsControl的ItemsSource绑定到该ObservableCollection,但问题仍然存在。
内容的提问来源于stack exchange,提问作者martinrhan
相关产品推荐
相关产品推荐

