WPF中的win32窗口

18

我们的应用程序最近遇到了一个奇怪的问题。

该应用程序在WPF窗口中有一个win32窗口,在调整WPF窗口大小时,问题出现了。

堆栈跟踪:

Exception object: 0000000002ab2c78
Exception type: System.OutOfMemoryException
InnerException: <none>
StackTrace (generated):
    SP       IP       Function
    0048D94C 689FB82F PresentationCore_ni!System.Windows.Media.Composition.DUCE+Channel.SyncFlush()+0x80323f
    0048D98C 681FEE37 PresentationCore_ni!System.Windows.Media.Composition.DUCE+CompositionTarget.UpdateWindowSettings(ResourceHandle, RECT, System.Windows.Media.Color, Single, System.Windows.Media.Composition.MILWindowLayerType, System.Windows.Media.Composition.MILTransparencyFlags, Boolean, Boolean, Boolean, Int32, Channel)+0x127
    0048DA38 681FEAD1 PresentationCore_ni!System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean, System.Nullable`1<ChannelSet>)+0x301
    0048DBC8 6820718F PresentationCore_ni!System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean)+0x2f
    0048DBDC 68207085 PresentationCore_ni!System.Windows.Interop.HwndTarget.UpdateWindowPos(IntPtr)+0x185
    0048DC34 681FFE9F PresentationCore_ni!System.Windows.Interop.HwndTarget.HandleMessage(Int32, IntPtr, IntPtr)+0xff
    0048DC64 681FD0BA PresentationCore_ni!System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr, Int32, IntPtr, IntPtr, Boolean ByRef)+0x3a
    0048DC88 68C6668E WindowsBase_ni!MS.Win32.HwndWrapper.WndProc(IntPtr, Int32, IntPtr, IntPtr, Boolean ByRef)+0xbe
    0048DCD4 68C665BA WindowsBase_ni!MS.Win32.HwndSubclass.DispatcherCallbackOperation(System.Object)+0x7a
    0048DCE4 68C664AA WindowsBase_ni!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate, System.Object, Boolean)+0x8a
    0048DD08 68C6639A WindowsBase_ni!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(System.Object, System.Delegate, System.Object, Boolean, System.Delegate)+0x4a
    0048DD50 68C64504 WindowsBase_ni!System.Windows.Threading.Dispatcher.WrappedInvoke(System.Delegate, System.Object, Boolean, System.Delegate)+0x44
    0048DD70 68C63661 WindowsBase_ni!System.Windows.Threading.Dispatcher.InvokeImpl(System.Windows.Threading.DispatcherPriority, System.TimeSpan, System.Delegate, System.Object, Boolean)+0x91
    0048DDB4 68C635B0 WindowsBase_ni!System.Windows.Threading.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority, System.Delegate, System.Object)+0x40
    0048DDD8 68C65CFC WindowsBase_ni!MS.Win32.HwndSubclass.SubclassWndProc(IntPtr, Int32, IntPtr, IntPtr)+0xdc

StackTraceString: <none>
HResult: 8007000e

此外,我找到了一些相关链接:

relatedA

relatedB

  1. 有没有办法避免或处理这个问题?

  2. 如何找出真正的问题?

  3. 从调用栈中,我们能否确定问题来自.NET Framework?

感谢您的回答或评论!


whunmr,你是否找到了问题的根本原因?如果找到了,能否分享一下你的发现? - Charlie
嗨,查理,我不认为我已经找到了这个问题的真正原因。这是很久以前的事情了。 - whunmr
4个回答

28
你遇到的问题不是由托管内存泄漏造成的。显然,您在非托管代码中某个地方触发了一个bug。
SyncFlush()方法在多次MILCore调用之后被调用,它似乎会立即处理已发送的更改,而不是将其留在队列中以供稍后处理。由于该调用处理先前发送的所有内容,因此不能从您发送的调用堆栈中排除视觉树中的任何部分。
包含非托管调用的调用堆栈可能会提供更有用的信息。使用VS.NET进行本机调试或使用windbg或其他本机代码调试器运行应用程序。设置调试器在异常时中断,并在相对断点处获取调用堆栈。
当然,调用堆栈将下降到MILCore中,然后可能进入DirectX层和DirectX驱动程序。在这个本机调用堆栈中可能会找到导致问题的代码的线索。
很可能是基于你提供的信息,MILCore将一些参数的巨大值传递给DirectX。检查您的应用程序是否存在可能导致DirectX分配大量内存的错误,例如:
- 设置为加载非常高分辨率的BitmapSources。 - 大型WritableBitmaps。 - 极大(或负)的变换或大小值。
另一种解决此问题的方法是逐步简化应用程序,直到问题消失,然后非常仔细地查看您最后删除的内容。如果方便,可以将其作为二分搜索进行:最初削减视觉复杂性的一半。如果可行,则将削减的一半放回,否则再删除一半。重复此操作直至完成。
还要注意,通常无需实际删除UI组件即可防止MILCore看到它们。任何具有Visibility.Hidden的Visual都可以完全跳过。
没有一般化的方法可以避免此问题,但是搜索技术将帮助您确定特定情况下需要更改以修复它的问题。
可以从调用堆栈中安全地说,您在NET Framework或特定视频卡的DirectX驱动程序中发现了一个错误。
关于您发布的第二个堆栈跟踪
John Knoeller正确指出,从RtlFreeHeap到ConvertToUnicode的转换是无意义的,但是得出了错误的结论。我们看到的是当追踪堆栈时,调试器丢失了方向。 它从异常正确启动,但在Assembly.ExecuteMainMethod框架下面丢失了,因为在处理异常并调用调试器时,该堆栈部分已被覆盖。很不幸,这个堆栈跟踪分析对你的目的毫无用处,因为它被捕获得太晚了。我们看到的是在处理 WM_LBUTTONDOWN 时发生异常,该消息被转换为 WM_SYSCOMMAND,然后捕获了一个异常。换句话说,你点击了某些东西,导致系统命令(例如调整大小)引发了异常。在捕获此堆栈跟踪时,异常已经被处理。你看到 User32 和 UxTheme 调用的原因是它们涉及处理按钮单击事件,与真正的问题无关。
你已经走上了正确的道路,但你需要在内存分配失败的那一刻捕获堆栈跟踪(或者你可以使用我上面提出的其他方法之一)。
当你的第一个堆栈跟踪中的所有托管帧都出现在它里面,并且堆栈顶部是失败的内存分配时,你就会知道你有了正确的堆栈跟踪。请注意,我们只对出现在 DUCE+Channel.SyncFlush 调用之上的未托管帧感兴趣——下面的所有内容都将是 .NET Framework 和你的应用程序代码。
如何在正确的时间获得本地堆栈跟踪
你想在 DUCE+Channel.SyncFlush 调用内的第一个内存分配失败时获得堆栈跟踪,这可能有点棘手。我使用了三种方法:(请注意,在每种情况下,你都要从 SyncFlush 调用内部开始设置断点——有关更多详细信息,请参见下面的说明)
  1. 将调试器设置为在所有异常(托管和未托管)发生时暂停,然后持续按“g”键(或 F5),直到它在你感兴趣的内存分配异常上中断。这是首先要尝试的事情,因为它很快,但当与本地代码一起工作时通常会失败,因为本地代码经常向调用本地代码的调用方返回错误代码,而不是抛出异常。
  2. 将调试器设置为在所有异常上中断,并在常见的内存分配例程上设置断点,然后重复按 F5 键(继续执行),直到异常发生,并计算你按了多少次 F5。下次运行时,使用较少的 F5 次数,你可能会位于生成异常的分配调用上。将调用堆栈捕获到记事本中,然后从那里反复使用 F10 键(逐过程)查看是否真的是分配失败。
  3. 在SyncFlush调用的第一个本地帧(wpfgfx_v0300!MilComposition_SyncFlush)上设置断点以跳过托管到本地转换,然后按F5运行到它。通过F10(步过)执行函数,直到EAX包含错误代码E_OUTOFMEMORY(0x8007000E)、ERROR_OUTOFMEMORY(0x0000000E)或ERROR_NOT_ENOUGH_MEMORY(0x0000008)中的一个。注意最近的“Call”指令。下次运行程序时,运行到那里并进入其中。重复此过程,直到你找到导致问题的内存分配调用并转储堆栈跟踪。请注意,在许多情况下,您将发现自己循环浏览一个较大的数据结构,因此需要一些智能来设置适当的断点,以跳过循环,以便快速到达所需位置。这种技术非常可靠,但需要付出很多人力。
    注意:在每种情况下,您都不想在应用程序处于失败的DUCE + Channel.SyncFlush调用中之前设置断点或开始单步执行。为确保此操作,请使用所有断点禁用启动应用程序。当它运行时,在System.Windows.Media.Composition.DUCE+Channel.SyncFlush上启用断点并调整窗口大小。第一次只需按F5,以确保异常在第一个SyncFlush调用上失败(如果不是这样,则计算在异常发生之前您必须按F5多少次)。然后禁用断点并重新启动程序。重复该过程,但这次在命中正确的SyncFlush调用后,像上面描述的那样设置断点或进行单步执行。
    推荐:
    我描述的调试技术需要耗费大量人力:计划至少花费几个小时。因此,我通常会尝试反复简化我的应用程序,以找出准确触发错误的内容,然后再跳进调试器。这有两个优点:它将为您提供一个良好的可重现情况以发送给显卡供应商,并且将使您的调试更快,因为显示的内容较少,因此需要单步执行的代码较少,分配也较少。
    由于问题仅发生在特定的显卡上,因此毫无疑问,问题要么是显卡驱动程序的错误,要么是调用它的MilCore代码的错误。最可能的情况是显卡驱动程序中存在错误,但是有可能MilCore正在传递无效值,这些值由大多数显卡正确处理,但不包括这个显卡。我描述的调试技术将告诉您是否是这种情况:例如,如果MilCore告诉显卡分配1000000x1000000像素区域,并且显卡提供了正确的分辨率信息,则错误可能在MilCore中。但是,如果MilCore的请求是合理的,则错误可能在显卡驱动程序中。

2
嗨,雷, 我转储了托管和非托管堆栈: 我可以说问题来自于“uxtheme!_ThemeDefWindowProc”吗? - whunmr
2
不,这与UxTheme无关。UxTheme代码只是处理您的按钮点击。堆栈跟踪是正确类型的跟踪,但是没有在正确的时间进行。我已经在我的答案中添加了更多解释,并提供了获取良好堆栈跟踪的一些提示。希望它们有所帮助。 - Ray Burns

2

这是一篇关于WPF内存泄漏的有用文章。您还可以考虑使用来自RedGate的ANTS性能和/或内存分析器来帮助诊断此类问题。


是的... Ants内存分析器看起来对我来说是最好的选择。 - Anvaka

1

我不确定堆栈部分(或至少是UXTheme部分)是否可靠。堆栈底部似乎正常。我们看到一个异常处理程序试图进行清理。然后有大量嵌套调用各种层次的堆管理代码。

但是,从 RtlFreeHeapConvertToUnicode 的堆栈转换部分没有任何意义。我怀疑上面的所有内容都是之前使用堆栈留下的。

0048f40c 6b88f208 mscorwks!_EH_epilog3_GS+0xa, calling mscorwks!__security_check_cookie 
0048f410 6b8a756e mscorwks!SString::ConvertToUnicode+0x81, calling mscorwks!_EH_epilog3_GS 
0048f424 77b4371e ntdll_77b10000!RtlpFreeHeap+0xbb1, calling ntdll_77b10000!RtlLeaveCriticalSection 
0048f42c 77b436fa ntdll_77b10000!RtlpFreeHeap+0xb7a, calling ntdll_77b10000!_SEH_epilog4 

RtlFreeHeap 中的崩溃指向堆损坏,这表明问题在非托管代码中,但托管对象的内存最终必须从非托管内存中分配,因此可能是两者都有问题。

我建议您查找非托管窗口可能会破坏堆的地方;多次释放相同的分配或覆盖分配的边界。


谢谢你的分析。现在我可以得出结论,UXTheme工作正常。 - whunmr
1
@John Knoeller:是的,您所识别的转换是堆栈覆盖的一部分。您的其余分析是合理的,但是原始跟踪显示OutOfMemoryException发生在SyncFlush内,并响应按钮单击。由于RtlpFreeHeap调用完全位于所有托管代码之外,包括Application.Run,因此该调用堆栈显然未显示与先前异常相同的异常。具体而言,它未显示导致在调整大小时引发内存不足异常的错误。 - Ray Burns
请注意,问题中的堆栈覆盖是由于线程被关闭而引起的。最终退出可能包括来自RtlpFreeHeap的GP故障,但在堆栈跟踪中没有任何可见的迹象表明实际情况是如此。同样有可能根本没有损坏,我们看到的是由于未处理的OutOfMemoryException而导致的正常异常退出。然而,在这些情况下检查您的非托管代码是否可能破坏堆总是一个好主意。 - Ray Burns
@Ray: 很有道理。我假设在某个时刻,特别是大型托管分配请求可能会触发非托管分配。我还假设他并没有真正没有内存。根据我的经验,当你实际上用完内存时,你的进程就会崩溃得如此严重,以至于它无法在一个友好的对话框中报告“内存不足”。 - John Knoeller

0
如果有人遇到SyncFlush问题,我们通过我的MSDN订阅获得了微软出色的支持,解决了这个问题。原来我们创建的多媒体定时器比使用timeBeginPeriod和timeEndPeriod调用释放的还要多。这些定时器是有限资源,一旦用完,WPF渲染线程就会因为缺少定时器而停止工作。

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