天天看點

SWT的UI線程和非UI線程

要了解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線程

  1. 主線程是UI線程
  2. 監聽器方法中是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();
                }
            }
        }
    });
      

程式中模拟在點選按鈕後,執行一段費時的操作,運作可以看到,程式處于無響應狀态:

SWT的UI線程和非UI線程

避免無響應

那麼,如何避免程式進入無響應狀态呢? 其實很簡單,不要在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秒的無響應狀态中, 也相當于新開一個線程執行耗時操作的目的就沒有達到了。

讀者可以試着根據上面的例子,自己寫一個時鐘程式,每秒鐘重新整理顯示目前時間,注意不要讓程式陷入無響應狀态。 可以參考示例代碼。

繼續閱讀