You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为什么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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 10:54:04