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

16

我有一个应用程序,使用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堆上寻找可用内存。我的泄漏似乎在非托管内存中。


您,先生,是我的救星!终于,TextboxBase附加了滚动锁定功能! - Ohad Schneider
有趣的一点:使用richTextBox.Text+=str而不是richTextBox.AppendText(str)可以消除闪烁,但当字符串变大时会导致严重的减速。 - Ohad Schneider
2个回答

3
你是如何确定内存使用情况的?你需要监控应用程序的CLR内存使用情况,而不是整个应用程序系统使用的内存(可以使用Perfmon)。也许你已经在使用正确的监控方法。
我觉得你内部使用了StringBuilder。如果是这样,那就可以解释为什么内存会翻倍,因为StringBuilder内部就是这样工作的。
如果你的对象引用仍然在作用域内,或者你的代码使用静态变量,则GC.Collect()可能不起作用。
编辑: 我将保留以上内容,因为它可能仍然是正确的,但我查阅了AppendText的内部结构。它不是追加(即到StringBuilder),而是设置SelectedText属性,它不设置字符串,而是发送Win32消息(字符串在检索时被缓存)。
由于字符串是不可变的,这意味着对于每个字符串,将有三个副本:一个在调用应用程序中,一个在基本Control的“高速缓存”中,一个在实际的Win32文本框控件中。每个字符占两个字节。这意味着任何1MB的文本将消耗6MB的内存(我知道,这有点简单化,但基本上就是发生了什么)。
编辑2:不确定是否会有任何改变,但你可以考虑自己调用SendMessage。但它确实开始看起来你需要自己的滚动算法和自己的所有者绘制文本框来降低内存使用。

内存调试使用了permon和WinDbg的组合。内存不出现在CLR堆中,而是在非托管内存中的某个地方。这就是有问题的代码,但是可以确定。如果将logToScreen设置为false,则内存使用量不会随时间(几天)增加。 - cgyDeveloper
谢谢更新,这为问题提供了一些线索。确实,P/Invoke SendMessage 用于发送字符串(在设置时不会在内部存储!)。然后,封送和 Win32 API 是可能的剩余内存消耗者。如果我没记错的话,封送不会复制字符串,但我可能弄错了。 - Abel
有趣,你在哪里找到了关于AppendText的内部信息?我已经在MSDN上搜索过了,但没有找到深入的内容。这很可能正是我正在寻找的东西。 - cgyDeveloper
我通常使用Reflector来查看这些信息:http://www.red-gate.com/products/reflector/. 它是免费的,但不是开源的(没有开源替代品)。如果你需要更深入地了解(虽然这里不需要,但Reflector无法反汇编本地代码),我有时会深入研究SSCLI代码(http://www.microsoft.com/downloads/details.aspx?FamilyId=8C09FD61-3F26-4555-AE17-3121B4F51D4D&displaylang=en)。虽然有点过时,但它仍然能够很好地完成其工作。 - Abel
我觉得使用一个ListBox可能会更容易,所以我会检查一下它的内存使用情况。到目前为止谢谢你的帮助。 - cgyDeveloper

1

跟踪应用程序使用的内存量,特别是垃圾回收语言,是一个棘手的任务。 人们经常使用应用程序的总体存储器计数来确定仍在使用的对象(例如通过任务管理器)。 对于本地应用程序来说,这可能不太有效,但对于托管应用程序来说,结果将非常误导。

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

这里有一篇关于这个主题的好文章。


是的,那是一篇非常好的文章。使用WinDbg和perfmon的组合,我确定了额外的内存不在CLR托管内存中(使WinDbg变得不那么有用),而是在某个未受管理的内存中。 - cgyDeveloper
关于“我发现最好的方法是使用WinDbg和sos.dll”的说法:很有趣,但是使用适用的CLR计数器和Perfmon不是同样容易吗?许多(但不是全部!)与CLR + GC相关的内存问题可以通过这种方式追踪到。 - Abel
Perfmon是开始的最佳位置,然后WinDbg可以告诉您为什么某个对象不符合垃圾回收的条件(使用sos.dll的gcroot命令)。 - cgyDeveloper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接