如何改善Swing程式的響應性
在上一篇文章中,曾經說過不能在EDT中執行耗時的任務,主要的原因是因為當EDT在執行這些耗時任務的時候,不能及時更新UI界面,這個時候整個界面是處于“卡住”狀态的,不能接受任何事件(如鍵盤輸入等)和描繪界面,容易給使用者一種程式“死掉”的感覺,非常不友好,比如下面的代碼是不受歡迎的。
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
writeHugeData(); // 執行耗時的寫檔案任務
jLabel.setText("Writting data..."); // 試圖立刻改變 jLabel 的内容
}
});
這個時候我們應該很容易地想到使用一個新線程來處理writeHugeData()任務,即把耗時的任務從EDT中剝離出來,這個時候代碼變成
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// 啟動一個新線程來處理耗時工作
new Thread(new Runnable() {
public void run() {
writeHugeData();
jLabel.setText("Writting data...");
}
}).start();
}
});
上面的代碼應該是很多人經常寫的,但是 注意這裡它違背了前面提到的一個注意事項:更新界面的操作必須由EDT中去完成 。也即上面修改jLabel狀态的代碼不能由建立的線程完成,否則程式随時存在着死鎖的危險。然後,我們可以将上面的代碼改成
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
new Thread(new Runnable() {
public void run() {
writeHugeData();
// 将更新界面的代碼放到事件隊列中
SwingUtilities.invokeLater(new Runnable() {
public void run() {
jLabel.setText("Writting data...");
}
});
}
}).start();
}
});
這個時候已經能解決所有問題了,但是如果這樣做的話每次更新界面都需要建立一個Runnable的執行個體,對程式的性能有點浪費。其實最佳的方法應該是讓一個線程做好準備,運作和等待完成任務,這個線程我們稱其為“任務線程”。EDT将負責搜集事件,使用同步化将其傳遞給等待的任務線程,并通過等待/通知機制來告訴任務線程有新的要求。當任務線程完成了請求後,它将通過invokeLater()方法将更新界面的代碼交給EDT去處理,然後任務線程将傳回并繼續等待下一個通知。也就是在這樣的背景下,SwingWorker類應運而生。
關于SwingWorke類
SwingWorker類是在JavaSE6中才出現的,它的目的是為了簡化程式員開發任務線程的工作,SwingWorker可以與各種UI元件在多線程的環境下互動,而不用程式員去過多關注。一般使用SwingWorker的做法是建立一個SwingWorker的子類,然後重寫其doInBackground()、done()和process()方法來實作我們需要完成的功能。上面的代碼可以繼續修改為
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
new SwingWorker<Long, Void>() {
protected Long doInBackground() {
// 執行耗時的寫檔案任務
return writeHugeData();
}
protected void done() {
try {
jLabel.setText("Writting data...");
} catch (Exception e) {
e.printStackTrace();
}
}
}.execute();
}
});
測試程式
下面通過一個程式來測試下讓EDT去執行耗時任務所導緻的後果,如下
/*
* SynSwingDemo.java
*
* Created on 2009/11/27
*/
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.util.concurrent.ExecutionException;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
/**
* 這是一個展示編寫 Swing 程式時因為在 EDT 中執行了長時間的時間而導緻
* 界面無法及時更新的例子,例子通過對比來增強感受
* @author zhouych
* @since JDK 1.6
*/
public class SynSwingDemo extends JFrame {
private JButton button;
private JLabel jLabel;
private JCheckBox checkBox;
public SynSwingDemo() {
super("EDT阻塞");
initComponents();
setSize(500, 200);
setLayout(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
private void initComponents() {
jLabel = new JLabel("顯示資訊");
jLabel.setBounds(10, 10, 300, 25);
this.add(jLabel);
checkBox = new JCheckBox("是否讓 EDT 阻塞");
checkBox.setBounds(10, 50, 200, 25);
this.add(checkBox);
button = new JButton("點選執行長時間事件");
button.setBounds(10, 90, 200, 25);
this.add(button);
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
jLabel.setText("程式正在寫檔案...");
if (checkBox.isSelected()) {
// 如果讓 EDT 阻塞
// 這是一個非常不好的程式設計習慣
long time = writeHugeData();
jLabel.setText("耗費時間為: " + time);
} else {
// 如果 EDT 不阻塞,則使用 SwingWorker 來提高程式響應性
// 我們應該提倡這種做法
new SwingWorker<Long, Void>() {
@Override
protected Long doInBackground() {
return writeHugeData();
}
@Override
protected void done() {
try {
jLabel.setText("耗費時間為: " + get());
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ExecutionException e2) {
e2.printStackTrace();
}
}
}.execute();
}
}
});
}
/**
* 一個寫巨大資料量的方法,需要長時間執行
*/
public long writeHugeData() {
try {
long startTime = System.currentTimeMillis();
FileOutputStream fos = new FileOutputStream("file.dat");
DataOutputStream dos = new DataOutputStream(fos);
// 寫入資料
for (int i = 0; i < 2000000; i++) {
dos.writeDouble(Math.random());
}
dos.flush();
dos.close();
fos.close();
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
return time;
} catch (Exception e) {
e.printStackTrace();
}
return 0L;
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
SynSwingDemo frame = new SynSwingDemo();
frame.setVisible(true);
}
});
}
}
在運作上述程式的時候,請注意:
1)請對比當點選按鈕後jLabel的内容是否立刻改變;
2)請對比在寫入資料過程中checkBox是否能改變值;
3)請對比在寫入資料過程中的時候改變視窗的大小;
4)……
通過上面的測試,大家應該都已經初步了解到如何改善自己的Swing程式的互動性了,SwingWorker的作用非常大,在SUN的官方文獻裡有篇很不錯的文章《 Improve Application Performance With SwingWorker in Java SE 6 》 ,并且William Chen已經将其翻譯了出來,有興趣的可以看下。