【问题标题】:WPF DataGrid.items.Refresh() memory leakWPF DataGrid.items.Refresh() 内存泄漏
【发布时间】:2021-12-23 08:17:45
【问题描述】:

下午好。 帮助解决以下问题。 如果我为 DataGrid.ItemsSource 设置了一些 DataTable,我将在另一个线程中对其进行更新,然后定期调用 DataGrid.Items.Refresh() 我会有内存泄漏。 有没有办法避免这种情况?

<Window x:Class="TestDataGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestDataGrid"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="7*"/>
        </Grid.ColumnDefinitions>
        <DataGrid x:Name="DataGrid1" d:ItemsSource="{d:SampleData ItemCount=5}" Grid.ColumnSpan="2"/>

    </Grid>
</Window>

主窗口

using System;
using System.Windows;
namespace TestDataGrid
{    
    public partial class MainWindow : Window
    {
        DataWorker data = new DataWorker();
        public MainWindow()
        {
            InitializeComponent();            
            DataGrid1.ItemsSource = data.dt.DefaultView;
            data.DataChanged += OnDataChanged;
            data.RunThread();
        }

        private void OnDataChanged(object obj, EventArgs e)
        {
            Dispatcher.Invoke(new Action(() =>
            {
                DataGrid1.Items.Refresh();
                //this.Title = data.dt.Rows[0].ItemArray[0].ToString();
            }));
        }
    }
}

DataWorker.cs

using System;
using System.Data;
using System.Threading;

namespace TestDataGrid
{
    internal class DataWorker
    {
        public event EventHandler DataChanged;
        public DataWorker()
        {
            column.DataType = Type.GetType("System.UInt64");
            column.ColumnName = "DATA";
            dt.Columns.Add(column);
        }

        public DataTable dt = new DataTable();
        private DataColumn column = new DataColumn();        

        public void RunThread()
        {
            Thread th = new Thread(DataChange);
            th.Start();
        }
        private void DataChange()
        {
            while (true)
            {
                dt.Clear();
                dt.Rows.Add(new object[] { DateTime.Now.Ticks });
                DataChanged?.Invoke(this, EventArgs.Empty);
                Thread.Sleep(1000);
            }
                
        }

    }
}

如果我将 DataGrid1.Items.Refresh() 更改为 this.Title = data.dt.Rows[0].ItemArray[0].ToString() 为例,然后内存OK。

Start Screen

2 min later Screen

项目:GitHub

【问题讨论】:

  • 这在我看来不像是内存泄漏。这看起来像是 .NET 的正常(如果激进)内存使用量,这是由其每 CPU 核心 GC 堆引起的。你的机器有多少个 CPU 内核?
  • 我在 i7-1165G7 上运行 4 核。
  • 如今几乎没有使用 thread.sleep 的好理由。请改用 await task.delay(n) 。并调用异步。我认为真正的事情是做一些更复杂的事情。您是否考虑过行视图模型的可观察集合而不是数据表?除非您的所有数据每秒都完全更改,否则识别更改的属性并仅在现有视图模型中更改它们是很常见的。一个相当普遍的要求是用不同的背景颜色突出显示变化。

标签: c# wpf


【解决方案1】:

不知道如何

this.Title = data.dt.Rows[0].ItemArray[0].ToString();

涉及

DataGrid1.Items.Refresh();

这是针对两个不同目标对象的两个不同操作。

以下简单地将单元格值分配给Window.Title 属性:

this.Title = data.dt.Rows[0].ItemArray[0].ToString();

DataGrid1.Items.Refresh();

指示DataView 重新创建视图。这意味着缓存的数据被丢弃,但仍驻留在内存中。垃圾收集器收集这些可辨认的对象需要时间。
这不是内存泄漏。
CollectionView.Refresh 背后有一些严肃的事情正在发生。
仅仅因为它是单行代码并不意味着您可以将其指标与任何随机代码行进行比较。

如果内存使用对您来说是个问题,请考虑使用ObservableCollection 而不是DataTable

如果您描述的场景是真实场景,那么使用ObservableCollection 比使用具有单行单列的DataTable 更方便。在您的情况下,您可以将时间戳直接添加到ObservableCollection

一些改进代码的建议:

  1. 不要使用ThreadThread.Sleep。始终使用现代的 Task.Runawait Task.Delay 代替
  2. 避免while(true) 构造
  3. 使用计时器进行定期操作
  4. 不要定义公共字段。改为使用属性。在您的情况下,该属性应该是只读的。
  5. 首选代理 EventHandler 而不是 EventHandler&lt;EventArgs&gt;
event EventHandler MyEvent;

结束

event EventHandler<EventArgs> MyEvent;

改进后的实现如下所示:

DataWorker.cs

internal class DataWorker
{
  private DispatcherTimer Timer { get; }
  public DataTable Dt { get; } = new DataTable();
  private DataColumn Column { get; } = new DataColumn();
  public event EventHandler DataChanged;

  public DataWorker()
  {
    this.Column.DataType = Type.GetType("System.UInt64");
    this.Column.ColumnName = "DATA";
    this.Dt.Columns.Add(this.Column);

    this.Timer = new DispatcherTimer(
      TimeSpan.FromSeconds(1), 
      DispatcherPriority.Normal, 
      OnTimerElapsed, 
      Application.Current.Dispatcher);
  }

  private void OnTimerElapsed(object? sender, EventArgs e)
  {
    this.Dt.Clear();
    this.Dt.Rows.Add(new object[] { DateTime.Now.Ticks });
    this.DataChanged?.Invoke(this, EventArgs.Empty);
  }
}

MainWindow.xaml.cs

public MainWindow(TestViewModel dataContext)
{
  var data = new DataWorker();
  DataGrid.ItemsSource = data.dt.DefaultView;
  data.DataChanged += OnDataChanged;
}

private void OnDataChanged(object obj, EventArgs e)
{
  DataGrid.Items.Refresh();
}

如何识别内存泄漏

只看总内存分配是不够的。内存分配随时间增加的原因有很多。内存泄漏的特点是内存中包含的对象永远不会被收集,因为它们总是可以访问的。造成这种情况的原因多种多样。这可能是因为静态引用或事件源的对象生命周期管理、数据绑定等。

要识别泄漏,您必须检查堆上的对象。您可以通过比较内存的快照来做到这一点。并通过像这样强制垃圾收集:

for (int i = 0; i < 4; i++)
{
    GC.Collect(2, GCCollectionMode.Forced, true);
    GC.WaitForPendingFinalizers();
}

如果对象仍在堆上,尽管您希望它们被收集,因为您已删除所有引用,但您可能有泄漏。

每个好的分析器都允许您扩展引用树,以识别导致对象可访问的类。你必须检查这个类来找到强引用的原因。

【讨论】:

  • 非常感谢您的回答。我知道任务。我只是举了一个最简单的例子。显然我对 Visual Studio 2022 分析器的工作感到困惑,在调试模式下工作 30 分钟后,分析器显示超过 5GB 的已用内存。但是当应用程序在调试器之外启动时,就没有内存问题了。
  • 您不应在调试模式下分析应用程序。而是使用像 DotMemory 这样的实时分析器。有一个 30 天的完整功能试用版。它允许您通过挂钩到进程来分析正在运行的应用程序。您还可以手动强制垃圾回收以查看堆内存的行为方式以及可以访问哪些对象。请参阅我更新的答案以了解如何手动强制 GC 收集。您可以从按钮单击处理程序执行它。
  • 此外,要识别内存泄漏,您必须检查堆上的对象,而不是总内存分配。您想查看哪些对象永远不会被收集。
猜你喜欢
  • 2011-02-21
  • 2010-12-05
  • 2011-03-19
  • 2011-08-12
  • 1970-01-01
相关资源
最近更新 更多