博客 / 詳情

返回

記調試 RX-Explorer-WAS 文件管理器 UI 未響應問題

開始之前,先提供 RX-Explorer-WAS 的安裝地址,通過應用商店即可安裝: https://apps.microsoft.com/detail/9pdn2q3dcqs3

在我設備上覆現打開黑屏問題的界面如下圖

此時非常快速的第一反映就是打開 Visual Studio 進行附加調試。有開發環境的機器上,就不要去打 DUMP 分析了,通過 DUMP 分析是不如直接用開發機的 Visual Studio 附加調試來得爽的

點擊 Visual Studio 的 調試->附加到進程 選項,選擇未響應窗口對應的進程,然後勾選代碼類型為託管類型

遇到這類未響應問題,優先勾選託管代碼調試,附帶加上託管(本機編譯)選項,不先勾選本機調試。這樣做的原因是大部分情況下,可以通過託管代碼快速定位到卡住的問題,無需掛載本機調試來干擾調試

掛進去之後,現在的進程還在正常跑着,點一下暫停,看看 UI 主線程卡在哪裏了

這個時候我習慣讓 Visual Studio 的調試界面佈局為一邊為調用堆棧,一邊為線程。如此可以快速切線程來查看各個線程當前所在的堆棧情況。通過 Visual Studio 的 調試->窗口 選項裏面,可以打開調用堆棧、線程等窗口

我看到這裏,我開始就以為一定就是 RX_Explorer_WAS.UI.Views.FilePresenter.FilePresenter 寫了什麼代碼導致的卡住。但是具體是什麼呢,這裏看不到。於是我就去找了 Ruofan 要來了 pdb 符號文件,此時有符號文件是很有幫助的

那會的我,認為一定是在 RX_Explorer_WAS.UI.Views.FilePresenter.FilePresenter 寫了什麼代碼導致的卡住。再或者就是遇到靜態構造函數進鎖的問題,為什麼這麼説呢?這是因為如果某個類型的靜態構造函數被另一個線程執行,且執行過程中進入了鎖等無法結束的情況,那另一個線程再碰觸到這個類型時,將會進入等待狀態,此時的調用堆棧看起來也差不多這樣

要到了符號文件之後,點擊 調試->窗口->模塊 找到了對應的 DLL 右擊點加載符號,選擇符號文件

加載符號之後,可以看到卡在這一行了:

按照我的經驗,優先懷疑是靜態構造函數相關問題,畢竟正在 new 一個對象而已,這個過程不應該有任何問題才對。因為如果發生在 new 對象裏面,那應該堆棧就去到對象的構造函數裏了,而不是在這裏

此時我就在線程窗口裏面,從上到下全看了一遍,然而沒有發現任何的調用堆棧可能發生在靜態構造函數裏面,此時調試過程卡住了,這不是一個常見問題

我向 Ruofan 要了這部分的源代碼,然而經過我仔細閲讀,我都沒有找到問題

看起來優先懷疑是靜態構造函數這個事情是不成立的,此時我應該懷疑的是調用堆棧的最上方,是否因為碰到了某些非託管代碼導致的卡住。畢竟最上方就是 [託管到本機的轉換] 證明進入卡的是本機代碼,即非託管代碼

 	[託管到本機的轉換]	
>	RX-Explorer-WAS.dll!RX_Explorer_WAS.UI.Views.FilePresenter.FilePresenter() 行 110	C#
 	RX-Explorer-WAS.dll!RX_Explorer_WAS.RX_Explorer_WAS_XamlTypeInfo.XamlTypeInfoProvider.Activate_248_FilePresenter() 行 1024	C#
 	Microsoft.WinUI.dll!ABI.Microsoft.UI.Xaml.Markup.IXamlType.Do_Abi_ActivateInstance_13(nint thisPtr, nint* result)	未知

停下調試,再次附加進程,這一次勾選了本機調試選項

此時附加過程中出現了一個調試異常,如下圖所示,忽略即可,忽略方法就是點擊繼續運行

再次點擊暫停,看看這一次的主線程堆棧

似乎是卡在加載某個程序集的過程了

>	ntdll.dll!NtWaitForSingleObject()	未知
 	ntdll.dll!LdrpDrainWorkQueue()	未知
 	ntdll.dll!LdrpLoadDllInternal()	未知
 	ntdll.dll!LdrpLoadDll()	未知
 	ntdll.dll!LdrLoadDll()	未知
 	KernelBase.dll!LoadLibraryExW()	未知
 	[託管到本機的轉換]	
 	RX-Explorer-WAS.dll!RX_Explorer_WAS.UI.Views.FilePresenter.FilePresenter()	未知
 	RX-Explorer-WAS.dll!RX_Explorer_WAS.RX_Explorer_WAS_XamlTypeInfo.XamlTypeInfoProvider.Activate_248_FilePresenter()	未知
 	Microsoft.WinUI.dll!ABI.Microsoft.UI.Xaml.Markup.IXamlType.Do_Abi_ActivateInstance_13(nint thisPtr, nint* result)	未知

那這裏會是加載哪個程序集呢?是否加載了某個犯天條的程序集導致的卡頓。這樣的事情我是遇到過的,我在某些用户設備上調試過,加載某些 DLL 需要花超級長的時間

但是在這裏開啓了託管調試導致無法看出來加載的是哪個 DLL 導致的卡頓。還請記住這句話,後面要考。我就是在這裏思路錯誤的,我一直嘗試追蹤是加載哪個 DLL 時出現卡頓的。既然是經驗貼,我就不掩蓋我的錯誤調試過程了,按照我的一路調試順序告訴大家啦

我點擊停止調試,準備這一次只開啓本機調試方式進行附加調試。只開啓本機調試的方式,可以展示出整個 CoreCLR 的執行過程,如此可以看到在加載的發起方 CoreCLR 的調用堆棧,進而瞭解到正在加載的程序集是哪個

再次附加調試就看到了 CoreCLR 發起加載程序集的堆棧了

 	ntdll.dll!NtWaitForSingleObject()	未知
 	ntdll.dll!LdrpDrainWorkQueue()	未知
 	ntdll.dll!LdrpLoadDllInternal()	未知
 	ntdll.dll!LdrpLoadDll()	未知
 	ntdll.dll!LdrLoadDll()	未知
 	KernelBase.dll!LoadLibraryExW()	未知
>	coreclr.dll!LoadLibraryExWrapper(const wchar_t * lpLibFileName=0x000000d700000000, void * dwFlags=8, unsigned long) 行 273	C++
 	[內聯框架] coreclr.dll!CLRLoadLibraryExWorker(const wchar_t *) 行 981	C++
 	[內聯框架] coreclr.dll!CLRLoadLibraryEx(const wchar_t *) 行 999	C++
 	coreclr.dll!LoadedImageLayout::LoadedImageLayout(PEImage * pOwner, HRESULT * loadFailure=0x000000d746578c60) 行 548	C++
 	coreclr.dll!PEImageLayout::Load(PEImage * pOwner=0x0000027670282b10, HRESULT * loadFailure=0x000000d746578c60) 行 140	C++
 	coreclr.dll!PEImage::CreateLoadedLayout(bool throwOnFailure=false) 行 675	C++
 	coreclr.dll!PEImage::GetOrCreateLayoutInternal(unsigned long imageLayoutMask) 行 647	C++
 	coreclr.dll!PEImage::GetOrCreateLayout(unsigned long imageLayoutMask=15) 行 586	C++
 	coreclr.dll!BinderAcquireImport(PEImage * pPEImage=0x0000027670282b10, IMDInternalImport * * ppIAssemblyMetaDataImport=0x000000d746578e10, unsigned long * pdwPAFlags=0x000000d746578e20) 行 125	C++
 	coreclr.dll!BINDER_SPACE::AssemblyName::Init(PEImage * pPEImage) 行 62	C++
 	coreclr.dll!BINDER_SPACE::Assembly::Init(PEImage * pPEImage=0x0000027670282b10, int fIsInTPA=1) 行 44	C++
    ...

點擊進入 LoadLibraryExWrapper 這一行,此時我習慣讓 Visual Studio 的調試界面一邊是調用堆棧,一邊是局部變量窗口。如此可以在切調用堆棧時,看到各個方法的局部變量情況。此時我看到了有 path 變量,再點擊 Visual Studio 的 調試->窗口->內存->內存1 選項卡,打開內存查看窗口。在內存查看窗口,輸入 path 變量的地址,此時就看到了準備加載的路徑

我看到準備加載的是非常正常的 System.IO.FileSystem.Watcher.dll 文件,再在網上搜也找不到相關的問題。此時調試再次卡住

而且卡住的地方還是在 KernelBase 裏面

我再次閲讀調用堆棧,似乎明白了,當前主線程的卡住不是主原因。如以下堆棧:

 	ntdll.dll!NtWaitForSingleObject()	未知
 	ntdll.dll!LdrpDrainWorkQueue()	未知
 	ntdll.dll!LdrpLoadDllInternal()	未知
 	ntdll.dll!LdrpLoadDll()	未知
 	ntdll.dll!LdrLoadDll()	未知
 	KernelBase.dll!LoadLibraryExW()	未知
 	coreclr.dll!LoadLibraryExWrapper(const wchar_t * lpLibFileName=0x000000d700000000, void * dwFlags=8, unsigned long) 行 273	C++

可以看到當前的狀態是卡在 LdrpDrainWorkQueue 方法,即當前進程還有另一個 DLL 正在排隊加載。我回憶了剛才遍歷所有進程,似乎看到有可能的問題。我重新退出調試,使用帶託管調試附加進程

打開 調試->窗口->並行堆棧 窗口,作為熟練工,我很快就定位到一個線程正在一個加載 DLL 的堆棧

此時就能看到主線程卡住的核心原因就是因為存在一個線程在加載 DLL 的過程中卡住,如下圖所示。注:下圖是後面截的,導致線程號和後文的不匹配,還請大家略過

切換到對應的線程,查看堆棧,可以看到確實是正在加載 DLL 的過程中

也就是主線程的加載 DLL 卡住是因為正在等待這個線程完成 DLL 加載。那這個線程正在加載啥呢?為什麼需要加載呢?

通過調用堆棧可以看到一個陌生的非託管 DLL 正在加載過程中幹活

    ...
 	user32.dll!UserCallWinProcCheckWow(struct _ACTIVATION_CONTEXT *,__int64 (*)(struct tagWND *,unsigned int,unsigned __int64,__int64),struct HWND__ *,enum _WM_VALUE,unsigned __int64,__int64,void *,int)	未知
 	user32.dll!DispatchClientMessage()	未知
 	user32.dll!__fnDWORD()	未知
 	ntdll.dll!KiUserCallbackDispatcherContinue()	未知
 	win32u.dll!NtUserCreateWindowEx()	未知
 	user32.dll!VerNtUserCreateWindowEx(unsigned long,struct _LARGE_STRING *,struct _LARGE_STRING *,unsigned long,int,int,int,int,struct HWND__ *,struct HMENU__ *,void *,void *,enum ZBID,unsigned long,unsigned long)	未知
 	user32.dll!CreateWindowInternal()	未知
 	user32.dll!CreateWindowExW()	未知
 	YunShellExtV164.dll!00007ffed62555b2()	未知
 	YunShellExtV164.dll!00007ffed628d278()	未知
 	YunShellExtV164.dll!00007ffed6292a69()	未知
 	YunShellExtV164.dll!00007ffed6298520()	未知
 	ntdll.dll!LdrpCallInitRoutineInternal()	未知
 	ntdll.dll!LdrpCallInitRoutine()	未知
 	ntdll.dll!LdrpInitializeNode()	未知
 	ntdll.dll!LdrpInitializeGraphRecurse()	未知
 	ntdll.dll!LdrpPrepareModuleForExecution()	未知
 	ntdll.dll!LdrpLoadDllInternal()	未知
 	ntdll.dll!LdrpLoadDll()	未知
 	ntdll.dll!LdrLoadDll()	未知
 	KernelBase.dll!LoadLibraryExW()	未知
 	combase.dll!00007fff16b53865()	未知
    ...
 	windows.storage.dll!_SHCoCreateInstance(struct _GUID const &,struct IUnknown *,unsigned long,int,enum EXTCOCREATEFLAGS,struct _GUID const &,void * *)	未知
 	windows.storage.dll!SHExtCoCreateInstance()	未知
 	shell32.dll!DCA_SHExtCoCreateInstance()	未知
 	shell32.dll!HDXA_QueryContextMenu(struct _DSA *,struct IDataObject *,unsigned int,struct HKEY__ * * const,class ATL::CComPtr<struct IAssociationElement> * const,struct _ITEMIDLIST_ABSOLUTE const *,struct _QCMINFO *,unsigned int,struct _DCA *,unsigned int * const,struct IUnknown *,struct _DCA *)	未知
 	shell32.dll!CDefFolderMenu::QueryContextMenu(struct HMENU__ *,unsigned int,unsigned int,unsigned int,unsigned int)	未知
 	ContextMenuSafeEnumerator.dll!00007ffe70955c97()	未知
 	[託管到本機的轉換]	
 	RX-Explorer-WAS.dll!RX_Explorer_WAS.Class.Utils.ContextMenuUtil.PrefetchContextMenuData.__PrefetchContextMenuDataCore|5_0(string Path)	未知
 	RX-Explorer-WAS.dll!RX_Explorer_WAS.Class.Utils.ContextMenuUtil.PrefetchContextMenuData()	未知
 	RX-Explorer-WAS.dll!RX_Explorer_WAS.Class.Providers.ThreadDispatcherProvider.ExecuteOnStandAloneThreadAsync.AnonymousMethod__0(System.__Canon Argument, System.Threading.CancellationToken CancelToken)	未知

首要的調查就是卡在哪裏,從 YunShellExtV164.dll 作為入手點,這個 DLL 我在應用程序所在的路徑 C:\Program Files\WindowsApps\36186RuoFan.RXBeta_2.2.2.0_x64__q3e6crc0w375t 找不到,意味着這是被注入進來的或被從其他地方加載的

打開 Visual Studio 的調試->窗口->模塊界面,嘗試搜 YunShellExtV164.dll 找到路徑

通過 C:\Users\lindexi\AppData\Roaming\baidu\BaiduNetdisk\YunShellExtV164.dll 路徑可以看到這是百度雲的 DLL 文件。為什麼會加載它?它又卡在哪?

順着調用堆棧頂部,可以看到是在等鎖。由於這是發生在百度雲的 DLL main 裏面,我就不想繼續調查具體在卡什麼了,只需要知道在卡一個鎖就好

按照最佳實踐,不應該在 DLL main 裏面執行任何長時間的邏輯,或者可能導致卡住的邏輯。如果真要執行,最好是自己開一個線程去執行。這是因為 DLL main 是在 DLL 被加載的時候將被執行的方法,如果在這個方法卡住,那這個進程將無法加載其他的 DLL 了

當前的情況屬於如此,加載到百度雲的 DLL 就卡住了,導致後續主線程加載其他 DLL 就卡住

通過堆棧底部,可以明白是從 RX_Explorer_WAS.Class.Utils.ContextMenuUtil.PrefetchContextMenuData.__PrefetchContextMenuDataCore|5_0(string Path) 調用到 ContextMenuSafeEnumerator.dll 從而加載的百度雲的文件的

詢問開發者才知道,這是嘗試加載所有右鍵菜單的模塊

加載右鍵菜單時,加載到百度雲的右鍵菜單,導致百度雲的模塊被加載。百度雲的模塊在被加載的時候執行的邏輯卡住,導致了主線程無法再加載任何 DLL 文件,導致主線程卡住,從而讓界面顯示黑屏

此時的調試工作就完成了,徹底明白了問題

解決方法是什麼呢?

從用户側來説,我可以禁用百度雲的右鍵菜單,如此也可以提升我的右鍵菜單的性能。我完全不用右鍵菜單的百度雲的功能,其百度雲右鍵菜單的功能如下圖所示

通過 https://www.nirsoft.net/utils/shexview.html 提供的 ShellView 工具幹掉百度雲右鍵菜單

右擊百度雲相關的項,點擊禁用即可

從開發者的角度來説,可以嘗試在進程外加載右鍵菜單,也就是跨進程的方案。這部分我還沒學會,還請大家摸索

回到開始的問題,我一開始通過調用堆棧,認為是靜態構造函數相關的問題導致的。繼續深入調試,發現是加載 DLL 卡住的。通過這個調試經驗,也讓我更明確了按照最佳實踐,不能在加載過程中乾耗時的事情或可能會卡住的事情是多麼重要

感謝 Ruofan 提供了優雅的文件管理器

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.