在做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的文字, 在该线程中打印该线程的名称

代码是这样的:

view plain
  1. publicstaticvoidmain(String[]args){
  2. finalDisplaydisplay=Display.getDefault();
  3. finalShellshell=newShell();
  4. shell.setSize(500,375);
  5. shell.setText("SWTApplication");
  6. shell.setLayout(newFillLayout());
  7. btn=newButton(shell,SWT.NULL);
  8. btn.setText("shit");
  9. registerAction();
  10. shell.open();
  11. shell.layout();
  12. while(!shell.isDisposed()){
  13. if(!display.readAndDispatch())
  14. display.sleep();
  15. }
  16. shell.dispose();
  17. display.dispose();
  18. }
  19. privatestaticvoidregisterAction(){
  20. btn.addMouseListener(newMouseListener(){
  21. @Override
  22. publicvoidmouseDoubleClick(MouseEvente){
  23. //TODOAuto-generatedmethodstub
  24. }
  25. @Override
  26. publicvoidmouseDown(MouseEvente){
  27. methodA();
  28. }
  29. @Override
  30. publicvoidmouseUp(MouseEvente){
  31. }
  32. });
  33. }
  34. /**
  35. *持续的跑动,打印线程的名称,注意拖拽不动,界面死掉,直到跑完
  36. */
  37. privatestaticvoidmethodA(){
  38. for(inti=0;i<count;i++){
  39. haveArest(300);
  40. System.out.println("MethodA:"+Thread.currentThread().getName());
  41. btn.setText(i+"");
  42. }
  43. }

haveArest方法在最后出现, 只是封装了一个让线程等待一段时间, 打印的结果都为main, 于是得到第一个重要的结论:

UI所在的线程和Main所在的线程都是同一个线程。

再来推断一把:

UI在哪个线程启动的, 则这个线程就是UI线程.

view plain
  1. /**
  2. *@paramargs
  3. */
  4. publicstaticvoidmain(String[]args){
  5. //TODOAuto-generatedmethodstub
  6. Threadt=newThread(newRunnable(){
  7. @Override
  8. publicvoidrun(){
  9. createUI();
  10. }
  11. });
  12. t.start();
  13. }
  14. privatestaticvoidcreateUI()
  15. {
  16. System.out.println(Thread.currentThread().getName());
  17. finalDisplaydisplay=Display.getDefault();
  18. finalShellshell=newShell();
  19. shell.setSize(500,375);
  20. shell.setText("SWTApplication");
  21. shell.setLayout(newFillLayout());
  22. Buttonbtn=newButton(shell,SWT.NULL);
  23. btn.setText("shit");
  24. shell.open();
  25. shell.layout();
  26. while(!shell.isDisposed()){
  27. if(!display.readAndDispatch())
  28. display.sleep();
  29. }
  30. shell.dispose();
  31. display.dispose();
  32. }

通过打印结果发现, 推论是正确的.

根据铺天盖地参考书提示, 有这样一条定律:

只可以存在一个UI线程

验证一下, 我们的验证方式是创建两个UI线程:

view plain
  1. /**
  2. *@paramargs
  3. */
  4. publicstaticvoidmain(String[]args){
  5. //TODOAuto-generatedmethodstub
  6. Threadt=newThread(newRunnable(){
  7. @Override
  8. publicvoidrun(){
  9. createUI();
  10. }
  11. });
  12. t.start();
  13. t=newThread(newRunnable(){
  14. @Override
  15. publicvoidrun(){
  16. createUI();
  17. }
  18. });
  19. t.start();
  20. }
  21. privatestaticvoidcreateUI()
  22. {
  23. System.out.println(Thread.currentThread().getName());
  24. finalDisplaydisplay=newDisplay();
  25. finalShellshell=newShell();
  26. shell.setSize(500,375);
  27. shell.setText("SWTApplication");
  28. shell.setLayout(newFillLayout());
  29. Buttonbtn=newButton(shell,SWT.NULL);
  30. btn.setText("shit");
  31. shell.open();
  32. shell.layout();
  33. while(!shell.isDisposed()){
  34. if(!display.readAndDispatch())
  35. display.sleep();
  36. }
  37. shell.dispose();
  38. display.dispose();
  39. }

但这里确实创建了两个线程。看来一个进程是可以创建两个线程的。

可以存在一个或者多个UI线程,下次看到参考书这么写的时候, 可以BS它了。

之前犯了一个错误就是用Diplay display = Display.getDefault(); 这样得到的是前一个线程创建的Display,故不能创建. 造成只能创建一个UI线程的错觉

当然我们的研究不能到此为止, 我们需要探究一下, 为什么总是被告知更新UI的动作要放在UI线程中?

回到第一个例子中, 即:


view plain
  1. publicstaticvoidmain(String[]args){
  2. finalDisplaydisplay=Display.getDefault();
  3. finalShellshell=newShell();
  4. shell.setSize(500,375);
  5. shell.setText("SWTApplication");
  6. shell.setLayout(newFillLayout());
  7. btn=newButton(shell,SWT.NULL);
  8. btn.setText("shit");
  9. registerAction();
  10. shell.open();
  11. shell.layout();
  12. while(!shell.isDisposed()){
  13. if(!display.readAndDispatch())
  14. display.sleep();
  15. }
  16. shell.dispose();
  17. display.dispose();
  18. }
  19. privatestaticvoidregisterAction(){
  20. btn.addMouseListener(newMouseListener(){
  21. @Override
  22. publicvoidmouseDoubleClick(MouseEvente){
  23. //TODOAuto-generatedmethodstub
  24. }
  25. @Override
  26. publicvoidmouseDown(MouseEvente){
  27. methodA();
  28. }
  29. @Override
  30. publicvoidmouseUp(MouseEvente){
  31. }
  32. });
  33. }
  34. /**
  35. *持续的跑动,打印线程的名称,注意拖拽不动,界面死掉,直到跑完
  36. */
  37. privatestaticvoidmethodA(){
  38. for(inti=0;i<count;i++){
  39. haveArest(300);
  40. System.out.println("MethodA:"+Thread.currentThread().getName());
  41. btn.setText(i+"");
  42. }
  43. }

运行的时候拖动试试, 发现不动, 直到for循环中修改btn的操作完成.

这里我们不难明白一个观点:

同一个线程的情况下, 一个操作(拖动), 是需要等待另外一个操作(更新btn)完成后, 才可以进行的。

不难理解, 我们常用的做法是:

通过启动另外一个线程, 在cpu微小的间隔时间内,完成两个动作的交替

于是有了下面的代码:

view plain
  1. privatestaticButtonbtn;
  2. privatestaticfinalintcount=20;
  3. publicstaticvoidmain(String[]args){
  4. finalDisplaydisplay=Display.getDefault();
  5. finalShellshell=newShell();
  6. shell.setSize(500,375);
  7. shell.setText("SWTApplication");
  8. shell.setLayout(newFillLayout());
  9. btn=newButton(shell,SWT.NULL);
  10. btn.setText("shit");
  11. registerAction();
  12. shell.open();
  13. shell.layout();
  14. while(!shell.isDisposed()){
  15. if(!display.readAndDispatch())
  16. display.sleep();
  17. }
  18. shell.dispose();
  19. display.dispose();
  20. }
  21. privatestaticvoidregisterAction(){
  22. btn.addMouseListener(newMouseListener(){
  23. @Override
  24. publicvoidmouseDoubleClick(MouseEvente){
  25. //TODOAuto-generatedmethodstub
  26. }
  27. @Override
  28. publicvoidmouseDown(MouseEvente){
  29. methodB();
  30. }
  31. @Override
  32. publicvoidmouseUp(MouseEvente){
  33. }
  34. });
  35. }
  36. /**
  37. *为了解决拖拽不动,界面死掉,增加线程控制,但产生了Invalidthreadaccess的问题
  38. */
  39. privatestaticvoidmethodB(){
  40. Threadt=newThread(newRunnable(){
  41. @Override
  42. publicvoidrun(){
  43. for(inti=0;i<count;i++){
  44. haveArest(300);
  45. System.out.println("MethodB:"
  46. +Thread.currentThread().getName());
  47. btn.setText(i+"");
  48. }
  49. }
  50. });
  51. t.start();
  52. }

但这样发现会报错, 原因是, 线程访问出错了, 因为有一个这样的规则需要我们保障:

所有的UI相关的操作, 务必保证在UI线程中更新.

为什么会有这样一条铁律? 原因是界面的消息需要分发到各大控件上面去, 如果不能保证UI在相同的线程, 分发起来就会比较复杂. UI本身占用的资源比较多. 如果在将UI分属不同的线程, 切换起来, 将耗费大量的CPU资源.

为了保证这条, SWT 是这么做的, 利用Diplay这个变量获取UI线程, 然后在其中做UI访问和操作:

view plain
  1. privatestaticButtonbtn;
  2. privatestaticfinalintcount=20;
  3. publicstaticvoidmain(String[]args){
  4. finalDisplaydisplay=Display.getDefault();
  5. finalShellshell=newShell();
  6. shell.setSize(500,375);
  7. shell.setText("SWTApplication");
  8. shell.setLayout(newFillLayout());
  9. btn=newButton(shell,SWT.NULL);
  10. btn.setText("shit");
  11. registerAction();
  12. shell.open();
  13. shell.layout();
  14. while(!shell.isDisposed()){
  15. if(!display.readAndDispatch())
  16. display.sleep();
  17. }
  18. shell.dispose();
  19. display.dispose();
  20. }
  21. privatestaticvoidregisterAction(){
  22. btn.addMouseListener(newMouseListener(){
  23. @Override
  24. publicvoidmouseDoubleClick(MouseEvente){
  25. //TODOAuto-generatedmethodstub
  26. }
  27. @Override
  28. publicvoidmouseDown(MouseEvente){
  29. methodC();
  30. }
  31. @Override
  32. publicvoidmouseUp(MouseEvente){
  33. }
  34. });
  35. }
  36. privatestaticvoidmethodC(){
  37. Threadt=newThread(newRunnable(){
  38. @Override
  39. publicvoidrun(){
  40. for(inti=0;i<count;i++){
  41. System.out.println("MethodBThread:"
  42. +Thread.currentThread().getName());
  43. haveArest(300);
  44. finalDisplaydisplay=Display.getDefault();
  45. finalStrings=i+"";
  46. if((display!=null)&&(!display.isDisposed())){
  47. display.asyncExec(newRunnable(){
  48. @Override
  49. publicvoidrun(){
  50. System.out.println("MethodBThreadasyncExec:"
  51. +Thread.currentThread().getName());
  52. btn.setText(s);
  53. }
  54. });
  55. }
  56. }
  57. }
  58. });
  59. t.start();
  60. }
  61. privatestaticvoidhaveArest(intsleepTime)
  62. {
  63. try{
  64. Thread.sleep(sleepTime);
  65. }catch(InterruptedExceptione){
  66. //TODOAuto-generatedcatchblock
  67. e.printStackTrace();
  68. }
  69. }

后面会继续关注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的区别, 用个例子说明一下:

view plain
  1. /**
  2. *在UI线程中跑动,注意,在UI线程中跑动asyncExec/syncExec都不能解决拖动的问题,只能另起线程
  3. *才能解决如:methodC
  4. */
  5. privatestaticvoidmethodD(){
  6. for(inti=0;i<count;i++){
  7. haveArest(300);
  8. finalDisplaydisplay=Display.getDefault();
  9. finalStrings=i+"";
  10. if((display!=null)&&(!display.isDisposed())){
  11. display.syncExec(newRunnable(){
  12. @Override
  13. publicvoidrun(){
  14. //如果是asyncExec的话,这里到最后次才执行
  15. //asyncExec要等到发起asyncExec的线程执行完毕,他才有机会执行。在单线程的情况下,发起asyncExec的线程和asyncExec里面的run内容都在同一个线程
  16. //所以要等到asyncExec执行完毕,asyncExec中run的东西,才有机会执行
  17. //syncExec则不同,它务必要保证里面的方法执行后,再回到发起syncExec方法所在的线程,所以这里相当于一个流畅的串行操作
  18. btn.setText(s);
  19. System.out.println(""+s);
  20. }
  21. });
  22. }
  23. }
  24. }

按注释运行下, 就会发现这里面大有玄机, 不过我这边并不是为了解决SWT的问题, 而是针对所有的UI线程来的, 所以, 不再做解释.

推荐阅读.该牛人的长篇大作.

多谢同事章导对SWT修正的问题. 即使弥补了误导大家的观点, :)

Android 也是相同的原理:

1. 通过多线程避免界面假死


2. 通过Hander保证访问界面元素在UI线程中进行.

其它的一些细微差别, 不需要多讲. 原理乃一个模子出来的.

一个Android Helloworld运行起来的时候, 有四个线程

1. 传说中的Main线程

2. 另外三个都是Binder Thread, 貌似是为了跨进程通信用的监听线程.

貌似很多Android的教程都把UI线程当特殊的一个线程.

至于Swing/awt 貌似无需费心机去了解了.

分享到:查看评论
5楼donghong822010-12-13 15:18发表[回复]
想了想, 也许SWT真不一样.
Re:ostrichmyself2010-12-14 09:20发表[回复]
回复 donghong82:

兄台, 我这里没有对AWT进行分析.... 但SWT的分析应该是没有问题的.
4楼donghong822010-12-13 14:53发表[回复]
几乎胡扯了, 再好好测测, UI线程与启动UI的线程绝对不是一个. 而且, 也一定只有一个UI处理线程(EDT).其实还有一个AWT事件线程, 但用于UI控件的只有EDT. 不是起了多个窗口就是多个UI线程.
不过很有想法. 有怀疑精神, 但几乎唐吉柯德了.
Re:ostrichmyself2011-05-12 11:42发表[回复]
回复 donghong82:

谢谢你的肯定和否定,

事件线程, 是很多参考书误导的结果, 没有所谓专门的事件线程。

没有创建的话, 只有一个线程. 这就是主线程. 这个都是上面用代码验证过的.
3楼ch53782010-08-27 11:38发表[回复]
2楼harmony_dota2010-08-23 10:29发表[回复]
我是eoe看到你写的东西的 发现跟别人不一样 喜欢您经常反问的方式 能提供给我们多思考的空间 这个android的ui机制 我没有看的太明白 可能是大学时候没有基础吧
1.swt awt swing 都没有学过 会不会影响android开发呢?
2.要不要学习下服务端的语言呢?比如js,jsp,还有比较新的python rub
y什么的呢?大学时期就上过jsp和asp,我个人基础不太好...
3.java基础超级菜, 什么 多线程 synchronized 我基本没有用过 ...java基础怎么提高呢???
我问的这几个问题也是帮着和我一样大学不懂事天天玩的孩子们问的.......
Re:ostrichmyself2010-08-23 11:06发表[回复]
回复 harmony_dota:
千万不要被我说的这些东西误导, 永远关注一个方向,深入了解,其它方向,慢慢熟悉,不要一口气搞定全部的问题。希望这些回答能解决你心中的部分困惑。我的想法不一定正确,只是参考。相信你会找到自己的方向!
Re:harmony_dota2010-08-24 09:27发表[回复]
回复 ostrichmyself:谢谢,嘿嘿,我也在多想呢,可是很多还是瞎想,我很笨所以基本都想不明白 只能模仿 以后有什么不明白的 我会经常问你的 哈哈
Re:ostrichmyself2010-08-23 11:02发表[回复]
回复 harmony_dota:
第三个问题, 提高java语言, 第一步要提高自己所站的高度, 假如你是面试官,假如你是Java课程的老师,你会出一份什么样的试卷考核学生?问问自己,Java里面核心的东西在哪里? 就语法应用而言,排名的先后应该是:多线程,集合类,文件io, 这些问题先搞清楚一下, 注意这些仅仅是语法,其实Java的核心是虚拟技术,即jvm, 思考一下为什么可以跨平台,为什么是安全的,为什么可以网络移动? 这样一步步的去想,遇到问题记录,总结,慢慢的你就会对这些有兴趣。会在不知不觉中提高。
Re:ostrichmyself2010-08-23 10:55发表[回复]
回复 harmony_dota:
第二个问题, 是否需要服务端的语言, 我的答案是肯定的,服务端的语言,我指的是纯服务端,没有html, 没有js,也没有jsp,即没有前端的服务端语言,即Servlet. 为什么? 因为后面智能终端应用出来后,不需要通过浏览器访问,而是单纯的数据传递. 这个才是Server的核心. 你要了解纯Serlet编程,和HTTP协议, 如果有能力,最好了解一下Database, 对你后续发展是很有帮助的
Re:ostrichmyself2010-08-23 10:50发表[回复]
回复 harmony_dota:
你的第一个问题, Android不必要学习swt等其他的gui开发. 没有任何影响, 但如果有swt开发,你愿意做类比,对你的学习会有帮助,仅此而已, 所以零基础入门,也同样可以学好Android, 关键是自己多反思问题,反思Android为什么有这样的设计,而不是另外的设计。这样你就不单单是一个编码者,而是一个设计者。
Re:ostrichmyself2010-08-23 10:48发表[回复]
回复 harmony_dota:
谢谢您的关注! 我说的这个Android UI线程的问题, 应该是我没有讲清楚才会导致你不懂, 这里确实是给那些有过GUI开发的人看的. 所以做了部分类比. 如果你有兴趣, 可以仿照前面SWT的例子, 做一个多线程更新. 当然后面我会重构这部分, 将Android 源码提供出来.
1楼mx2855459742010-08-18 14:42发表[回复]

更多相关文章

  1. android中push机制实现:搭建XMPP协议,实现自主推送消息到手机
  2. Android艺术探索读书笔记 -IPC机制
  3. android离开一个页面时关闭子线程
  4. Android再按一次完全退出程序及禁止返回上一个界面等
  5. 转换 iOS 用户界面到 Android(安卓)的 5 大要诀
  6. 【Android】UI设计之界面布局
  7. iOS到Android到底有多远
  8. Android(安卓)重要基本开发规范
  9. 都在说EventBus,我也来一波EventBus

随机推荐

  1. 2021年5月IDEA最新激活码,解决This licens
  2. Amazon blink 智能安全摄像头被发现命令
  3. 网络战争模拟软件:在持续的网络战争期间保
  4. 代码分析平台CodeQL学习手记(二)
  5. 一线大厂在机器学习方向的面试题(一)
  6. 股票均线策略
  7. “360给奔驰挖了19个漏洞”,车联网给***留
  8. JavaScript:演示Ajax的get和post请求,练习
  9. PyCharm版本有哪些?是否免费?
  10. 通过Windows预读取文件找寻***者的蛛丝马