.NET Framework 4.8调用OpenFileDialog.ShowDialog异常卡顿及内存泄漏问题
.NET Framework 4.8 调用OpenFileDialog卡顿及内存泄漏问题
问题表现
在.NET Framework 4.8环境下调用OpenFileDialog.ShowDialog时出现无响应卡顿,同时内存占用持续攀升出现泄漏。即使内存涨到数GB,托管内存快照中也没有可观测的异常对象增长。
- 对话框打开的初始目录文件数量极少,其他应用(如Visual Studio 2019)打开同目录的文件对话框无任何问题
- 应用无异常定时任务逻辑,打开对话框的代码路径确认运行在UI线程,该功能已稳定运行多年,出现异常前仅在无关代码区域新增了报表打印功能
已完成排查进展
- 系统仅安装了一个稳定运行多年的WinMerge Shell扩展
- 虚拟机环境中运行该程序功能完全正常
- 加载微软及NuGet官方符号文件后确认,泄漏发生在Windows非托管代码层:泄漏内存属于
unresolved allocations类型,为持续增长的不透明void*内存地址列表,关联调用栈如下:
ntdll.dll!NtTraceEvent() ntdll.dll!RtlpLogHeapFreeEvent() ntdll.dll!RtlpFreeHeap() ntdll.dll!RtlpFreeHeapInternal() ntdll.dll!RtlFreeHeap() ntdll.dll!RtlDebugFreeHeap() ntdll.dll!RtlpFreeHeap() ntdll.dll!RtlpFreeHeapInternal() ntdll.dll!RtlFreeHeap() KernelBase.dll!LocalFree() ExplorerFrame.dll!CTContainer_PolicyLocalMem::DestroyMem(void *) ExplorerFrame.dll!TabletModeHelpers::GetMonitorConfig(int *, bool *) ExplorerFrame.dll!SHIsFileExplorerInTabletMode(void) ExplorerFrame.dll!UICheckbox::OnHosted() dui70.dll!DirectUI::Element::OnHosted() ExplorerFrame.dll!UIItem::OnHosted(class DirectUI::Element *) dui70.dll!DirectUI::Element::OnPropertyChanged() ExplorerFrame.dll!UIItem::OnPropertyChanged() dui70.dll!DirectUI::Element::_PostSourceChange() dui70.dll!DirectUI::Element::Insert() ExplorerFrame.dll!UICollection::_CreateAndInsertItems() ExplorerFrame.dll!UICollection::_RealizeItems() ExplorerFrame.dll!UICollection::RealizeSection(class SectionInfo *) ExplorerFrame.dll!LineScroller::_RealizeDirectionWorker() ExplorerFrame.dll!LineScroller::_RealizeDirectionWorker() ExplorerFrame.dll!LineScroller::_RealizeDirectionWorker() ExplorerFrame.dll!LineScroller::_RealizeAroundAnchor() ExplorerFrame.dll!LineScroller::_RealizeContent() ExplorerFrame.dll!LineScroller::_LayoutContent() ExplorerFrame.dll!LineScroller::_Viewer_SelfLayoutDoLayout(int,int) dui70.dll!DirectUI::Element::_FlushLayout() dui70.dll!DirectUI::Element::_FlushLayout() dui70.dll!DirectUI::Element::_FlushLayout() dui70.dll!DirectUI::Element::_FlushLayout() dui70.dll!DirectUI::Element::_FlushLayout() dui70.dll!DirectUI::DeferCycle::_EndDefer() dui70.dll!DirectUI::Element::EndDefer() dui70.dll!DirectUI::Element::_PostSourceChange() dui70.dll!DirectUI::Element::_SetValue() dui70.dll!DirectUI::Element::SetValue(struct DirectUI::PropertyInfo const * (*)(void),int,class DirectUI::Value *) ExplorerFrame.dll!LineViewer::SetLayoutState(int) ExplorerFrame.dll!LineScroller::_EnsureRealizePass(void) ExplorerFrame.dll!LineScroller::_OnLayoutChangeEvent(struct LayoutChangeEvent *) ExplorerFrame.dll!LineScroller::OnEvent() dui70.dll!DirectUI::Element::s_HandleDUIEventMessage(class DirectUI::Element *,struct EventMsg *) dui70.dll!DirectUI::Element::_DisplayNodeCallback() duser.dll!GPCB::xwInvokeDirect() duser.dll!SafeMsgQ::xwProcessNL() duser.dll!SafeMsgQ::xwProcessNL(void) duser.dll!CoreSC::xwProcessMsgQNL(void) duser.dll!CoreSC::xwProcessNL() duser.dll!MphProcessMessage() user32.dll!__ClientGetMessageMPH() ntdll.dll!KiUserCallbackDispatcherContinue() win32u.dll!NtUserPeekMessage() user32.dll!_PeekMessage() user32.dll!DialogBox2() user32.dll!InternalDialogBox() user32.dll!DialogBoxIndirectParamAorW() user32.dll!DialogBoxIndirectParamW() comdlg32.dll!<lambda>(void)() comdlg32.dll!CFileOpenSave::Show(struct HWND__*) [Managed to Native Transition] System.Windows.Forms.dll!System.Windows.Forms.FileDialog.RunDialogVista(System.IntPtr hWndOwner) System.Windows.Forms.dll!System.Windows.Forms.CommonDialog.ShowDialog(System.Windows.Forms.IWin32Window owner) Bingo.exe!OPEQ.frmBingo.tsmiFileOpen_Click(object sender, System.EventArgs e) Line 1098
关联业务代码如下,卡顿发生在openFileDialog1.ShowDialog(this)调用行:
private void tsmiFileOpen_Click(object sender, EventArgs e) { Logging.Method(); // 获取程序启动时启动的对话框过滤器描述检索任务 // 如果任务失败或尚未完成,则使用初始值 CultureInfo uiCulture = Thread.CurrentThread.CurrentUICulture; TaskParams openDialogFilterTaskParams; if (_tasks.TryGetValue(_fileDialogFilterTaskKey, out openDialogFilterTaskParams)) { bool removeFromTasks = false; // 优先取任务结果,否则使用现有过滤器 switch (openDialogFilterTaskParams.Task.Status) { case TaskStatus.RanToCompletion: // 更新过滤器变量 var odfTask = (Task<IDictionary<CultureInfo, FileDialogFilterList>>)openDialogFilterTaskParams.Task; foreach (var kvPair in odfTask.Result) { // 如果GetFileTypeName调用失败,返回的描述为null,此处做兼容处理 var updatedList = ReplaceNullDescriptions(kvPair.Value, kvPair.Key); _openDialogFilterDict[kvPair.Key] = updatedList.CopyEmbellished().ToString(); } removeFromTasks = true; break; case TaskStatus.Canceled: case TaskStatus.Faulted: removeFromTasks = true; // 使用过滤器初始值 break; default: // 使用过滤器初始值 break; } if (removeFromTasks) { try { // 此处主动调用Wait是为了避免将事件处理程序改为异步 openDialogFilterTaskParams.Task.Wait(openDialogFilterTaskParams.TokenSource.Token); } catch(OperationCanceledException) { // 忽略取消异常 } catch (AggregateException aggEx) when (aggEx.InnerExceptions.Any(ex => !(ex is OperationCanceledException))) { // 记录并忽略异常 Logging.Exception(aggEx, isRecoverableError: true, includeStackTrace: true); } finally { openDialogFilterTaskParams.Task.Dispose(); openDialogFilterTaskParams.TokenSource.Dispose(); _tasks.TryRemove(_fileDialogFilterTaskKey, out _); } } } // 对话框会保证用户选择的文件存在,无需额外校验 // 无需处理初始目录,对话框会自动处理 openFileDialog1.Filter = _openDialogFilterDict[uiCulture]; openFileDialog1.DefaultExt = _openDialogDefaultExtension; // 程序在下行代码处挂起 if (openFileDialog1.ShowDialog(this) == DialogResult.OK) { // 原子化处理任务创建、字典添加和检索 var fileLoadTaskParams = _tasks.GetOrAdd(_fileLoadingTaskKey, (_, fnames) => { TaskParams tParam = new TaskParams( null, null, new Progress<ReportLoaderProgressChangedEventArgs>()); try { tParam.TokenSource = CancellationTokenSource.CreateLinkedTokenSource(_tidyShutdownCts.Token); tParam.Task = new ReportLoader(fnames).LoadAsync(tParam.TokenSource.Token, (IProgress<ReportLoaderProgressChangedEventArgs>)tParam.Additional); } catch { tParam.TokenSource?.Dispose(); tParam.Task?.Dispose(); throw; } return tParam; }, openFileDialog1.FileNames.AsEnumerable()); // 设置默认扫描模式 // 加载多个文件时为搜索模式,仅加载一个文件时为校验模式 _scanModeSelected = openFileDialog1.FileNames.Length > 1 ? tsBtnScanType_Search : tsBtnScanType_Verification; /* 将获取到的参数传递给LoadFile,LoadFile负责向用户展示非关键异常 * 严重异常会从该方法抛出到FireAndForget,此时允许应用退出 */ LoadFile( (Task<OPEQReportDataSet>)fileLoadTaskParams.Task, fileLoadTaskParams.TokenSource, (Progress<ReportLoaderProgressChangedEventArgs>)fileLoadTaskParams.Additional) .FireAndForget(null, null, () => { fileLoadTaskParams.Task.Dispose(); fileLoadTaskParams.TokenSource?.Dispose(); _tasks.TryRemove(_fileLoadingTaskKey, out _); }); } }
最新进展
程序已无征兆恢复正常,推测问题与Windows系统更新相关。
排查思路参考
- 系统补丁校验:从调用栈可以看出泄漏出现在ExplorerFrame.dll的平板模式检测逻辑中,可先排查最近安装的Windows累积更新,尤其是涉及文件资源管理器、平板模式适配的补丁,卸载可疑更新后复测
- Shell扩展兼容验证:虽然WinMerge已稳定运行多年,仍可临时禁用所有非微软Shell扩展,确认是否是扩展与新系统补丁兼容问题导致的异常
- 对话框降级适配:调用
OpenFileDialog前设置AutoUpgradeEnabled = false,强制使用XP风格的旧版文件对话框,绕过基于DirectUI实现的新版对话框逻辑,验证是否能规避问题 - 系统状态重置:检查系统平板模式开关是否处于异常状态、多显示器配置是否有异常,可尝试重置文件资源管理器设置、重启桌面窗口管理器(dwm.exe)后复测
内容的提问来源于stack exchange,提问作者Geoff
相关产品推荐
相关产品推荐

