要了解UI線程,先要了解一下“消息循環”這個概念。連結是百度百科上的條目,簡單地說,作業系統把使用者界面上的每個操作都轉化成為對應的消息,加入消息隊列。然後把消息轉發給對應的應用程式(一般來說,就是活動視窗),應用程式根據自己的邏輯處理這些消息。 如果應用程式處理某個消息事件的時候,用了很長的時間,這時候後續的消息無法及時得到處理,就會造成應用程式沒有響應,也就是常說的“假死”狀态。 是以,應用程式如果處理某個事件需要較長的時間,需要把這個操作放到一個另外的線程中進行處理。 下面再回顧一下前面的簡單的SWT程式的結構:
public static void main(String[] args) {
Display display = new Display ();
Shell shell = new Shell (display);
......
shell.open ();
while (!shell.isDisposed ()) {
if (!display.readAndDispatch ()) display.sleep ();
}
display.dispose ();
}
while循環一段就是處理消息循環的開始,也就是說,一個SWT程式的主線程,就是對應的所謂的UI線程。
程式中什麼地方是UI線程什麼地方是非UI線程
- 主線程是UI線程
- 監聽器方法中是UI線程 比如下面這段小程式:
Label label = new Label (shell, SWT.NONE);
label.setText ("Enter your name:");
Text text = new Text (shell, SWT.BORDER);
text.setLayoutData (new RowData (100, SWT.DEFAULT));
Button ok = new Button (shell, SWT.PUSH);
ok.setText ("OK");
ok.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
while(true) {
System.out.println(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
});
程式中模拟在點選按鈕後,執行一段費時的操作,運作可以看到,程式處于無響應狀态:
避免無響應
那麼,如何避免程式進入無響應狀态呢? 其實很簡單,不要在UI線程中執行長時間的操作,如果必需要執行費時操作,就在另外的線程中執行:
ok.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
new Thread() {
public void run() {
while(true) {
System.out.println(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}.start();
}
});
這樣再次執行,可以看到點選按鈕後,程式不再會進入無響應狀态。
非UI線程通路UI
是以對控件的操作都必需在UI線程中進行,否則會抛出線程通路錯誤,還是以上面代碼為例,我們現在改成列印text控件的文本:
ok.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
// 此處代碼直接在監聽器方法中,是UI線程
new Thread() {
public void run() {
// 此處為另外一個單獨線程,非UI線程
while(true) {
System.out.println(text.getText());
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}.start();
}
});
運作程式,點選按鈕,就會抛出下面的異常:
Exception in thread "Thread-0" org.eclipse.swt.SWTException: Invalid thread access
at org.eclipse.swt.SWT.error(SWT.java:4441)
at org.eclipse.swt.SWT.error(SWT.java:4356)
at org.eclipse.swt.SWT.error(SWT.java:4327)
at org.eclipse.swt.widgets.Widget.error(Widget.java:476)
at org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:367)
at org.eclipse.swt.widgets.Text.getText(Text.java:1350)
at test.Snippet108$1$1.run(Snippet108.java:24)
對于這種在非UI線程通路UI的情況,需要用Display類的syncExec(Runnable)或asyncExec(Runnable)兩個方法來執行:
ok.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
// 此處代碼直接在監聽器方法中,是UI線程
new Thread() {
public void run() {
// 此處為另外一個單獨線程,非UI線程
while(true) {
// 非UI線程通路UI
display.syncExec(new Runnable() {
@Override
public void run() {
// 這段代碼實際上會被放在UI線程中執行
System.out.println(text.getText());
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}.start();
}
});
注意上面的注釋說明,syncExec(runnable)方法的參數runnable對象實際上是會被放在UI線程中執行的,是以要注意,不要把Tread.sleep()放到這個runnable裡,否則同樣會導緻界面無響應。
syncExec和asyncExec方法的差別就是這兩個方法一個會等待runnable執行完才傳回,asyncExec方法則是立即傳回,UI線程會在有空閑的時候去執行runnable。
那麼,是否能夠用asyncExec方法執行,同時把上面的sleep放到runnable中呢? 答案依然是不能,原因前面已經提到了,因為runnable實際上是會放到UI線程中執行的,如果這個runnable是非常耗時的,同樣會讓界面不斷陷入每次1秒的無響應狀态中, 也相當于新開一個線程執行耗時操作的目的就沒有達到了。
讀者可以試着根據上面的例子,自己寫一個時鐘程式,每秒鐘重新整理顯示目前時間,注意不要讓程式陷入無響應狀态。 可以參考示例代碼。