自动滚动文本框使用的内存比预期的多

时间:2009-11-16 16:59:28

标签: c# .net memory textbox

我有一个应用程序,使用TextBox将消息记录到屏幕上。更新功能使用一些Win32函数来确保框自动滚动到结尾,除非用户正在查看另一行。这是更新功能:

private bool logToScreen = true;

// Constants for extern calls to various scrollbar functions
private const int SB_HORZ = 0x0;
private const int SB_VERT = 0x1;
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;
private const int SB_THUMBPOSITION = 4;
private const int SB_BOTTOM = 7;
private const int SB_OFFSET = 13;

[DllImport("user32.dll")]
static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetScrollPos(IntPtr hWnd, int nBar);
[DllImport("user32.dll")]
private static extern bool PostMessageA(IntPtr hWnd, int nBar, int wParam, int lParam);
[DllImport("user32.dll")]
static extern bool GetScrollRange(IntPtr hWnd, int nBar, out int lpMinPos, out int lpMaxPos);

private void LogMessages(string text)
{
    if (this.logToScreen)
    {
        bool bottomFlag = false;
        int VSmin;
        int VSmax;
        int sbOffset;
        int savedVpos;
        // Make sure this is done in the UI thread
        if (this.txtBoxLogging.InvokeRequired)
        {
            this.txtBoxLogging.Invoke(new TextBoxLoggerDelegate(LogMessages), new object[] { text });
        }
        else
        {
            // Win32 magic to keep the textbox scrolling to the newest append to the textbox unless
            // the user has moved the scrollbox up
            sbOffset = (int)((this.txtBoxLogging.ClientSize.Height - SystemInformation.HorizontalScrollBarHeight) / (this.txtBoxLogging.Font.Height));
            savedVpos = GetScrollPos(this.txtBoxLogging.Handle, SB_VERT);
            GetScrollRange(this.txtBoxLogging.Handle, SB_VERT, out VSmin, out VSmax);
            if (savedVpos >= (VSmax - sbOffset - 1))
                bottomFlag = true;
            this.txtBoxLogging.AppendText(text + Environment.NewLine);
            if (bottomFlag)
            {
                GetScrollRange(this.txtBoxLogging.Handle, SB_VERT, out VSmin, out VSmax);
                savedVpos = VSmax - sbOffset;
                bottomFlag = false;
            }
            SetScrollPos(this.txtBoxLogging.Handle, SB_VERT, savedVpos, true);
            PostMessageA(this.txtBoxLogging.Handle, WM_VSCROLL, SB_THUMBPOSITION + 0x10000 * savedVpos, 0);
        }
    }
}

现在奇怪的是文本框消耗的内存至少是我期望的内存的两倍。例如,当TextBox中有大约1MB的消息时,应用程序最多可以消耗6MB的内存(除了当logToScreen设置为false时使用的内容)。增长总是至少是我预期的两倍,并且(如我的例子中)有时更多。

更奇怪的是使用:

this.txtBoxLogging.Clear();
for (int i = 0; i < 3; i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

不释放内存(事实上,它略有增加)。

当我记录这些消息时,知道内存的去向吗?我不相信它与Win32调用有任何关系,但我把它包括在内是彻底的。

编辑:

我得到的前几个回复与如何跟踪内存泄漏有关,所以我认为我应该分享我的方法。我使用WinDbg和perfmon的组合来跟踪内存使用情况(从几个小时到几天)。所有CLR堆上的总字节数不会增加超过我的预期,但随着更多消息的记录,私有字节总数会稳定增加。这使得WinDbg不太有用,因为它的工具(sos)和命令(dumpheap,gcroot等)都基于.NET的托管内存。

这可能是GC.Collect()无法帮助我的原因,因为它只是在CLR堆上寻找空闲内存。我的泄漏似乎是在未管理的内存中。

2 个答案:

答案 0 :(得分:3)

您如何确定内存使用情况?您必须观察应用程序的CLR内存使用情况,而不是系统为整个应用程序使用的内存(您可以使用Perfmon)。也许您已经在使用正确的监控方法。

在我看来,你在内部使用StringBuilder。如果是这样,那将解释内存的加倍,因为这是StringBuilder内部工作的方式。

如果对象的引用仍在范围内,或者任何代码使用静态变量,GC.Collect()可能无法执行任何操作。


编辑:
我会抛弃上面的内容,因为它可能仍然是真的,但我查了AppendText的内部结构。它不会附加(即,添加到StringBuilder),而是设置SelectedText属性,该属性不设置字符串但发送Win32消息(字符串在检索时被缓存)。

因为字符串是不可变的,这意味着,对于每个字符串,将有三个副本:一个在调用应用程序中,一个在基础Control的“缓存”中,一个在实际的Win32文本框控件中。每个字符宽两个字节。这意味着任何1MB的文本都将占用6MB的内存(我知道,这有点过于简单,但基本上就是这样)。

编辑2:不确定是否会进行任何更改,但您可以考虑自己调用SendMessage。但肯定看起来你需要自己的滚动算法和你自己绘制的文本框来减少记忆。

答案 1 :(得分:1)

跟踪应用程序使用的内存量,尤其是垃圾收集语言,是一项棘手的任务。人们经常使用应用程序的总内存计数来确定仍在使用的对象(例如通过任务管理器)。这对于本机应用程序来说是有效的,但是对于托管应用程序会产生非常误导性的结果。

为了正确确定CLR对象使用的内存量,您需要使用专门用于测量它的工具。例如,我发现最好的方法是使用WinDbg和sos.dll的组合来测量当前的有根对象。这将告诉您托管对象的大小,并指出哪些对象实际占用了额外的内存。

这是一篇关于这个主题的好文章