該賞金過期7天。回答這個問題有資格獲得 50聲望獎勵。 display name想引起更多人對這個問題的關注。
我使用 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);
}
}
The xaml codes of views are unlikely to be relevant to the context so I didn't write them here. If any more detail is required please tell me.
Update0
It was a fault to neglect the xaml codes. I found its actually relevant.
<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>
After removing the whole ItemsControl, the memory leak was not happenning.
Update1
Based on what I observed, I made a demo project inorder for audiences to have better information and able to reproduce the bug. As StackOverFlow dosen't support file attach and the same thing is posted on github as well, I am posting the link of that post here. You may goto that link to download the demo. https://github.com/dotnet/wpf/issues/5739
Update2
Based on whats done already, I tried to used .NET provided ObservableCollection instead of my own implementation of INotifyCollectionChanged. DemoCollectionViewModel was changed to :
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);
}
}
}
And in the view, the ItemsControl's ItemsSource is binded to the ObservableCollection instead. However the problem persists
uj5u.com熱心網友回復:
經過多次嘗試,我終于自己找到了答案。我沒有繼續創建新實體,而是將其修改為僅在從模型檢索到的集合具有更多項數時才添加新項。如果源數量較少,每次從模型復制資料只是更新現有實體。
這是最終的代碼:
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();
}
}
演示的最終版本,修復了記憶體泄漏錯誤,也可以在 github 帖子https://github.com/dotnet/wpf/issues/5739 中找到
uj5u.com熱心網友回復:
正如INotifyCollectionChanged 中所述,不更新 UI INotifyCollectionChanged 應該由集合而不是視圖模型實作,在這里最好只使用 ObservableCollection,它會在集合更改時自動處理更新 UI 并擺脫 GameBodyCollectionViewModel.NotifyChange ().
更新
我很抱歉之前沒有注意到 IEnumerable。泄漏很可能是因為您每次呼叫回圈時都使用“NotifyCollectionChangedAction.Reset”,這將觸發集合中所有專案的 UI 更新,而不管是否有任何更改。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/367398.html
上一篇:只有一個Azure函式實體?
