Текстовое поле с автоматической прокруткой использует больше памяти, чем ожидалось
У меня есть приложение, которое регистрирует сообщения на экране с помощью 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 имеется ~ 1 МБ сообщений, приложение может потреблять до 6 МБ памяти (в дополнение к тому, что оно использует, когда для параметра logToScreen установлено значение false). Увеличение всегда как минимум вдвое больше, чем я ожидаю, и (как в моем примере) иногда больше.
Что более странно, так это использование:
this.txtBoxLogging.Clear();
for (int i = 0; i < 3; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Не освобождает память (на самом деле она немного увеличивается).
Любая идея, по которой происходит память, когда я регистрирую эти сообщения? Я не верю, что это имеет какое-либо отношение к вызовам Win32, но я включил его в тщательное описание.
EDIT:
Первые две ответы, которые я получил, были связаны с тем, как отслеживать утечку памяти, поэтому я решил поделиться своей методологией. Я использовал комбинацию WinDbg и perfmon для отслеживания использования памяти с течением времени (от пары часов до нескольких дней). Общее количество байтов во всех кучах CLR не увеличивается больше, чем я ожидаю, но общее количество частных байтов постоянно увеличивается по мере регистрации большего количества сообщений. Это делает WinDbg менее полезным, так как его инструменты (sos) и команды (dumpheap, gcroot и т.д.) Основаны на управляемой памяти .NET.
Вероятно, почему GC.Collect() не может мне помочь, поскольку он ищет свободную память в куче CLR. Моя утечка находится в неконтролируемой памяти.
Ответы
Ответ 1
Как вы определяете использование памяти? Вам нужно будет следить за использованием памяти CLR для вашего приложения, а не с памятью, используемой системой для всего приложения (для этого вы можете использовать Perfmon). Возможно, вы уже используете правильный метод мониторинга.
Мне кажется, что вы используете StringBuilder
внутренне. Если это так, это объясняет удвоение памяти, потому что так, как работает StringBuilder внутри.
GC.Collect()
может ничего не делать, если ссылки на ваши объекты по-прежнему находятся в области видимости или если какой-либо из ваших кодов использует статические переменные.
EDIT:
Я оставлю это, потому что это может быть правдой, но я искал внутренние элементы AppendText
. Он не добавляет (т.е. В StringBuilder), вместо этого он устанавливает свойство SelectedText
, которое не устанавливает строку, но отправляет сообщение Win32 (строка кэшируется при извлечении).
Поскольку строки неизменяемы, это означает, что для каждой строки будет три копии: одна в вызывающем приложении, одна в "кеше" базы Control
и одна в фактическом элементе управления текстовым полем Win32. Каждый символ имеет ширину в два байта. Это означает, что любой 1 Мб текста будет потреблять 6 МБ памяти (я знаю, это немного упрощенно, но в основном это похоже на то, что происходит).
РЕДАКТИРОВАТЬ 2: не уверен, что он внесет какие-либо изменения, но вы можете сами называть SendMessage
. Но он наверняка начнет выглядеть так, как будто вам понадобится ваш собственный алгоритм прокрутки и собственный собственный текстовый блок, чтобы получить память.
Ответ 2
Отслеживание объема памяти, используемой приложением, особенно язык сбора мусора, является сложным делом. Люди часто использовали общий объем памяти для приложения для определения объектов, которые все еще используются (например, через диспетчер задач). Это сомнительно эффективно для собственного приложения, но даст очень вводящие в заблуждение результаты для управляемых приложений.
Чтобы правильно определить объем памяти, используемой вашими объектами CLR, вам необходимо использовать инструмент, специально предназначенный для измерения этого. Например, я считаю, что лучший способ - использовать комбинацию WinDbg и sos.dll для измерения текущих корневых объектов. Это позволит вам определить размер ваших управляемых объектов и указать, какие объекты фактически занимают дополнительную память.
Вот хорошая статья по теме