通過解讀WPF觸控原始碼,分析WPF插拔裝置觸控失效的問題(問題篇)

傑克.陳發表於2018-09-21
原文:通過解讀 WPF 觸控原始碼,分析 WPF 插拔裝置觸控失效的問題(問題篇)
版權宣告:本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名呂毅(包含連結:http://blog.csdn.net/wpwalter/),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯絡(walter.lv@qq.com)。 https://blog.csdn.net/WPwalter/article/details/82119868

在 .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 的更多使用,可以閱讀:


相關文章