【问题标题】:In WPF: Children.Remove or Children.Clear doesn't free objects在 WPF 中: Children.Remove 或 Children.Clear 不释放对象
【发布时间】:2010-03-29 22:31:31
【问题描述】:

更新:我在另一台安装得更干净的机器上尝试了这个。我无法在那台机器上重现这个。如果我找出是什么有问题的 (VSStudio) 组件导致了这种情况,我会告诉你的。

我从后面的代码创建了一些 UIElements,并期待垃圾收集来清理东西。但是,这些对象在我预期的时候并没有被释放。我期待它们在 RemoveAt(0) 处被释放,但它们仅在程序结束时被释放。

如何使对象从 Canvas 的 Children 集合中被释放?

<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
    MouseDown="Window_MouseDown">
  <Grid>
    <Canvas x:Name="main" />
  </Grid>
</Window>

后面的代码是:

public partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
  }

private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
  GC.Collect(); // This should pick up the control removed at a previous MouseDown
  GC.WaitForPendingFinalizers(); // Doesn't help either

  if (main.Children.Count == 0)
    main.Children.Add(new MyControl() { Background = Brushes.Yellow, Width = 100, Height = 50 });
  else
    main.Children.RemoveAt(0);
 }
}

public class MyControl : UserControl
{
  ~MyControl()
  {
    Debug.WriteLine("Goodbye");
  }
}

【问题讨论】:

    标签: .net wpf memory-management garbage-collection


    【解决方案1】:

    您可能会对此感兴趣。我最近发现 x:Name 标记扩展在由字符串名称键入的字典中存储了对父控件中 UIElement 的引用。

    当您从其父级中移除 UIElement 时,字典会保留对该控件的引用。

    这里有一篇博客文章/视频调试内存泄漏:WPF x:Name Memory Leak

    解决方案是不使用 x:Name 或确保清除由 x:Name 保持活动状态的控件,以免在收集可视化树的一部分之前消耗太多内存。

    更新:您使用 NameScope 取消注册命名类

    this.grid.Children.Remove(child); // Remove the child from visual tree
    NameScope.GetNameScope(this).UnregisterName("child"); // remove the keyed name
    this.child = null; // null the field
    
    // Finally it is free to be collected! 
    

    【讨论】:

      【解决方案2】:

      改变

      public class MyControl : UserControl

      public class MyControl : ContentControl

      它会说再见(在您第二次删除控件之后)。我还通过使用验证了内存不会泄漏

      Debug.WriteLine("mem: " + GC.GetTotalMemory(true).ToString());

      另外,请参阅this

      您通过清除 grid.Children 删除了 TestControl,但它不能立即进行垃圾收集。其上的几个异步操作处于挂起状态,并且在这些操作完成之前无法进行 GC(这些操作包括引发 Unloaded 事件和渲染引擎中的一些清理代码)。

      我已验证,如果您等到这些操作完成(例如通过以 ContextIdle 优先级调度 Dispatcher 操作),TestControl 将符合 GC 条件,而与 TextBlock 上是否存在绑定无关。

      UserControl 必须有一个不能快速清理的内部事件,或者它可能是 VS2010 RC 的错误。我会通过 connect 报告此问题,但现在切换到 ContentControl。

      由于您使用的是 UserControl,我假设您还必须切换到使用 Generic.xaml 模板。转换并不太难(而且对于大多数情况来说这是一个更好的解决方案。)

      【讨论】:

        【解决方案3】:

        C# 中有 3 代垃圾回收,因此即使没有对您的对象的引用,也可能需要 3 次垃圾回收才能释放它们。

        您可以使用 GC.Collect() 的参数来强制进行第 3 代垃圾回收,
        但是,最好的方法是不要自己调用 GC.Collect(),
        而是使用 IDisposable 接口并将 Children 绑定到 ObservableCollection 当您收到任何已移除对象的 CollectionChanged 事件 Dispose() 时。

        【讨论】:

          【解决方案4】:

          C# 中的对象在不再使用时不会自动“释放”。

          相反,当您从 Control 中删除对象时,假设您没有其他对该 UI 元素的引用,它就会有资格进行垃圾回收

          一旦一个对象是“无根的”(没有直接或间接来自应用程序中任何使用的对象的引用),它就有资格被收集。然后,垃圾收集器最终会清理您的对象,但是当这种情况发生时,您(通常)无法控制。

          相信它最终会被清理干净。这就是 C#(以及一般的 .NET)的美妙之处 - 管理和担心都由您处理。


          编辑:经过一些测试,Window 似乎持有对 UIelement 的引用,直到下一次布局通过。您可以通过添加对以下内容的调用来强制执行此操作:

          this.UpdateLayout();
          

          从画布子项中删除元素后。这将导致对象可用于 GC。

          【讨论】:

          • 我知道,但在这种情况下不这么认为。当我在 MouseDown 处理程序的开头添加 GC.Collect() 时,它应该选择符合垃圾收集条件的控件。它没有。我认为在 Canvas 对象内部仍然必须有一些对它的引用。
          • @Bart:尝试在处理程序的末尾添加 GC.Collection - 控件仍在处理程序开头的 UI 中......这有帮助吗?
          • @Bart:另外 - 尝试添加对 GC.WaitForPendingFinalizers() 的调用 - 请参阅:msdn.microsoft.com/en-us/library/…
          • 这并不重要。如果您继续单击,则始终会创建和删除对象。这些都没有发布,但在程序结束时。
          • @Bart:您订阅了 UserControl 上的事件吗?如果您有事件订阅,它将“根”您的对象...
          【解决方案5】:

          我们遇到了同样的问题,也认为这可能是原因。但是我们使用 Memory profiler 工具分析了我们的项目,发现与 Grid.Remove 或 Grid.RemoveAt 无关。所以我认为我的建议是在内存分析器工具中查看您的项目,看看您的项目内部发生了什么。希望这会有所帮助。

          【讨论】:

            【解决方案6】:

            我认为answer by Reed Copsey 最直接地说明了 OP 所看到的行为的解释。

            这是一个经过修改的程序,事实上,它的行为与 OP 似乎期望的完全一样。

            与原作的不同之处在于:

            • 调用UpdateLayout()确保从布局系统中移除控件的效果已经实现。

            • 等待 Dispatcher 完成任何挂起的处理(如果没有这个,用户控件完成的确切时间可能会有所不同,并且可能在比预期更晚的 GC 调用中发生)。

            • 进行第二次 GC CollectWaitForFullGCComplete 以确保 GC 确实有机会完成其工作。

             

            private void Window_MouseDown(object sender, MouseButtonEventArgs e)
            {
                if (main.Children.Count == 0)
                {
                    Console.WriteLine("Adding control");
                    main.Children.Add(new MyControl() { Background = Brushes.Yellow, Width = 100, Height = 50 });
                }
                else
                {
                    Console.WriteLine("Removing control");
                    main.Children.RemoveAt(0);
                }
            
                UpdateLayout();
                Dispatcher.Invoke(new Action(() => { }), System.Windows.Threading.DispatcherPriority.ContextIdle, null);
            
                GC.Collect(); // This should pick up the control removed at a previous MouseDown
                GC.WaitForPendingFinalizers(); // Doesn't help either
                GC.Collect();
                GC.WaitForFullGCComplete();
            }
            

            我在调试和发布版本中得到的输出是:

            Adding control
            Removing control
            Goodbye
            Adding control
            Removing control
            Goodbye
            Adding control
            Removing control
            Goodbye
            ...
            

            我认为这确实表明垃圾收集正在按预期工作。

            关于 OP 在不同的 PC 上看到不同的行为的注释——谁知道这是否可能是 2010 年的一些错误,但我怀疑这只是不同的 PC 有不同的负载,这影响了 GC 处理的一些东西。

            我在 VS2019 中使用 .NET framework 4.7.2 进行了测试。

            【讨论】:

            • 在我的情况下没有达到预期的效果:(
            • @sergiol 你有没有运行这个确切的代码并得到不同的输出?如果是这样,那就很有趣了!有什么区别?
            猜你喜欢
            • 1970-01-01
            • 2014-12-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-11-18
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多