Swing的事件處理過(guò)程為:事件調(diào)度線程(Event Dispatch Thread)從事件隊(duì)列(EventQueue)中獲取底層系統(tǒng)捕獲的原生事件,如鼠標(biāo)、鍵盤、焦點(diǎn)、PAINT事件等。接著調(diào)用該事件源組件的dispachEvent。該方法過(guò)濾出特殊事件后,調(diào)用processEvent進(jìn)行處理。processEvent方法根據(jù)事件類型調(diào)用注冊(cè)在這個(gè)組件上的相應(yīng)事件處理器函數(shù)。事件處理器函數(shù)根據(jù)這些事件的特征,判斷出用戶的期望行為,然后根據(jù)期望行為改變組件的狀態(tài),然后根據(jù)需要刷新組件外觀,觸發(fā)帶有特定語(yǔ)義的高級(jí)事件。此事件繼續(xù)傳播下去,直至調(diào)用應(yīng)用程序注冊(cè)在該組件上的處理器函數(shù)。下圖是這個(gè)過(guò)程的示意圖:
上圖所示意的過(guò)程簡(jiǎn)要說(shuō)就是:
Pump an Event->Dispatch & Process Event->MouseListener.mousePressed->fireActionPerformed->ActionListener.actionPeformed->Do database query and display result to a table->Return from actionPerformed->Return from fireActionPerformed->Return from MouseListener.mousePressed->Pump another Event.
事件調(diào)度線程在應(yīng)用程序事件處理函數(shù)actionPerformed沒(méi)有完成之前是不能處理下一個(gè)事件的,如果應(yīng)用程序處理函數(shù)是一個(gè)時(shí)間復(fù)雜的任務(wù)(比如查詢數(shù)據(jù)庫(kù)并將結(jié)果顯示到表格中),后面包括PAINT事件將在長(zhǎng)時(shí)間內(nèi)得不到執(zhí)行。由于PAINT事件負(fù)責(zé)將界面更新,所以這就使用戶界面失去響應(yīng)。
打一個(gè)比方,事件處理線程就像進(jìn)入某城唯一的單行道一樣,事件相當(dāng)于汽車。有種PAINT汽車負(fù)責(zé)為城市運(yùn)輸非常重要的生活物資。但是有一天,PAINT前面有一輛汽車突然壞掉了,司機(jī)下來(lái)修車。但是這車太難修,一修就是幾天,結(jié)果后面的PAINT汽車無(wú)法前進(jìn),物資無(wú)法按時(shí)運(yùn)到城里。市民急了,市長(zhǎng)雖然不停的打電話催PAINT公司,但即使PAINT公司多添加幾輛車也沒(méi)用。由于進(jìn)城的唯一條路被那輛車給占著,所以再多的PAINT車也只能堵在路上。
不了解Swing的這種事件處理模型的人往往將時(shí)間復(fù)雜的任務(wù)放在處理函數(shù)中完成,這是造成Swing應(yīng)用程序速度很慢的原因。用戶觸發(fā)這個(gè)動(dòng)作,用戶界面就失去了響應(yīng),于是給用戶的感覺(jué)就是Swing太慢了。其實(shí)這個(gè)錯(cuò)誤是程序員造成的,并不是Swing的過(guò)失。
說(shuō)點(diǎn)題外話,所有采用這種事件模型的用戶界面工具都會(huì)產(chǎn)生這種問(wèn)題,包括SWT、GTK、MFC等流行的用戶界面工具。之所以只有Swing被誤解,主要是和Swing的歷史、市場(chǎng)時(shí)機(jī)、商業(yè)宣傳策略和心理學(xué)相關(guān)的。
首先Swing的歷史和市場(chǎng)時(shí)機(jī)極差。Swing出現(xiàn)早期性能也差、錯(cuò)誤也多,而Java程序員脫身于傳統(tǒng)圖形界面工具,對(duì)于Swing這種新的事件處理模型并不太了解,而此時(shí)正處于Java第一輪狂熱的時(shí)期,大家都滿懷希望做了大量的Swing應(yīng)用程序,而這些程序中大量存在這種錯(cuò)誤方法。于是市場(chǎng)上涌現(xiàn)了大批的這種程序。自從那個(gè)時(shí)代,因?yàn)檫@些程序,Swing被貼上了慢的標(biāo)簽。又由于當(dāng)時(shí)的Swing界面也丑,和一般的Windows程序風(fēng)格炯異,更加深人們的這種印象。這種印象一直持續(xù)到現(xiàn)在,像烙印一樣深深的刻在人們的腦海里。
其次,Swing還有一個(gè)致命的問(wèn)題,就是沒(méi)有涌現(xiàn)出一個(gè)具有標(biāo)識(shí)性的好程序,這是造成它比SWT印象慘的原因。為什么SWT采用相同的事件處理模型,而獲得了速度快的聲譽(yù)呢?這是因?yàn)槿藗儺?dāng)時(shí)對(duì)于Java做桌面應(yīng)用的期望心理達(dá)到了低谷,而SWT的出現(xiàn)恰恰是伴隨Eclipse出現(xiàn)的,早期的Eclipse的確是在速度快、界面漂亮,這一掃當(dāng)時(shí)人們認(rèn)為Java慢,Java界面丑陋,Java無(wú)法做桌面應(yīng)用的印象,繼而這個(gè)印象被加在SWT身上,人們認(rèn)為Eclipse速度快、漂亮是因?yàn)镾WT,其實(shí)如果你知道Swing/SWT事件處理模型的話,你就明白功勞是Eclipse開(kāi)發(fā)者的,Eclipse界面漂亮其實(shí)要?dú)w功于Eclipse界面設(shè)計(jì)專家,他們的高水平造就了這個(gè)好的IDE,從而也抬起了SWT的聲譽(yù)。而Swing的名譽(yù)恰恰就被早期Swing低水平開(kāi)發(fā)者給毀了。
再次, 這和商業(yè)宣傳策略有關(guān)。IBM和Eclipse很懂得市場(chǎng)宣傳,人們不是認(rèn)為Java慢嗎,就宣傳SWT使用原生組件,人們不是認(rèn)為Swing丑陋、風(fēng)格炯異吧,就宣傳SWT風(fēng)格一致性,人們不是認(rèn)為Java不能做桌面應(yīng)用嗎,就宣傳基于SWT的Eclipse。其實(shí)這一切的背后原因只是“人”的不同,Eclipse的開(kāi)發(fā)者和Swing應(yīng)用程序的開(kāi)發(fā)者,Swing和SWT技術(shù)差異并沒(méi)有造成那么大的差別,如果是相近能力的人使用他們開(kāi)發(fā)的話,應(yīng)該能做出相近的產(chǎn)品。這可以從現(xiàn)在Eclipse和NetBeans、Intellij IDEA、JDeveloper和JBuilder看的出來(lái)。
最后,人類有一個(gè)心理學(xué)現(xiàn)象,就是一旦形成對(duì)某種事物的印象,很難擺脫舊的認(rèn)識(shí),有時(shí)甚至人們不愿意承認(rèn)擺在眼前的事實(shí)。總而言之,Swing和SWT不同遭遇是因?yàn)闅v史、市場(chǎng)時(shí)機(jī)、商業(yè)宣傳策略、心理學(xué)的種種原因造成的。
那么如何避免這個(gè)問(wèn)題,編寫響應(yīng)速度快的Swing應(yīng)用程序呢?在SwingWorker的javadoc中有這樣兩條原則:
Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the application becomes unresponsive. 耗時(shí)任務(wù)不要放到事件調(diào)度線程上執(zhí)行,否則程序就會(huì)失去響應(yīng)。
Swing components should be accessed on the Event Dispatch Thread only. Swing組件只能在事件調(diào)度線程上訪問(wèn)。
因此處理耗時(shí)任務(wù)時(shí),首先要啟動(dòng)一個(gè)專門線程,將當(dāng)前任務(wù)交給這個(gè)線程處理,而當(dāng)前處理函數(shù)立即返回,繼續(xù)處理后面未決的事件。這就像前面塞車的例子似的,那個(gè)司機(jī)只要簡(jiǎn)單的把車開(kāi)到路邊或者人行道上修理,整個(gè)公路系統(tǒng)就會(huì)恢復(fù)運(yùn)轉(zhuǎn)。
其次,在為耗時(shí)任務(wù)啟動(dòng)的線程訪問(wèn)Swing組件時(shí),要使用SwingUtilties. invokeLater或者SwingUtilities.invokeAndWait來(lái)訪問(wèn),invokeLater和invokeAndWait的參數(shù)都是一個(gè)Runnable對(duì)象,這個(gè)Runnable對(duì)象將被像普通事件處理函數(shù)一樣在事件調(diào)度線程上執(zhí)行。這兩個(gè)函數(shù)的區(qū)別是,invokeLater不阻塞當(dāng)前任務(wù)線程,invokeAndWait阻塞當(dāng)前線程,直到Runnable對(duì)象被執(zhí)行返回才繼續(xù)。在前面塞車的例子中,司機(jī)在路邊修車解決了塞車問(wèn)題,但是他突然想起來(lái)要家里辦些事情,這時(shí)他就可以打個(gè)電話讓家里開(kāi)車來(lái)。假如修車不受這件事情的影響,比如叫家人送他朋友一本書,他可以繼續(xù)修車,這時(shí)就相當(dāng)于invokeLater;假如修車受影響,比如缺少某個(gè)汽車零件,叫家人給他送過(guò)來(lái),那么在家人來(lái)之前,他就沒(méi)法繼續(xù)修車,這時(shí)就相當(dāng)于invokeAndWait。
下面舉一個(gè)例子說(shuō)明這兩點(diǎn),比如按下查詢按鈕,查詢數(shù)據(jù)量很大的數(shù)據(jù)庫(kù),并顯示在一個(gè)表中,這個(gè)過(guò)程需要給用戶一個(gè)進(jìn)度提示,并且能動(dòng)態(tài)顯示表格數(shù)據(jù)動(dòng)態(tài)增加的過(guò)程。假設(shè)按鈕的處理函數(shù)是myButton_actionPerformed,則:
void myButton_actionPerformed(ActionEvent evt){
new MyQueryTask().start();
}
public class MyQueryTask extends Thread{
public void run(){
//查詢數(shù)據(jù)庫(kù)
final ResultSet result=...;
/ /顯示記錄
for(;result.next();){
//往表的Model中添加一行數(shù)據(jù),并更新進(jìn)度條,注意這都是訪問(wèn)組件
SwingUtilities.invokeLater(new Runnable(){
public void run(){
addRecord(result);
}
});
}
....
}
void addRecord(ResultSet result){
//往表格中添加數(shù)據(jù)
jTable.add....
//更新進(jìn)度條
jProgress.setValue(....);
}
}
JDK1.6以后,Swing提供了一個(gè)專門的類SwingWorker能幫你解決這個(gè)編程范式,你所需要做的就是繼承這個(gè)類,重載doInBackground,然后在actionPeformed中調(diào)用它的execute方法,并通過(guò)publish/process方法來(lái)更新界面。SwingWorker的主要方法和它們的作用在下面的示意圖:

從上面示意圖可以看出,SwingWorker實(shí)際上不過(guò)是封裝了前面我所說(shuō)的例子中的MyQueryTask,并做了更詳盡的考慮。execute方法相當(dāng)于MyQueryTask線程start,它啟動(dòng)這個(gè)后臺(tái)線程并立刻返回。SwingWorker可以注冊(cè)PropertyChangeListener,這些listener都被在事件調(diào)度線程上執(zhí)行,相當(dāng)于MyQueryTask中的那些訪問(wèn)組件的Runnable對(duì)象。另外,publish、setProgress只不過(guò)是特殊的property事件吧,process和done不過(guò)是響應(yīng)publish和PropertyChangeEvent.DONE這個(gè)事件的方法罷了。因此我們很容易將上面的例子改成SwingWorker的版本:
void myButton_actionPerformed(ActionEvent evt){
new MyQueryTask().execute();
}
public class MyQueryTask extends SwingWorker{
public void doInBackground(){
//查詢數(shù)據(jù)庫(kù)
final ResultSet result=...;
//顯示記錄
for(;result.next();){
//往表的Model中添加一行數(shù)據(jù),并更新進(jìn)度條,注意這都是訪問(wèn)組件
publish(result);
}
....
}
public void process(Object ... result){
//往表格中添加數(shù)據(jù)
jTable.add....
//更新進(jìn)度條
jProgress.setValue(....);
}
}
對(duì)于一般的耗時(shí)任務(wù)這樣做是比較普遍的,但是有一些任務(wù)是一旦觸發(fā)之后,會(huì)周期性的觸發(fā),如何做處理這種任務(wù)呢?JDK中提供了兩個(gè)Timer類幫你完成定時(shí)任務(wù),一個(gè)是javax.swing.Timer,一個(gè)java.util.Timer。使用它們的方法很簡(jiǎn)單,對(duì)于Swing的timer,使用方法如下:
public void myActionPerformed(){
//假設(shè)點(diǎn)擊了某個(gè)按鈕開(kāi)始記時(shí)
Action myAction=new AbstractAction(){
public void actionPerformed(ActionEvent e){
//做周期性的活動(dòng),比如顯示當(dāng)前時(shí)間
Date date=new Date();
jMyDate.setDate(date);//jMyDate是個(gè)假想的組件,能顯示日期時(shí)間
}
};
new Timer(1000, myAction).start();
}
java.util.Timer類似,只不過(guò)使用TimerTask完成動(dòng)作封裝。注意這兩個(gè)Timer有一個(gè)關(guān)鍵的區(qū)別:Swing的Timer的事件處理都是在事件調(diào)度線程上進(jìn)行的,因而它里面的操作可以直接訪問(wèn)Swing組件。而java.util.Timer則可能在其他線程上,因而訪問(wèn)組件時(shí)要使用SwingUtilities.invokeLater和invokeAndWait來(lái)進(jìn)行。這一點(diǎn)要記住。
如果要了解更詳細(xì)的信息,可以查閱SwingWorker、Swing Timer和util Timer這些類javadoc文檔和其他網(wǎng)上資料。最重要的是要記住了那兩條原則。
轉(zhuǎn)自:WilliamChen