我在WinForms中跟踪了很多UI未按预期卸载的问题。
以下是一些总体提示:
突破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 Object、GDI Object和Handles列显示出来,如下所示,要执行此操作,请选择View菜单> Select Columns:
按照引起问题的步骤,观察USER Object计数增加到约10,000个或GDI Objects或Handles达到其极限。
当您看到Object或Handles增加(通常会显著增加)时,可以通过单击Pause按钮在Visual Studio中停止代码执行。
然后只需按住F10或F11浏览代码,看到Object/Handle计数显著增加时即可。
到目前为止我发现的最好的工具是来自NirSoft的GDIView,它将GDI Handle字段拆分:
我把它追溯到DataGridView设置“Filter Combobox”列位置和宽度时使用的此代码:
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
在我的情况下(如上所述),显式释放和清理解决了释放USER对象的问题。
以下是堆栈跟踪:
在System.Windows.Forms.Control.CreateHandle() 中 在System.Windows.Forms.ComboBox.CreateHandle() 中 在System.Windows.Forms.Control.get_Handle() 中 在System.Windows.Forms.ComboBox.InvalidateEverything() 中 在System.Windows.Forms.ComboBox.OnResize(EventArgs e) 中 在System.Windows.Forms.Control.OnSizeChanged(EventArgs e) 中 在System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32width, Int32 height, Int32 clientWidth, Int32 clientHeight) 中 在System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32width, Int32 height) 中 在System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32width, Int32 height, BoundsSpecified specified) 中 在System.Windows.Forms.ComboBox.SetBoundsCore(Int32 x, Int32 y, Int32width, Int32 height, BoundsSpecified specified) 中 在System.Windows.Forms.Control.SetBounds(Int32 x, Int32 y, Int32 width,Int32 height, BoundsSpecified specified) 中 在System.Windows.Forms.Control.set_Width(Int32 value)
以下是Fabrice的一篇有用文章的核心部分,该文章帮助我弄清楚了限制:
我们应该关注的另一种资源类型是GDI(图形设备接口)对象和用户对象。您可以在MSDN上获取这三类资源的概述。
我们尝试确定应用程序可以使用多少个用户对象的限制。 每个进程有10,000个用户句柄的配额。这个值可以在注册表中更改,但在我们的情况下,这个限制并不是真正的阻碍因素。 另一个限制是每个Windows会话的66,536个用户句柄。这个限制是理论上的。实际上,您会注意到它无法达到。在我们的情况下,在当前会话中的用户对象总数达到11,000之前,我们就遇到了可怕的“创建窗口句柄错误”异常。
桌面堆
然后,我们发现真正的罪魁祸首是“桌面堆”。
默认情况下,交互式用户会话的所有图形应用程序都在所谓的“桌面”中执行。分配给这样的桌面的资源是有限的(但可配置)。
注意:用户对象是消耗桌面堆内存空间的主要对象。这包括窗口。 有关桌面堆的更多信息,请参考NTDebugging MSDN博客上发布的非常好的文章:
真正的解决方案是什么?做绿色应用!
增加桌面堆是一种有效的解决方案,但不是最终解决方案。真正的解决方案是消耗更少的资源(在我们的情况下是较少的窗口句柄)。我可以猜测您对这个解决方案会感到非常失望。这真的是我能想出的全部吗?
好吧,在这里没有什么大秘密。唯一的出路就是要简洁。拥有更简单的用户界面是一个很好的开始。这对资源和可用性都有好处。接下来的步骤是避免浪费,保护资源并回收它们!
以下是我们在客户应用程序中执行此操作的方式:
我们使用TabControls,并在其变为可见时动态创建每个选项卡的内容; 我们使用可扩展/可折叠区域,并在需要时再填充控件和数据; 尽快释放资源(使用Dispose方法)。当一个区域被折叠时,可以清除其子控件。当选项卡变为隐藏状态时也是如此; 我们使用MVP设计模式,它有助于使上述操作成为可能,因为它将数据与视图分离; 我们使用布局引擎,标准的FlowLayoutPanel和TableLayoutPanel以及自定义的引擎,而不是创建深层次的嵌套面板、GroupBoxes和Splitters(空Splitters本身就消耗了三个窗口句柄...)。 以上只是提示,如果您需要构建丰富的Windows Forms屏幕,您可以做什么。毫无疑问,您可以找到其他方法。 在我看来,您应该首先基于用例和场景构建应用程序。这有助于仅在给定时间和给定用户需要时显示所需内容。
当然,另一个解决方案是使用不依赖于句柄的系统...... WPF呢?
当我手动调用CreateHandler时,我在子类化NativeWindow时遇到了这个错误。问题是我忘记在我的重写版本的WndProc中添加base.WndProc(m)。这导致了相同的错误。
我遇到了这个异常,因为我创建了许多UI控件并设置了它们的属性,导致了无限循环。当改变控件的可见性属性时,抛出了这个异常。我发现用户对象和GDI对象(从任务管理器中查看)都非常大。
我猜测你的问题可能是由于系统资源被这些UI控件耗尽所导致的。
我在工作中使用Janus Controls。就控件自我处理而言,它们非常容易出现错误。我建议您确保正确处理它们的处置。另外,有时与它们绑定的对象无法释放,因此您必须手动解除对象绑定以处置控件。
在向面板添加控件时,我遇到了这个异常,因为面板中的子控件没有清除。如果清除面板中的子控件,则可以修复该错误。
For k = 1 To Panel.Controls.Count
Panel.Controls.Item(0).Dispose()
Next
我遇到了相同的 .Net 运行时错误,但我的解决方案不同。
我的情况: 从一个返回 DialogResult 的弹出式对话框中,用户会单击一个按钮来发送电子邮件。我添加了一个线程,以便在后台生成报告时 UI 不会锁定。这种情况最终导致了那个不寻常的错误消息。
导致问题的代码: 这段代码的问题在于线程立即启动并返回,导致 DialogResult 被返回并且在线程能够正确从字段中获取值之前关闭了对话框。
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.
}
针对此情况的解决方案: 解决方案是在将值传递给创建线程的方法之前抓取并存储这些值。
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.
}
while (mGridPanel.Controls.Count > 0)
mGridPanel.Controls[0].Dispose();
当我在我的WinForm应用程序中开始使用线程时,发生了相同的错误, 我使用堆栈跟踪查找是什么导致错误,并发现Infragistics的UltraDesktopAlert组件是原因,所以我以不同的方式调用它,现在错误已经消失。
this.Invoke((MethodInvoker)delegate
{
//call your method here
});
完整的代码将是这样。
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
Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems
但我的已经具有相同的值。只有调用方法委托对我有用。希望这可以帮到你。