简单的WPF示例导致不受控制的内存增长

时间:2008-10-10 17:22:19

标签: c# wpf memory-leaks

我已经将我在其中一个应用程序中看到的问题归结为一个非常简单的复制样本。我需要知道是否有什么不对劲或者我缺少的东西。

无论如何,下面是代码。行为是代码在内存中运行并稳定增长,直到它与OutOfMemoryException崩溃。这需要一段时间,但行为是对象被分配,而不是垃圾收集。

我已经进行了内存转储并在某些事情上运行了!gcroot以及使用ANTS来找出问题所在,但我已经使用了一段时间并需要一些新的眼睛。

此复制示例是一个简单的控制台应用程序,可创建Canvas并为其添加一行。它不断地这样做。这就是所有的代码。它不时地睡觉,以确保CPU不会因为你的系统没有响应而被征税(并且确保GC无法运行并不奇怪)。

有人有什么想法吗?我已经尝试过仅使用.NET 3.0,.NET 3.5以及.NET 3.5 SP1,并且在所有三种环境中都会出现相同的行为。

另请注意,我已将此代码放在WPF应用程序项目中,并通过单击按钮触发代码,它也会出现在那里。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows;

namespace SimplestReproSample
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            long count = 0;
            while (true)
            {
                if (count++ % 100 == 0)
                {
                    // sleep for a while to ensure we aren't using up the whole CPU
                    System.Threading.Thread.Sleep(50);
                }
                BuildCanvas();
            }
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static void BuildCanvas()
        {
            Canvas c = new Canvas();

            Line line = new Line();
            line.X1 = 1;
            line.Y1 = 1;
            line.X2 = 100;
            line.Y2 = 100;
            line.Width = 100;
            c.Children.Add(line);

            c.Measure(new Size(300, 300));
            c.Arrange(new Rect(0, 0, 300, 300));
        }
    }
}

注意:下面的第一个答案有点偏离基础,因为我已明确声明在WPF应用程序的按钮单击事件期间发生了同样的行为。然而,我没有明确说明在那个应用程序中我只进行了有限数量的迭代(比如1000)。这样做可以让GC在您单击应用程序时运行。另请注意,我明确表示我已经进行了内存转储,并发现我的对象是通过!gcroot生根的。我也不同意GC无法运行。 GC不在我的控制台应用程序的主线程上运行,特别是因为我在双核机器上,这意味着并发工作站GC处于活动状态。但是,消息泵是的。

为了证明这一点,这是一个在DispatcherTimer上运行测试的WPF应用程序版本。它在100ms定时器间隔期间执行1000次迭代。有足够的时间处理泵中的任何消息并保持低CPU使用率。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;

namespace SimpleReproSampleWpfApp
{
    public partial class Window1 : Window
    {
        private System.Windows.Threading.DispatcherTimer _timer;

        public Window1()
        {
            InitializeComponent();

            _timer = new System.Windows.Threading.DispatcherTimer();
            _timer.Interval = TimeSpan.FromMilliseconds(100);
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.Start();
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        void RunTest()
        {
            for (int i = 0; i < 1000; i++)
            {
                BuildCanvas();
            }
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static void BuildCanvas()
        {
            Canvas c = new Canvas();

            Line line = new Line();
            line.X1 = 1;
            line.Y1 = 1;
            line.X2 = 100;
            line.Y2 = 100;
            line.Width = 100;
            c.Children.Add(line);

            c.Measure(new Size(300, 300));
            c.Arrange(new Rect(0, 0, 300, 300));
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            _timer.Stop();

            RunTest();

            _timer.Start();
        }
    }
}

注意2:我使用了第一个答案中的代码,我的记忆增长得非常慢。请注意,1ms比我的示例慢得多且迭代次数少。在开始注意到增长之前,你必须让它运行几分钟。 5分钟后,从30MB的起点开始,它的容量为46MB。

注意3:删除对.Arrange的调用完全消除了增长。不幸的是,这个调用对我的使用非常重要,因为在很多情况下我是从Canvas创建PNG文件(通过RenderTargetBitmap类)。如果没有调用.Arrange,它根本不会布局画布。

4 个答案:

答案 0 :(得分:10)

我能够使用您提供的代码重现您的问题。内存不断增长,因为Canvas对象永远不会被释放;内存分析器指示Dispatcher的ContextLayoutManager正在保留所有内容(以便它可以在必要时调用OnRenderSizeChanged)。

似乎一个简单的解决方法是添加

c.UpdateLayout()

BuildCanvas的结尾。

那就是说,CanvasUIElement;它应该在UI中使用。它不是设计用作任意绘图表面。正如其他评论者已经指出的那样,创建数千个Canvas对象可能表明存在设计缺陷。我意识到你的生产代码可能更复杂,但如果它只是在画布上绘制简单的形状,基于GDI +的代码(即System.Drawing类)可能更合适。

答案 1 :(得分:2)

.NET 3和3.5中的WPF有内部内存泄漏。它只在某些情况下触发。我们永远无法确切地知道触发它的是什么,但我们在我们的应用程序中有它。显然,它已在.NET 4中修复。

我认为它与this blog post

中提到的相同

无论如何,将以下代码放在App.xaml.cs构造函数中为我们解决了

public partial class App : Application
{
   public App() 
   { 
       new HwndSource(new HwndSourceParameters()); 
   } 
}

如果没有其他解决办法,请尝试并查看

答案 2 :(得分:1)

通常在.NET中,当超过某个阈值时,对象分配会触发GC,它不依赖于消息泵(我无法想象它与WPF的不同)。

我怀疑Canvas对象在某种程度上是根深蒂固的东西。如果您在BuildCanvas方法完成之前执行c.Children.Clear(),则内存增长会大幅减慢。

无论如何,作为这里提到的评论者,这种框架元素的使用是非常不寻常的。你为什么需要这么多画布?

答案 3 :(得分:0)

编辑2:显然不是答案,但这是答案和评论中来回的一部分,所以我不会删除它。

GC永远不会有机会收集这些对象,因为你的循环及其阻塞调用永远不会结束,因此消息泵和事件永远不会轮到他们。如果您使用某种Timer以便消息和事件实际上有机会处理,那么您可能无法消耗掉所有的记忆。

编辑:只要间隔大于零,以下内容就不会耗尽我的记忆。即使间隔只是1 Tick,只要它不是0.如果它是0,我们就会回到无限循环。

public partial class Window1 : Window {
    Class1 c;
    DispatcherTimer t;
    int count = 0;
    public Window1() {
        InitializeComponent();

        t = new DispatcherTimer();
        t.Interval = TimeSpan.FromMilliseconds( 1 );
        t.Tick += new EventHandler( t_Tick );
        t.Start();
    }

    void t_Tick( object sender, EventArgs e ) {
        count++;
        BuildCanvas();
    }

    private static void BuildCanvas() {
        Canvas c = new Canvas();

        Line line = new Line();
        line.X1 = 1;
        line.Y1 = 1;
        line.X2 = 100;
        line.Y2 = 100;
        line.Width = 100;
        c.Children.Add( line );

        c.Measure( new Size( 300, 300 ) );
        c.Arrange( new Rect( 0, 0, 300, 300 ) );
    }
}