2019獨角獸企業重金招聘Python工程師標準>>>
在做GUI的時候, 無論是SWT, AWT, Swing 還是Android, 都需要面對UI線程的問題, UI線程往往會被單獨的提出來單獨對待, 試著問自己,
當GUI啟動的時候, 后臺會運行幾個線程? 比如?
1. SWT 從Main函數啟動?
2. Swing 從Main函數啟動?
3. Android 界面啟動?
常常我們被告知, 主線程, UI線程, 因此這里很多會回答, 有兩個線程, 一個線程是Main, 另外一個是UI.? 如果答案是這樣, 這篇文章就是寫給你的。
?
OK, 我們以SWT為例, 設計以下方案尋找答案, 第一步, 我們看能否找到兩個線程:
1. 從Main中啟動SWT的界面, 在啟動界面前, 將Main所在的線程打印出來 這里設計為Shell中嵌入一個Button
2. 點擊Button, 運行一個耗時很長的操作, 反復修改Button的文字, 在該線程中打印該線程的名稱
?
代碼是這樣的:
[java]? view plain copy
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)?{?? ????????????//?TODO?Auto-generated?method?stub?? ????????}?? ????????@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方法在最后出現, 只是封裝了一個讓線程等待一段時間, 打印的結果都為main, 于是得到第一個重要的結論:
UI所在的線程和Main所在的線程都是同一個線程。
?
再來推斷一把:
UI在哪個線程啟動的, 則這個線程就是UI線程.
[java]? view plain copy
/**? ?*?@param?args? ?*/?? public?static?void?main(String[]?args)?{?? ????//?TODO?Auto-generated?method?stub?? ?????? ????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();?? }?? 通過打印結果發現, 推論是正確的.
?
根據鋪天蓋地參考書提示, 有這樣一條定律:
只可以存在一個UI線程
驗證一下, 我們的驗證方式是創建兩個UI線程:
[java]? view plain copy
/**? ?*?@param?args? ?*/?? public?static?void?main(String[]?args)?{?? ????//?TODO?Auto-generated?method?stub?? ?????? ????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();?? }?? 但這里確實創建了兩個線程。看來一個進程是可以創建兩個線程的。
?可以存在一個或者多個UI線程,?下次看到參考書這么寫的時候, 可以BS它了。??
?
之前犯了一個錯誤就是用Diplay display = Display.getDefault(); 這樣得到的是前一個線程創建的Display,故不能創建. 造成只能創建一個UI線程的錯覺
?
當然我們的研究不能到此為止, 我們需要探究一下, 為什么總是被告知更新UI的動作要放在UI線程中?
回到第一個例子中, 即:
?
[java]? view plain copy
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)?{?? ????????????//?TODO?Auto-generated?method?stub?? ????????}?? ????????@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?+?"");?? ????}?? }?? ?
運行的時候拖動試試, 發現不動, 直到for循環中修改btn的操作完成.
這里我們不難明白一個觀點:
同一個線程的情況下, 一個操作(拖動), 是需要等待另外一個操作(更新btn)完成后, 才可以進行的。
不難理解, 我們常用的做法是:
通過啟動另外一個線程, 在cpu微小的間隔時間內,完成兩個動作的交替
于是有了下面的代碼:
[java]?view plaincopy 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)?{?? ????????????//?TODO?Auto-generated?method?stub?? ????????}?? ????????@Override?? ????????public?void?mouseDown(MouseEvent?e)?{?? ????????????methodB();?? ????????}?? ????????@Override?? ????????public?void?mouseUp(MouseEvent?e)?{?? ????????}?? ????});?? }?? /**? ?*?為了解決拖拽不動,?界面死掉,?增加線程控制,?但產生了Invalid?thread?access的問題? ?*/?? 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();?? }??
?
但這樣發現會報錯, 原因是, 線程訪問出錯了, 因為有一個這樣的規則需要我們保障:
所有的UI相關的操作, 務必保證在UI線程中更新.
為什么會有這樣一條鐵律? 原因是界面的消息需要分發到各大控件上面去, 如果不能保證UI在相同的線程, 分發起來就會比較復雜. UI本身占用的資源比較多.? 如果在將UI分屬不同的線程, 切換起來, 將耗費大量的CPU資源.
?
為了保證這條, SWT 是這么做的, 利用Diplay這個變量獲取UI線程, 然后在其中做UI訪問和操作:
[java]? view plain copy
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)?{?? ????????????//?TODO?Auto-generated?method?stub?? ????????}?? ????????@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)?{?? ????????//?TODO?Auto-generated?catch?block?? ????????e.printStackTrace();?? ????}?? }?? ?
后面會繼續關注Swing和Android的例子, 相信這些也是大同小異的.?關鍵是, UI特殊, 但特殊性不在于它是一個額外的線程.
?
這樣的應用其實很多, 比如我們不斷刷表格的時候,?為了讓界面能接受其它的響應事件, 一般都把刷表格的動作放置到另外的線程中, 用Display.asychronize()來保障其訪問UI元素的安全行(即在UI中訪問).?
?
總結一下, 本文由如下結論:?
UI線程和主線程,普通線程的關系?
1. UI線程和Main線程沒有必然聯系, 從Main函數啟動, 也可以從一個其它的線程啟動. 啟動UI的線程, 則為UI線程?
2. 如果第一個線程啟動了UI. 則第一個線程則成為UI線程. 如果第二個線程涉及UI操作, 則需要保證這個操作放在UI線程中. 否則會出現Invalid thread access錯誤.?
SWT為什么會有Display.asyncExec(new Runnable())操作:?
1. 當界面執行了長時段的UI操作, 比如進度條, 此時如果把更新UI的操作放在唯一的UI線程中執行, 那么本線程將全部消耗CPU資源, 造成界面無法拖動.拖動則界面死掉MethodA()。?
2. 為了解決問題1, 我們一般另外啟動一個線程進行操作, 這樣使得界面可以拖動, 但是UI的操作無法在其它的線程中完成, 只能在UI線程中完成,?
3. Display.asyncExec(new Runnable()的目的就是將這個動作放在UI線程中完成. 這樣避免報錯Invalid thread access
?
?
補充SWT的知識, 很多不明白Display.asyncExec 和Display.syncExec的區別, 用個例子說明一下:
[java]? view plain copy
/**? ?*?在UI線程中跑動,?注意,?在UI線程中跑動asyncExec/syncExec都不能解決拖動的問題,?只能另起線程? ?*?才能解決如:methodC? ?*/?? 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()?{?? ????????????????????//如果是asyncExec的話,?這里到最后次才執行?? ????????????????????//?asyncExec要等到發起asyncExec的線程執行完畢,?他才有機會執行。在單線程的情況下,?發起asyncExec的線程和asyncExec里面的run內容都在同一個線程?? ????????????????????//所以要等到asyncExec執行完畢,?asyncExec中run的東西,?才有機會執行?? ????????????????????//syncExec則不同,?它務必要保證里面的方法執行后,再回到發起syncExec方法所在的線程,?所以這里相當于一個流暢的串行操作?? ????????????????????btn.setText(s);?? ????????????????????System.out.println(""?+?s);?? ????????????????}?? ????????????});?? ????????}?? ????}?? }?? ?
按注釋運行下, 就會發現這里面大有玄機, 不過我這邊并不是為了解決SWT的問題, 而是針對所有的UI線程來的, 所以, 不再做解釋.
?
推薦閱讀.?該牛人的長篇大作.
?
多謝同事章導對SWT修正的問題. 即使彌補了誤導大家的觀點, :)
?
?
?
Android 也是相同的原理:
?
1. 通過多線程避免界面假死
2. 通過Hander保證訪問界面元素在UI線程中進行.
?
其它的一些細微差別, 不需要多講. 原理乃一個模子出來的.
?
一個Android Helloworld運行起來的時候, 有四個線程
?
1. 傳說中的Main線程
?
2. 另外三個都是Binder Thread, 貌似是為了跨進程通信用的監聽線程.
?
貌似很多Android的教程都把UI線程當特殊的一個線程.
?
轉載于:https://my.oschina.net/mojiewhy/blog/180525
總結
以上是生活随笔為你收集整理的理解UI线程——SWT, Android, 和Swing的UI机理的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。