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

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

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


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

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


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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章