【发布时间】:2022-01-03 09:05:24
【问题描述】:
我正在使用 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();
}
}
但是,当游戏运行时,即使什么都没有,内存使用量也会不断增加。当游戏暂停时,这种增加停止。所以我确信存在内存泄漏。经过调查,我发现问题来自 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 后,内存泄漏没有发生。
更新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。但是问题依然存在
【问题讨论】:
-
使用您的调试器和跟踪调用:在
internal void NotifyChange()中设置断点并在 VS 的诊断工具窗口中制作内存快照并找出差异。 -
INotifyCollectionChanged 不会导致内存泄漏,它是一个接口。您对实例的生命周期管理会导致内存泄漏 - 如果有的话。仅仅因为内存消耗增加,您不能假设内存泄漏。垃圾收集器不会立即收集对象。似乎您一直在以相对较高的频率更换所有项目。这会强制 ItemsControl 清除其所有项目容器并生成新容器。旧容器将保留在内存中。在这种情况下,您通常会使用 UI 虚拟化,尤其是容器回收。
-
重用容器可以避免那些不必要的内存分配和 UI 元素初始化。目前尚不清楚您实际上在做什么。如果你真的需要一个 Canvas,你必须实现容器回收和可能的 UI 虚拟化。这意味着您必须在自定义 Canvas 中实现这些功能。确保不再引用丢弃的项目。使用内存分析器来断言堆上的确切对象以及使它们保持活动/可访问的引用。如果这真的是内存泄漏,那么它是由您的对象生命周期管理而不是框架引起的。
-
当您的数据模型实现 INotifyPropertyChanged 时,您可能可以避免清除源集合,而是更新现有模型。