為了了解原因,您可以設(shè)想應(yīng)用程序上有一個按鈕,單擊按鈕時要做一些工作, 于是您對它添加了一個事件處理程序。 當(dāng)用戶單擊該按鈕時,OS 調(diào)用 GUI 工具包,轉(zhuǎn)而調(diào)用 您定義的事件處理函數(shù)。事件的處理代碼現(xiàn)在運行于 UI 線程, 而且只要這段代碼在運行,UI 線程無法對其他 UI 事件作出響應(yīng)。 這意味著 UI 看上去凍結(jié)了,用戶將對此情況感到不安。 問題的重點在于如果 UI 線程在運行您的代碼,應(yīng)用程序?qū)o法 再處理來自 OS 的 UI 事件。 如果應(yīng)用程序有一個按鈕用于取消某個長時間運行的操作,而您正在使用 UI 線程 執(zhí)行操作,那么這個取消事件只有到 UI 線程所做的操作完成后才會被處理! (如果代碼在 UI 線程中運行過久,OS 會提醒用戶,為用戶提供選項以 終止該應(yīng)用程序。)
上述情形說明了為什么 UI 線程上的緩慢和無法預(yù)期的 I/O 會造成問題。 每種類型的 I/O 都可能擁有極為不同的特性。磁盤 I/O 一般遵從線形 模型反應(yīng)時間(latency)+ 傳輸速率 * 數(shù)據(jù)量。 另一方面,網(wǎng)絡(luò) I/O 則沒有這么規(guī)則。它不僅比磁盤 I/O 慢,而且在可靠性上也遠不如磁盤 I/O, 這是因為它會受到(可能是暫時性地)網(wǎng)絡(luò)端點間擁塞的影響。
由于在開發(fā)時網(wǎng)絡(luò)可能很快而且延遲很小,所以很容易忽視 網(wǎng)絡(luò) I/O 在 UI 線程上造成的影響。在開發(fā)環(huán)境下,容易無意識地在 UI 線程上 做網(wǎng)絡(luò)調(diào)用,直到在較慢或穩(wěn)定性較差的網(wǎng)絡(luò)上運行的用戶注意到每次進入網(wǎng)絡(luò) UI 線程都會凍結(jié)時,才發(fā)現(xiàn)這個問題。 再加上套接字超時,應(yīng)用程序如果五秒鐘內(nèi)沒有響應(yīng) UI 事件,則 Windows 經(jīng)常出現(xiàn) “死亡白屏” 的情況。
表 1 介紹了一些用于發(fā)現(xiàn) UI 線程上長時間運行的操作的技術(shù),以及它們的優(yōu)缺點:
技術(shù) | 優(yōu)點 | 缺點 |
---|---|---|
使用分析器 | 如果您有一個分析器,那么設(shè)置它并不麻煩。 |
一般要花錢。 耗費的運行時間可能非常高。 |
記錄 JDK |
設(shè)置好后可以和應(yīng)用程序很好地合作,直到升級該 JDK。 耗費的運行時間很少。 |
不容易和他人共享。 |
記錄代碼 | 很多用戶可以共享,因為啟用記錄后,客戶、QA、開發(fā)人員和其他人都能運行。 |
可能要求您改變應(yīng)用程序的架構(gòu)以發(fā)現(xiàn)所有做了網(wǎng)絡(luò)調(diào)用的位置。 必須注意不要添加未被記錄的新方法。 需要記錄處理日志,日志文件會變得很大。 |
記錄技術(shù)
您可以用很多技術(shù)來洞察應(yīng)用程序所做的事情。 本節(jié)介紹其中一些技術(shù)。
使用方面(aspect)
您可以用面向方面(aspect-oriented)技術(shù)將變化 “編織” 到被記錄的類中。 舉例而言,可以直接將代碼組合到 SocketInputStream 和 SocketOutputStream 檢查流是否被 UI 線程訪問 (請參閱 參考資料 上關(guān)于面向方面技術(shù)和工具的更多信息的鏈接。)
Swing 與 SWT 的對比
Swing 和 SWT 的不同之處在于對 UI 線程的命名方式。在 SWT 中,UI 線程往往 命名為 main。在 Swing 中,您使用 java.awt.EventQueue.isDispatchThread() 詢問 當(dāng)前線程是否是分發(fā)線程。本文后面的例子按照 SWT 方法;如果您用的是 Swing,做對應(yīng)的替換即可。
使用斷點
如果能在調(diào)試器里運行您的應(yīng)用程序,有些時候用條件斷點記錄 JDK 更加簡單。 我曾參與過一個大型應(yīng)用程序,它在 UI 線程上進行網(wǎng)絡(luò)調(diào)用。該應(yīng)用程序的結(jié)構(gòu) (大量第三方代碼)使得分辨誰來負責(zé)網(wǎng)絡(luò)調(diào)用很困難,而在 Eclipse 中的 SocketInputStream 類設(shè)置條件斷點(如 圖 4 所示), 則很容易分辨出來:
圖 4. 條件斷點
使用安全管理器
另外,我曾成功地使用一個記錄式安全管理器替換應(yīng)用程序的安全管理器。 大量有趣的調(diào)用通過安全管理器傳遞。 比如,清單 1 中的安全管理器 記錄了一條消息,它試圖在 UI 線程中打開一個套接字:
清單 1. 記錄在 UI 線程中打開套接字時的錯誤
SecurityManager securityManager = new SecurityManager() {
public void checkPermission(Permission perm) {
if(perm instanceof java.net.SocketPermission) {
if(Thread.currentThread().getName().equals("main&")) {
logger.log(Level.SEVERE, "Network call on UI thread&");
new Error().printStackTrace();
}
}
}
};
System.setSecurityManager(securityManager);
記錄代碼
如果您的應(yīng)用程序分層很好,網(wǎng)絡(luò)調(diào)用只經(jīng)過一個(或很少)位置, 則能夠在進行網(wǎng)絡(luò)調(diào)用之前用應(yīng)用程序代碼檢查當(dāng)前線程, 如 清單 2 中所示。 在構(gòu)建產(chǎn)品時我會保留此類代碼,因為線程檢查很開銷較低。 創(chuàng)建并記錄異常日志會導(dǎo)致一些時間開銷,但是堆棧跟蹤可以很好地用于 捕獲問題原因。
清單 2. 記錄在 UI 線程中做網(wǎng)絡(luò)調(diào)用時的錯誤
if(Thread.currentThread().getName().equals("main")) {
logger.log(Level.SEVERE, "Network call on UI thread");
new Error().printStackTrace();
}
修改 JDK 的類
作為后一種手段,您可以通過修改 JDK 的類達到記錄 JDK 的目的。 這種手段不受支持、復(fù)雜并且有黑客嫌疑 —— 而且可能侵犯許可 —— 但是對于 某些不常見的情形,當(dāng)前述技術(shù)無能為力時,它還是一個有價值的選擇。 這種技術(shù)的要點是重新編譯 JDK 的類,并使用 -Xbootclasspath/p: 預(yù)置 JAR 或目錄到您的啟動類路徑中。
避免 UI 線程中的長時間運行動作
有一些技術(shù)用來避免 UI 線程中的長時間運行動作, 舉一個常見的例子:使用某種數(shù)據(jù)庫查詢、網(wǎng)絡(luò)調(diào)用或磁盤進行填充的表或樹。
好
不要指望您能在 UI 線程中填充該表。 也許可以處理幾百個項目,但是上千個項目則處理不了。
更好
不要指望在顯示給用戶初始結(jié)果之前完全填充該表或樹。舉例而言, 如果您正在開發(fā)一個電子郵件客戶機,您定不希望先載入所有文件夾下的所有郵件消息并生成表, 然后再給用戶顯示一個滿是郵件消息的 “頁面”。
還有更好
充分利用 SWT/JFace 的虛擬部件。您可以使用幾種不同的技術(shù), 但是所有技術(shù)都歸結(jié)于 “盡可能地延遲工作。” 在 UI 線程里,用占位符填充樹或表,在后臺的 Job 中, 檢索真實數(shù)據(jù)并在獲得數(shù)據(jù)后更新樹。
好
注意您在事件處理程序中做了多少工作。特別是注意捆綁到表、樹和列表的 SWT 選擇處理程序。 我看到過很多有此類錯誤的代碼。比如,清單 3 中是來自一個 郵件應(yīng)用程序的選擇偵聽器;每當(dāng)選中一條消息,執(zhí)行一個數(shù)據(jù)庫查詢以讀取郵件詳情并用它更新 UI:
清單 3. 對每個選擇改變做響應(yīng)的選擇偵聽器