要理解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秒的無響應狀態中, 也相當於新開一個線程執行耗時操作的目的就沒有達到了。