Android 應用程式要與 USB 裝置通訊,取得權限之後還要 claim 該裝置的介面。claim 成功的語意是獨佔:從那一刻起這個介面歸這支程式使用,作業系統原本掛在同一個介面上的功能停止運作。

這條語意在觸控一體機上有一個具體的後果——螢幕的觸控面板本身就是一個 USB HID 裝置。應用程式 claim 到它,螢幕就不再回應觸碰,而應用程式本身完全正常,沒有例外也沒有閃退。

「當掉」是一個症狀詞,指向四種不同的機制

使用者能觀察到的只有「畫面停住、點不動」,而這個描述涵蓋的機制各自需要不同的診斷方向。下面這張表是事後歸納的通則,不是某一次排查實際走過的路徑——列出來的用途是讓下一次的第一個問句對得上,而不是重述一段經過:

使用者看到的底下發生的事可查證的痕跡
畫面消失、回到桌面未捕捉的例外導致行程結束logcat 有 stack trace
畫面停住、系統跳出關閉對話框主執行緒被阻塞超過門檻(ANR)ANR trace 檔、logcat 有 ANR 紀錄
畫面停住、轉圈持續等待某個逾時尚未到期該次呼叫的起始 log 有、結束 log 無
畫面停住而畫面本身仍在更新輸入通道被接管,繪製與邏輯都正常三種痕跡都沒有

最後一種的鑑別特徵是三種痕跡都沒有。這一種在只看 log 的排查裡形成一個空集合,而空集合容易被讀成「還沒找到線索」——空集合本身就是線索:程式沒有壞,壞掉的是輸入到程式的路徑。

分辨這一種的成本很低——請現場的人回報畫面上的時鐘或動畫有沒有繼續走。繼續走代表繪製與主執行緒都活著,剩下的可能就只有輸入通道。機器不在手邊時同一個判別也做得到:間隔幾秒連抓兩張畫面比對,或錄一段短影片。取畫面的指令與它的陷阱見 adb 遠端診斷 Android 實機,那裡給的是二進位輸出為什麼要走特定通道、以及多顯示器機器上編號取錯會得到什麼。

連線成功驗證的是連線動作,不是連線對象

USB 權限的授權對象是裝置,判斷依據是使用者同不同意,而不是這個裝置適不適合當前用途。觸控面板是一個合法的 USB 裝置,權限請求會正常彈出、使用者會正常按下允許、claim 會正常成功。

這件事推翻一種把關寫法:連線成功才儲存設定。那個寫法的隱含前提是「連得上的裝置就是對的裝置」,而權限的成功條件與 claim 的成功條件各自都只看裝置合不合法,所以它對選錯裝置這一類問題完全無效——它驗的是連線這個動作成不成立,不是連線對象對不對。

真正有效的把關位置在更前面:列出候選裝置的那一刻。候選清單該用什麼條件篩,換掉的是「誰承受篩錯的代價」這一項,而白名單與黑名單的選擇由這一項決定——那一篇比較兩種篩法各自錯的時候誰承擔、補不補得回來。掃描結果送到使用者面前之前就把不該出現的裝置濾掉,使用者就沒有選錯的機會。把關放在連線之後,等於讓使用者先做出選擇、再由程式去驗一個驗不出對象正確性的條件。

獨佔的代價要在設計時就估

claim 的獨佔語意帶來一個一般性的判斷點:這支程式接管的硬體,有沒有其他人正在用它。

在單一用途的外接裝置上,答案通常是沒有,獨佔沒有代價。在一體機上,答案經常是有——觸控面板、內建讀卡機、內部集線器底下的其他裝置都可能是系統功能的一部分,而接管它們的代價由使用者承擔,不由程式承擔(程式自己一切正常)。

判斷標準因此是claim 成功之後,誰失去了什麼,而不是 claim 會不會失敗。這個問題在寫程式的機器上問不出答案——開發機的螢幕通常不是 USB 裝置,那台機器上沒有這個衝突可以觀察。問題要在目標硬體上問。觸控螢幕有專門的查法(adb shell dumpsys input 直接回答哪個裝置是它),而讀卡機、內部集線器底下的其他裝置沒有對應的單一指令——它們要靠拓樸盤點才認得出歸屬,做法見陌生機器的硬體盤點的 sysfs 那一段。

判斷標準

「畫面凍結」這個症狀的四種機制各自在別的地方留下痕跡,而輸入通道被接管的那一種在每一個地方都不留痕跡。這使痕跡的缺席本身成為判別依據:例外留 stack trace、主執行緒阻塞留 ANR 紀錄、逾時留一個有起始無結束的呼叫,而輸入通道被接管的程式一切正常,所以三種紀錄同時空白、畫面又還在更新時,剩下的可能只有一種。

設計會 claim 硬體的功能時,把關放在列出候選的階段,不放在連線之後——連線成功這個訊號不承載「對象正確」這個資訊。而接管硬體之前要先確認這塊硬體現在歸誰用,在目標機器上確認、不在開發機上推論。