[英]Why is this NotifyCollectionChanged causing memory leak?
我正在使用 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) {//Check if the backgroud update has completed
CopyData();//Copy the data but not update UI
double deltaTime = stopwatch.Elapsed.TotalSeconds;
stopwatch.Restart();
task = gameWorld.BeginUpdate(deltaTime);//Let backgroud game engine calculate next frame of the game.
NotifyChange();//Update UI, runing concurrently with backgroud game engine thread. After this line is removed, the memory leak doesn't occur any more.
}
Task task_forLambda = task;
Action action = () => DispatherLoopCallback(task_forLambda);
Application.Current.Dispatcher.BeginInvoke(action, DispatcherPriority.Background);//Send next call of this method to the dispatcher, leave space for other WPF process.
}
private void CopyData() {
GameBodyCollectionViewModel.CopyData();
}
private void NotifyChange() {
GameBodyCollectionViewModel.NotifyChange();
}
}
然而,当游戏运行时,即使什么都没有,memory 的使用量却不断增加。 当游戏暂停时,这种增加停止。 所以我确信存在 memory 泄漏。 经过调查,我发现问题来自 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));//After this line is removed, the memory leak doesn't happen any more.
}
}
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 后,memory 泄漏没有发生。
更新1
根据我观察到的情况,我做了一个演示项目,以便观众获得更好的信息并能够重现错误。 由于 StackOverFlow 不支持文件附加,并且同样的内容也发布在 github 上,因此我在此处发布该帖子的链接。 您可以转到该链接下载演示。 https://github.com/dotnet/wpf/issues/5739
更新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。 然而问题依然存在
经过多次尝试,我最终自己找到了答案。 我没有继续创建新实例,而是将其修改为仅在从 model 检索到的集合具有更多项目计数时添加新项目。 如果源的计数较少,则每次从 model 复制数据只是更新现有实例。
这是最终的代码:
public class DemoCollectionViewModel : CollectionViewModel<DemoItemViewModel> {
public DemoCollectionViewModel() {
}
public override IEnumerator<DemoItemViewModel> GetEnumerator() => copiedData.GetEnumerator();
private readonly List<DemoItemViewModel> copiedData = new List<DemoItemViewModel>();
private int includedCount = 0;
internal void CopyData() {
int i = 0;
SimulatedModelItem[] simulatedModelItems = SimulatedModel.Items.ToArray();
foreach (SimulatedModelItem smi in simulatedModelItems) {
if (copiedData.Count == i) {
DemoItemViewModel demoItemViewModel = new DemoItemViewModel();
copiedData.Add(demoItemViewModel);
addRecord.Add(demoItemViewModel);
}
copiedData[i].CopyData(smi);
i++;
}
for (int j = i; j < includedCount; j++) {
copiedData[j].IsEmpty = true;
}
includedCount = i;
}
private readonly List<DemoItemViewModel> addRecord = new List<DemoItemViewModel>();
private int lastNotifyIncludedCount = 0;
internal void NotifyChange() {
foreach (DemoItemViewModel demoItemViewModel in addRecord) {
NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, demoItemViewModel));
}
addRecord.Clear();
int greaterInt = Math.Max(includedCount, lastNotifyIncludedCount);
for (int i = 0; i < greaterInt; i++) {
copiedData[i].NotifyChange();
}
lastNotifyIncludedCount = includedCount;
}
}
public class DemoViewModel : ViewModel {
public DemoViewModel() {
DemoCollectionViewModel = new DemoCollectionViewModel();
Proceed();
}
public DemoCollectionViewModel DemoCollectionViewModel { get; }
private bool isProceed = false;
public void Pause() {
if (!isProceed) throw new InvalidOperationException("The GameWorld is already paused.");
isProceed = false;
}
public void Proceed() {
if (isProceed) throw new InvalidOperationException("The GameWorld is already proceeding.");
isProceed = true;
Action action = () => DispatherLoopCallback();
Application.Current.Dispatcher.BeginInvoke(action, DispatcherPriority.Background);
}
private void DispatherLoopCallback() {
if (!isProceed) return;
CopyData();
NotifyChange();
Application.Current.Dispatcher.BeginInvoke(DispatherLoopCallback, DispatcherPriority.Background);//Send next call of this method to the dispatcher, leave space for other WPF process.
}
private void CopyData() {
DemoCollectionViewModel.CopyData();
}
private void NotifyChange() {
DemoCollectionViewModel.NotifyChange();
}
}
演示的最终版本,修复了 memory 泄漏错误,也可在 github 帖子https://github.com/dotnet/wpf/issues中找到
如INotifyCollectionChanged 中所述,不更新 UI INotifyCollectionChanged 应该由集合实现,而不是由视图 model 实现,这里最好只使用 ObservableCollection ,它会在集合更改时自动处理更新 UI 并摆脱 GameBodyCollectionViewModel.NotifyChange ()。
我很抱歉我之前没有注意到 IEnumerable。 泄漏很可能是因为您每次调用循环时都使用“NotifyCollectionChangedAction.Reset”,这将触发集合中所有项目的 UI 更新,无论是否有任何更改。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.