我们正在开发一个非常大型的.NET WinForms复合应用程序-不是CAB,而是一个类似的本地框架。 我们正在Windows Server 2003上运行的Citrix和RDP环境中运行。
我们开始遇到随机且难以重现的"错误创建窗口句柄"错误,这似乎是我们应用程序中的老式句柄泄漏。 我们大量使用了第三方控件(Janus GridEX,Infralution VirtualTree和.NET Magic对接),并且基于数据库中的元数据进行了大量的动态加载和内容呈现。
Google上有很多有关此错误的信息,但是关于如何避免该问题的建议却很少。
对于构建易于处理的winforms应用程序,stackoverflow社区是否对我有任何好的指导?
我已经跟踪了很多UI无法按WinForms预期卸载的问题。
以下是一些一般性提示:
-
很多时候,控件将保持使用状态,因为控件事件未正确删除(工具提示提供者在这里给我们造成了很大的问题)或控件未正确处置。
-
在所有模式对话框周围使用"使用"块,以确保将其丢弃
-
有一些控件属性会在必要之前强制创建窗口句柄(例如,设置TextBox控件的ReadOnly属性将强制实现该控件)
-
使用.Net内存分析器之类的工具来获取所创建类的计数。此工具的较新版本还将跟踪GDI和USER对象。
-
尝试最小化Win API调用(或其他DllImport调用)的使用。如果确实需要使用互操作,请尝试以使using / Dispose模式正常工作的方式包装这些调用。
我将NativeWindow子类化并手动调用CreateHandler时遇到此错误。问题是我忘记在WndProc的替代版本中添加base.WndProc(m)。它引起了同样的错误
了解此错误
突破Windows的极限:USER和GDI对象–第1部分,作者Mark Russinovich:
https://blogs.technet.microsoft.com/markrussinovich/2010/02/24/pushing-the-limits-of-windows-user-and-gdi-objects-part-1/
好的。
解决此错误
您需要能够重现该问题。这是记录执行步骤的一种方法https://stackoverflow.com/a/30525957/495455。
好的。
解决创建大量句柄的最简单方法是打开TaskMgr.exe。在TaskMgr.exe中,您需要使USER对象,GDI对象和Handles列可见,如图所示,要执行此操作,请选择View Menu> Select Columns:
好的。
好的。
请执行导致问题的步骤,并观察USER对象计数增加到大约10,000,或GDI对象或句柄达到其限制。
好的。
当您看到"对象"或"句柄"增加(通常非常大)时,可以通过单击"暂停"按钮来停止Visual Studio中的代码执行。
好的。
然后只需按住F10或F11即可在对象/句柄计数急剧增加时浏览代码。
好的。
到目前为止,我发现的最好的工具是来自NirSoft的GDIView,它可以分解GDI Handle字段:
好的。
好的。
我将其追溯到设置DataGridViews列宽时使用的以下代码:
好的。
1 2 3 4 5 6 7 8 9 10
| If Me.Controls.ContainsKey(comboName) Then
cbo = CType(Me.Controls(comboName), ComboBox)
With cbo
.Location = New System.Drawing.Point(cumulativeWidth, 0)
.Width = Me.Columns(i).Width
End With
'Explicitly cleaning up fixed the issue of releasing USER objects.
cbo.Dispose()
cbo = Nothing
End If |
这是堆栈跟踪:
好的。
at System.Windows.Forms.Control.CreateHandle() at
System.Windows.Forms.ComboBox.CreateHandle() at
System.Windows.Forms.Control.get_Handle() at
System.Windows.Forms.ComboBox.InvalidateEverything() at
System.Windows.Forms.ComboBox.OnResize(EventArgs e) at
System.Windows.Forms.Control.OnSizeChanged(EventArgs e) at
System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32
width, Int32 height, Int32 clientWidth, Int32 clientHeight) at
System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32
width, Int32 height) at
System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32
width, Int32 height, BoundsSpecified specified) at
System.Windows.Forms.ComboBox.SetBoundsCore(Int32 x, Int32 y, Int32
width, Int32 height, BoundsSpecified specified) at
System.Windows.Forms.Control.SetBounds(Int32 x, Int32 y, Int32 width,
Int32 height, BoundsSpecified specified) at
System.Windows.Forms.Control.set_Width(Int32 value)
Ok.
这是Fabrice有用的文章的关键所在,它帮助我确定了极限:
好的。
"创建窗口句柄时出错"
当积极使用我正在为客户端处理的大型Windows Forms应用程序时,用户经常会收到"错误创建窗口句柄"异常。
好的。
除了应用程序消耗太多资源这一事实(这是我们已经在解决的一个单独问题)之外,我们在确定耗尽哪些资源以及这些资源的限制方面也遇到了困难。
我们首先想到要关注Windows Task Manager中的Handles计数器。那是因为我们注意到某些进程倾向于消耗比正常情况更多的资源。但是,此计数器不是一个好的计数器,因为它跟踪诸如文件,套接字,进程和线程之类的资源。这些资源称为内核对象。
好的。
我们应关注的其他资源是GDI对象和用户对象。您可以在MSDN上获得三类资源的概述。
好的。
用户对象
窗口创建问题与用户对象直接相关。
好的。
我们试图确定应用程序可以使用的用户对象的限制。
每个进程的配额为10,000个用户句柄。可以在注册表中更改此值,但是在本例中,此限制并不是真正的限制因素。
另一个限制是每个Windows会话有66,536个用户句柄。此限制是理论上的。在实践中,您会注意到无法实现。在我们的例子中,在当前会话中的用户对象总数达到11,000之前,我们遇到了可怕的"错误创建窗口句柄"异常。
好的。
桌面堆
然后,我们发现真正的罪魁祸首是"桌面堆"。
默认情况下,交互式用户会话的所有图形应用程序都在所谓的"桌面"中执行。分配给此类桌面的资源是有限的(但可配置)。
好的。
注意:用户对象占用了Desktop Heap的大部分内存空间。这包括窗户。
有关桌面堆的更多信息,可以参考NTDebugging MSDN博客上发布的非常好的文章:
好的。
真正的解决方案是什么?是绿色的!
增加桌面堆是一种有效的解决方案,但这不是最终的解决方案。真正的解决方案是消耗更少的资源(在我们的案例中,更少的窗口句柄)。我猜想您对这种解决方案会感到多么失望。这真的是我能想到的全部吗?
好吧,这里没有什么大秘密。唯一的出路就是保持苗条。具有不太复杂的UI是一个好的开始。这对资源有好处,对可用性也有好处。下一步是避免浪费,保留资源并对其进行回收!
好的。
这是我们在客户应用程序中执行的操作:
好的。
我们使用TabControls,并在每个标签可见时动态创建每个标签的内容。
我们使用可扩展/可折叠区域,然后仅在需要时才用控件和数据填充它们。
我们将尽快释放资源(使用Dispose方法)。当区域折叠时,可以清除其子控件。选项卡在隐藏时也是如此;
我们使用MVP设计模式,因为它可以将数据与视图分开,因此有助于实现上述目标。
我们使用布局引擎,标准的FlowLayoutPanel和TableLayoutPanel或自定义引擎,而不是创建嵌套面板,GroupBoxes和Splitters的深层次结构(一个空的splitter本身会消耗三个窗口句柄...)。
以上只是提示,您需要构建丰富的Windows Forms屏幕时可以执行的操作。毫无疑问,您可以找到其他方法。
我认为您应该做的第一件事是围绕用例和场景构建应用程序。这有助于仅显示给定时间和给定用户的需求。
好的。
当然,另一种解决方案是使用不依赖于句柄的系统... WPF有人吗?
好的。
好。
我遇到此异常是因为创建新UI控件并设置其属性的无穷循环。
经过多次循环后,更改控件的可见属性会引发此异常。
我发现用户对象和GDI对象(来自任务管理器)都很大。
我猜您的问题与这些UI控件耗尽系统资源的原因类似。
我正在使用Janus控件。就处置自己而言,它们是极度越野车。我建议您确保正确处理它们。另外,与它们的绑定有时不会释放,因此您必须手动解除绑定对象以处置控件。
在向面板添加控件时遇到了此异常,因为未清除面板中的子控件。如果将子控件放置在面板中,则错误已修复。
1 2 3
| For k = 1 To Panel.Controls.Count
Panel.Controls.Item(0).Dispose()
Next |
当我开始在WinForm App中使用线程处理时,发生了相同的错误,
我使用堆栈跟踪来查找引发错误的内容,并发现基础设施的UltraDesktopAlert组件在此背后,因此我以不同的方式调用它,错误现在消失了。
1 2 3 4
| this.Invoke((MethodInvoker)delegate
{
//call your method here
}); |
完整的代码将如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private void ultraButton1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(() => myMethod1());
}
void myMethod1()
{
//my logic
this.Invoke((MethodInvoker)delegate
{
ultraDesktopAlert1.Show($"my message header","my message");
});
//my logic
} |
我也无法使用GDI实用程序来查找我的应用程序创建的处理数量,但是我的应用程序(64位)在其列表中不可用。
另一个解决方案是在以下位置HKEY上将桌面堆的值更改为SharedSection=1024,20480,768
1
| Computer\\HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\SubSystems |
但是我的已经具有相同的价值。仅调用方法委托对我有用。希望这对您有所帮助。
我遇到了相同的.Net运行时错误,但是我的解决方案有所不同。
我的场景:
从返回DialogResult的弹出对话框中,用户将单击一个按钮以发送电子邮件。我添加了一个线程,以便在后台生成报告时UI不会锁定。这种情况最终导致该异常错误消息。
导致问题的代码:
这段代码的问题是线程立即启动并返回,导致返回DialogResult,该DialogResult在线程可以从字段中正确获取值之前处理对话框。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private void Dialog_SendEmailSummary_Button_Click(object sender, EventArgs e)
{
SendSummaryEmail();
DialogResult = DialogResult.OK;
}
private void SendSummaryEmail()
{
var t = new Thread(() => SendSummaryThread(Textbox_Subject.Text, Textbox_Body.Text, Checkbox_IncludeDetails.Checked));
t.Start();
}
private void SendSummaryThread(string subject, string comment, bool includeTestNames)
{
// ... Create and send the email.
} |
此方案的解决方案:
解决方法是在将值传递到创建线程的方法之前,先获取并存储这些值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private void Dialog_SendEmailSummary_Button_Click(object sender, EventArgs e)
{
SendSummaryEmail(Textbox_Subject.Text, Textbox_Body.Text, Checkbox_IncludeDetails.Checked);
DialogResult = DialogResult.OK;
}
private void SendSummaryEmail(string subject, string comment, bool includeTestNames)
{
var t = new Thread(() => SendSummaryThread(subject, comment, includeTestNames));
t.Start();
}
private void SendSummaryThread(string subject, string comment, bool includeTestNames)
{
// ... Create and send the email.
} |