繁体   English   中英

为什么此 NotifyCollectionChanged 会导致 memory 泄漏?

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM