在做GUI的時(shí)候, 無(wú)論是SWT, AWT, Swing 還是Android, 都需要面對(duì)UI線程的問(wèn)題, UI線程往往會(huì)被單獨(dú)的提出來(lái)單獨(dú)對(duì)待, 試著問(wèn)自己, 當(dāng)GUI啟動(dòng)的時(shí)候, 后臺(tái)會(huì)運(yùn)行幾個(gè)線程? 比如
1. SWT 從Main函數(shù)啟動(dòng)
2. Swing 從Main函數(shù)啟動(dòng)
3. Android 界面啟動(dòng)
常常我們被告知, 主線程, UI線程, 因此這里很多會(huì)回答, 有兩個(gè)線程, 一個(gè)線程是Main, 另外一個(gè)是UI. 如果答案是這樣, 這篇文章就是寫給你的。 OK, 我們以SWT為例, 設(shè)計(jì)以下方案尋找答案, 第一步, 我們看能否找到兩個(gè)線程: 1. 從Main中啟動(dòng)SWT的界面, 在啟動(dòng)界面前, 將Main所在的線程打印出來(lái) 這里設(shè)計(jì)為Shell中嵌入一個(gè)Button 2. 點(diǎn)擊Button, 運(yùn)行一個(gè)耗時(shí)很長(zhǎng)的操作, 反復(fù)修改Button的文字, 在該線程中打印該線程的名稱 代碼是這樣的: - public static void main(String[] args) {
- final Display display = Display.getDefault();
- final Shell shell = new Shell();
- shell.setSize(500, 375);
- shell.setText("SWT Application");
- shell.setLayout(new FillLayout());
- btn = new Button(shell, SWT.NULL);
- btn.setText("shit");
- registerAction();
- shell.open();
- shell.layout();
- while (!shell.isDisposed()) {
- if (!display.readAndDispatch())
- display.sleep();
- }
- shell.dispose();
- display.dispose();
- }
- private static void registerAction() {
- btn.addMouseListener(new MouseListener() {
- @Override
- public void mouseDoubleClick(MouseEvent e) {
-
- }
- @Override
- public void mouseDown(MouseEvent e) {
- methodA();
- }
- @Override
- public void mouseUp(MouseEvent e) {
- }
- });
- }
-
-
-
- private static void methodA() {
- for (int i = 0; i < count; i++) {
- haveArest(300);
- System.out.println("MethodA:" + Thread.currentThread().getName());
- btn.setText(i + "");
- }
- }
haveArest方法在最后出現(xiàn), 只是封裝了一個(gè)讓線程等待一段時(shí)間, 打印的結(jié)果都為main, 于是得到第一個(gè)重要的結(jié)論: UI所在的線程和Main所在的線程都是同一個(gè)線程。 再來(lái)推斷一把: UI在哪個(gè)線程啟動(dòng)的, 則這個(gè)線程就是UI線程. -
-
-
- public static void main(String[] args) {
-
-
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- createUI();
- }
- });
- t.start();
- }
-
- private static void createUI()
- {
- System.out.println(Thread.currentThread().getName());
- final Display display = Display.getDefault();
- final Shell shell = new Shell();
- shell.setSize(500, 375);
- shell.setText("SWT Application");
- shell.setLayout(new FillLayout());
- Button btn = new Button(shell, SWT.NULL);
- btn.setText("shit");
- shell.open();
- shell.layout();
- while (!shell.isDisposed()) {
- if (!display.readAndDispatch())
- display.sleep();
- }
- shell.dispose();
- display.dispose();
- }
通過(guò)打印結(jié)果發(fā)現(xiàn), 推論是正確的. 根據(jù)鋪天蓋地參考書提示, 有這樣一條定律:
只可以存在一個(gè)UI線程 驗(yàn)證一下, 我們的驗(yàn)證方式是創(chuàng)建兩個(gè)UI線程: -
-
-
- public static void main(String[] args) {
-
-
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- createUI();
- }
- });
- t.start();
-
- t = new Thread(new Runnable() {
- @Override
- public void run() {
- createUI();
- }
- });
- t.start();
-
-
- }
-
- private static void createUI()
- {
- System.out.println(Thread.currentThread().getName());
- final Display display = new Display();
- final Shell shell = new Shell();
- shell.setSize(500, 375);
- shell.setText("SWT Application");
- shell.setLayout(new FillLayout());
- Button btn = new Button(shell, SWT.NULL);
- btn.setText("shit");
- shell.open();
- shell.layout();
- while (!shell.isDisposed()) {
- if (!display.readAndDispatch())
- display.sleep();
- }
- shell.dispose();
- display.dispose();
- }
但這里確實(shí)創(chuàng)建了兩個(gè)線程,??磥?lái)一個(gè)進(jìn)程是可以創(chuàng)建兩個(gè)線程的,。 可以存在一個(gè)或者多個(gè)UI線程, 下次看到參考書這么寫的時(shí)候, 可以BS它了,。 之前犯了一個(gè)錯(cuò)誤就是用Diplay display = Display.getDefault(); 這樣得到的是前一個(gè)線程創(chuàng)建的Display,,故不能創(chuàng)建. 造成只能創(chuàng)建一個(gè)UI線程的錯(cuò)覺(jué) 當(dāng)然我們的研究不能到此為止, 我們需要探究一下, 為什么總是被告知更新UI的動(dòng)作要放在UI線程中? 回到第一個(gè)例子中, 即:
- public static void main(String[] args) {
- final Display display = Display.getDefault();
- final Shell shell = new Shell();
- shell.setSize(500, 375);
- shell.setText("SWT Application");
- shell.setLayout(new FillLayout());
- btn = new Button(shell, SWT.NULL);
- btn.setText("shit");
- registerAction();
- shell.open();
- shell.layout();
- while (!shell.isDisposed()) {
- if (!display.readAndDispatch())
- display.sleep();
- }
- shell.dispose();
- display.dispose();
- }
- private static void registerAction() {
- btn.addMouseListener(new MouseListener() {
- @Override
- public void mouseDoubleClick(MouseEvent e) {
-
- }
- @Override
- public void mouseDown(MouseEvent e) {
- methodA();
- }
- @Override
- public void mouseUp(MouseEvent e) {
- }
- });
- }
-
-
-
- private static void methodA() {
- for (int i = 0; i < count; i++) {
- haveArest(300);
- System.out.println("MethodA:" + Thread.currentThread().getName());
- btn.setText(i + "");
- }
- }
運(yùn)行的時(shí)候拖動(dòng)試試, 發(fā)現(xiàn)不動(dòng), 直到for循環(huán)中修改btn的操作完成. 這里我們不難明白一個(gè)觀點(diǎn): 同一個(gè)線程的情況下, 一個(gè)操作(拖動(dòng)), 是需要等待另外一個(gè)操作(更新btn)完成后, 才可以進(jìn)行的,。 不難理解, 我們常用的做法是: 通過(guò)啟動(dòng)另外一個(gè)線程, 在cpu微小的間隔時(shí)間內(nèi),完成兩個(gè)動(dòng)作的交替 于是有了下面的代碼:
- private static Button btn;
-
- private static final int count = 20;
- public static void main(String[] args) {
- final Display display = Display.getDefault();
- final Shell shell = new Shell();
- shell.setSize(500, 375);
- shell.setText("SWT Application");
- shell.setLayout(new FillLayout());
- btn = new Button(shell, SWT.NULL);
- btn.setText("shit");
- registerAction();
- shell.open();
- shell.layout();
- while (!shell.isDisposed()) {
- if (!display.readAndDispatch())
- display.sleep();
- }
- shell.dispose();
- display.dispose();
- }
- private static void registerAction() {
- btn.addMouseListener(new MouseListener() {
- @Override
- public void mouseDoubleClick(MouseEvent e) {
-
- }
- @Override
- public void mouseDown(MouseEvent e) {
- methodB();
- }
- @Override
- public void mouseUp(MouseEvent e) {
- }
- });
- }
-
-
-
- private static void methodB() {
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < count; i++) {
- haveArest(300);
- System.out.println("MethodB:"
- + Thread.currentThread().getName());
- btn.setText(i + "");
- }
- }
- });
- t.start();
- }
但這樣發(fā)現(xiàn)會(huì)報(bào)錯(cuò), 原因是, 線程訪問(wèn)出錯(cuò)了, 因?yàn)橛幸粋€(gè)這樣的規(guī)則需要我們保障: 所有的UI相關(guān)的操作, 務(wù)必保證在UI線程中更新. 為什么會(huì)有這樣一條鐵律? 原因是界面的消息需要分發(fā)到各大控件上面去, 如果不能保證UI在相同的線程, 分發(fā)起來(lái)就會(huì)比較復(fù)雜. UI本身占用的資源比較多. 如果在將UI分屬不同的線程, 切換起來(lái), 將耗費(fèi)大量的CPU資源. 為了保證這條, SWT 是這么做的, 利用Diplay這個(gè)變量獲取UI線程, 然后在其中做UI訪問(wèn)和操作: - private static Button btn;
-
- private static final int count = 20;
- public static void main(String[] args) {
- final Display display = Display.getDefault();
- final Shell shell = new Shell();
- shell.setSize(500, 375);
- shell.setText("SWT Application");
- shell.setLayout(new FillLayout());
- btn = new Button(shell, SWT.NULL);
- btn.setText("shit");
- registerAction();
- shell.open();
- shell.layout();
- while (!shell.isDisposed()) {
- if (!display.readAndDispatch())
- display.sleep();
- }
- shell.dispose();
- display.dispose();
- }
- private static void registerAction() {
- btn.addMouseListener(new MouseListener() {
- @Override
- public void mouseDoubleClick(MouseEvent e) {
-
- }
- @Override
- public void mouseDown(MouseEvent e) {
- methodC();
- }
- @Override
- public void mouseUp(MouseEvent e) {
- }
- });
- }
-
- private static void methodC() {
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < count; i++) {
- System.out.println("MethodB Thread:"
- + Thread.currentThread().getName());
-
- haveArest(300);
- final Display display = Display.getDefault();
- final String s = i + "";
- if ((display != null) && (!display.isDisposed())) {
- display.asyncExec(new Runnable() {
- @Override
- public void run() {
- System.out.println("MethodB Thread asyncExec:"
- + Thread.currentThread().getName());
- btn.setText(s);
- }
- });
- }
- }
- }
- });
- t.start();
- }
-
- private static void haveArest(int sleepTime)
- {
- try {
- Thread.sleep(sleepTime);
- } catch (InterruptedException e) {
-
- e.printStackTrace();
- }
- }
后面會(huì)繼續(xù)關(guān)注Swing和Android的例子, 相信這些也是大同小異的. 關(guān)鍵是, UI特殊, 但特殊性不在于它是一個(gè)額外的線程. 這樣的應(yīng)用其實(shí)很多, 比如我們不斷刷表格的時(shí)候, 為了讓界面能接受其它的響應(yīng)事件, 一般都把刷表格的動(dòng)作放置到另外的線程中, 用Display.asychronize()來(lái)保障其訪問(wèn)UI元素的安全行(即在UI中訪問(wèn)). 總結(jié)一下, 本文由如下結(jié)論: UI線程和主線程,普通線程的關(guān)系 1. UI線程和Main線程沒(méi)有必然聯(lián)系, 從Main函數(shù)啟動(dòng), 也可以從一個(gè)其它的線程啟動(dòng). 啟動(dòng)UI的線程, 則為UI線程 2. 如果第一個(gè)線程啟動(dòng)了UI. 則第一個(gè)線程則成為UI線程. 如果第二個(gè)線程涉及UI操作, 則需要保證這個(gè)操作放在UI線程中. 否則會(huì)出現(xiàn)Invalid thread access錯(cuò)誤.
SWT為什么會(huì)有Display.asyncExec(new Runnable())操作: 1. 當(dāng)界面執(zhí)行了長(zhǎng)時(shí)段的UI操作, 比如進(jìn)度條, 此時(shí)如果把更新UI的操作放在唯一的UI線程中執(zhí)行, 那么本線程將全部消耗CPU資源, 造成界面無(wú)法拖動(dòng).拖動(dòng)則界面死掉MethodA(),。 2. 為了解決問(wèn)題1, 我們一般另外啟動(dòng)一個(gè)線程進(jìn)行操作, 這樣使得界面可以拖動(dòng), 但是UI的操作無(wú)法在其它的線程中完成, 只能在UI線程中完成, 3. Display.asyncExec(new Runnable()的目的就是將這個(gè)動(dòng)作放在UI線程中完成. 這樣避免報(bào)錯(cuò)Invalid thread access 補(bǔ)充SWT的知識(shí), 很多不明白Display.asyncExec 和Display.syncExec的區(qū)別, 用個(gè)例子說(shuō)明一下: -
-
-
-
- private static void methodD() {
- for (int i = 0; i < count; i++) {
- haveArest(300);
- final Display display = Display.getDefault();
- final String s = i + "";
- if ((display != null) && (!display.isDisposed())) {
- display.syncExec(new Runnable() {
- @Override
- public void run() {
-
-
-
-
- btn.setText(s);
- System.out.println("" + s);
- }
- });
- }
- }
- }
按注釋運(yùn)行下, 就會(huì)發(fā)現(xiàn)這里面大有玄機(jī), 不過(guò)我這邊并不是為了解決SWT的問(wèn)題, 而是針對(duì)所有的UI線程來(lái)的, 所以, 不再做解釋. 推薦閱讀. 該牛人的長(zhǎng)篇大作. 多謝同事章導(dǎo)對(duì)SWT修正的問(wèn)題. 即使彌補(bǔ)了誤導(dǎo)大家的觀點(diǎn), :)
Android 也是相同的原理: 1. 通過(guò)多線程避免界面假死
2. 通過(guò)Hander保證訪問(wèn)界面元素在UI線程中進(jìn)行. 其它的一些細(xì)微差別, 不需要多講. 原理乃一個(gè)模子出來(lái)的. 一個(gè)Android Helloworld運(yùn)行起來(lái)的時(shí)候, 有四個(gè)線程 1. 傳說(shuō)中的Main線程 2. 另外三個(gè)都是Binder Thread, 貌似是為了跨進(jìn)程通信用的監(jiān)聽(tīng)線程. 貌似很多Android的教程都把UI線程當(dāng)特殊的一個(gè)線程.
至于Swing/awt 貌似無(wú)需費(fèi)心機(jī)去了解了.
|