1. 程式人生 > >通過解讀 WPF 觸摸源碼,分析 WPF 插拔設備觸摸失效的問題(問題篇)

通過解讀 WPF 觸摸源碼,分析 WPF 插拔設備觸摸失效的問題(問題篇)

frame alter nth 閱讀 mpi release 和源 得出 mov

原文:通過解讀 WPF 觸摸源碼,分析 WPF 插拔設備觸摸失效的問題(問題篇)

版權聲明:本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名呂毅(包含鏈接:http://blog.csdn.net/wpwalter/),不得用於商業目的,基於本文修改後的作品務必以相同的許可發布。如有任何疑問,請與我聯系([email protected])。 https://blog.csdn.net/WPwalter/article/details/82119868

在 .NET Framework 4.7 以前,WPF 程序的觸摸處理是基於操作系統組件但又自成一套的,這其實也為其各種各樣的觸摸失效問題埋下了伏筆。再加上它出現得比較早,觸摸失效問題也變得更加難以解決。即便是 .NET Framework 4.7 以後也需要開發者手動開啟 Pointer 消息,並且存在兼容性問題。

本文將通過解讀 WPF 觸摸部分的源碼,分析 WPF 插拔設備觸摸失效的問題。隨後,會給微軟報這個 Bug。


本文使用多種語言編寫,請選擇適合你閱讀的語言:

  • 通過解讀 WPF 觸摸源碼,分析 WPF 插拔設備觸摸失效的問題(問題篇)
  • WPF Applications Stop Responding to Touches after Adding or Removing Tablet Devices

所謂“觸摸失效”,指的是無論你如何使用手指或觸摸筆在觸摸屏上書寫、交互,程序都沒有任何反應。而使用鼠標操作則能正常使用。

  • 本文所述的“觸摸失效問題”我在 WPF 程序無法觸摸操作 一文中有所提及,但本文偏向於分析其內部發生的原因。

  • 本文與 林德熙 的 WPF 插拔觸摸設備觸摸失效 所述的是同一個問題。那篇文章會更多的偏向於源碼解讀,而本文更多地偏向於分析觸摸失效的過程。


WPF 程序插拔設備導致觸摸失效問題

無論你寫的 WPF 程序多麽簡單,哪怕只有一個最簡單的窗口帶著一個可以交互的按鈕,本文所述的觸摸失效問題你都可能遇到。

具體需要的條件為:

  1. 運行 任意的 WPF 程序
  2. 插拔帶有觸摸的 HID 設備(可以是物理插拔,也可以是驅動或軟件層面的插拔)

以上雖說是必要條件,但如果要提高觸摸失效的復現概率,需要制造一個較高的 CPU 占用:

  • 當前系統中有 較高的 CPU 占用率

可能還有一些尚不確定的條件:

  • 是否對 .NET Framework 的版本有要求?
  • 是否對 Windows 操作系統的版本有要求?

將以上所有條件組合起來,對於觸摸失效的問題描述為:

  • 當運行任意的 WPF 程序時,如果此時操作系統有較高的 CPU 占用,並且此時存在帶有觸摸的 HID 設備插拔,那麽此 WPF 程序可能出現“觸摸失效”問題,即此後此程序再也無法觸摸操作了。
  • 如果此時系統中同時運行了多個 WPF 程序,多個 WPF 程序可能都會在此時出現觸摸失效問題。

觸摸失效原因初步分析

WPF 從收集設備觸摸到大多數開發者所熟知的 StylusMouse 事件需要兩個不同的線程完成。

  1. 主線程,負責進行 Windows 消息循環
  2. StylusInput 線程,負責從 WPF 非托管代碼和 COM 組件中獲得觸摸信息

主線程中的 Windows 消息循環處理這些消息:

  • LBUTTONDOWN, LBUTTONUP
  • DEVICECHANGE, TABLETADDED, TABLETREMOVED

Stylus Input 線程主要由 PenThreadWorker 類創建,在線程循環中使用 GetPenEventGetPenEventMultiple 這兩個函數來獲取整個觸摸設備中的觸摸事件,並將觸摸的原始信息向 WPF 的其他觸摸處理模塊傳遞。傳遞的其中一個模塊是 WorkerOperationGetTabletsInfo 類,其的 OnDoWork 方法中會通過 COM 組件獲取觸摸設備個數。

而導致觸摸失效的錯誤代碼就發生在以上 Stylus Input 線程的處理中。

  1. PenThreadWorkerGetPenEventMultiple 方法傳入的 _handles 為空數組,這會導致進行無限的等待。
  2. WorkerOperationGetTabletsInfoOnDoWork 因為 COM 組件錯誤出現 COMException 或因為線程安全問題出現 ArgumentException;此時方法內部會 catch 然後返回空數組,這使得即時存在觸摸設備也會因此而識別為不存在。

為了方便理解以上的兩個 Bug,可以看看我簡化後的 .NET Framework 源碼:

// PenThreadWorker.ThreadProc
while(這裏是兩層循環,簡化成一個以便理解)
{
    // 以下的 break 都只退出一層循環而已。
    if (this._handles.Length == 1)
    {
        if (!GetPenEvent(this._handles[0], 其他參數))
        {
            break;
        }
    }
    else if (!GetPenEventMultiple(this._handles, 其他參數))
    {
        break;
    }
    // 後續邏輯。
}
// WorkerOperationGetTabletsInfo.OnDoWork
try
{
    _tabletDeviceInfo = PenThreadWorker.GetTabletInfoHelper(pimcTablet);
}
catch(COMException)
{
    _tabletDevicesInfo = new TabletDeviceInfo[0];
}
catch(ArgumentException)
{
    _tabletDevicesInfo = new TabletDeviceInfo[0];
}
// 其他異常。

以上的問題分析中,ArgumentException 異常幾乎可以肯定是線程安全問題所致;COMException 不能確定;而 GetPenEventMultiple 中的參數 handles 實際上是用來進行非托管和托管代碼線程同步用的 ResetEvent 集合,所以實際上也是線程同步問題導致的死鎖。

同時聯系以上必要復現步驟中,如果當前存在高 CPU 占用則可以大大提高復現概率;我們幾乎可以推斷,此問題是 WPF 對觸摸的處理存在線程安全的隱患所致。

此觸摸失效問題的解決方法

在推斷出初步原因後,根本的解決方法其實只剩下兩個了:

  1. 修復 WPF 的 Bug
    • 由於我們無法編譯 .NET Framework 的源碼,所以幾乎只能由微軟來修復這個 Bug,即需要新版本的 WPF 來解決這個線程安全隱患
    • 當然,此問題的修復可以跟隨 .NET Framework 更新,也可以跟隨即將推出的 .NET Core 3 進行更新。
  2. 更新 Windows(傳說中的補丁)
    • 新的 Windows 提供給 WPF 的 COM 組件可能也需要修復線程安全或其他與觸摸硬件相關的問題

比較徹底的方案是以上兩者都需要修復,但都 只能由微軟來完成

那我們非微軟開發者可以做些什麽呢?

  1. 降低 CPU 占用率
    • 雖然這不由我們控制,不過我們如果能降低一些意料之外的高 CPU 占用,則可以大幅降低 WPF 觸摸失效問題出現的概率。

然而作為用戶又可以做些什麽呢?

  1. 重新插拔觸摸設備(如果你的觸摸框是通過 USB 連接可以手工插拔的話)

觸摸失效問題的分析過程

以上結論的得出,離不開對 .NET Framework 源碼的解讀和調試。

由於 WPF 的觸摸原理涉及到較多類型和源碼,需要大量篇幅描述,所以不在本文中說明。閱讀以下文章可以更加深入地了解這個觸摸失效的問題:

  • WPF 插拔觸摸設備觸摸失效 - lindexi
  • 通過解讀 WPF 觸摸源碼,分析 WPF 插拔設備觸摸失效的問題(分析篇) - walterlv

本文所有的 .NET Framework 源碼均由 dnSpy 反編譯得出,分析過程也基本是借助 dnSpy 的無 pdb 調試特性進行。關於 dnSpy 的更多使用,可以閱讀:

  • 斷點調試 Windows 源代碼 - lindexi
  • 神器如 dnSpy,無需源碼也能修改 .NET 程序 - walterlv

通過解讀 WPF 觸摸源碼,分析 WPF 插拔設備觸摸失效的問題(問題篇)