在android开发中,我们经常会遇到这样一种情况:在ui界面上进行某项操作后要执行一段很耗时的代码,比如我们在界面上点击了一个”下载“按钮,那么我们需要执行网络请求,这是一个耗时操作,因为不知道什么时候才能完成。为了保证不影响ui线程,所以我们会创建一个新的线程去执行我们的耗时的代码。当我们的耗时操作完成时,我们需要更新ui界面以告知用户操作完成了。所以我们可能会写出如下的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package ispring.com.testhandler; import android.app.activity; import android.os.bundle; import android.view.view; import android.widget.button; import android.widget.textview; public class mainactivity extends activity implements button.onclicklistener { private textview statustextview = null ; @override protected void oncreate(bundle savedinstancestate) { super .oncreate(savedinstancestate); setcontentview(r.layout.activity_main); statustextview = (textview)findviewbyid(r.id.statustextview); button btndownload = (button)findviewbyid(r.id.btndownload); btndownload.setonclicklistener( this ); } @override public void onclick(view v) { downloadthread downloadthread = new downloadthread(); downloadthread.start(); } class downloadthread extends thread{ @override public void run() { try { system.out.println( "开始下载文件" ); //此处让线程downloadthread休眠5秒中,模拟文件的耗时过程 thread.sleep( 5000 ); system.out.println( "文件下载完成" ); //文件下载完成后更新ui mainactivity. this .statustextview.settext( "文件下载完成" ); } catch (interruptedexception e){ e.printstacktrace(); } } } } |
上面的代码演示了单击”下载“按钮后会启动一个新的线程去执行实际的下载操作,执行完毕后更新ui界面。但是在实际运行到代码mainactivity.this.statustextview.settext(“文件下载完成”)时,会报错如下,系统崩溃退出:
android.view.viewrootimpl$calledfromwrongthreadexception: only the original thread that created a view hierarchy can touch its views.
错误的意思是只有创建view的原始线程才能更新view。出现这样错误的原因是android中的view不是线程安全的,在android应用启动时,会自动创建一个线程,即程序的主线程,主线程负责ui的展示、ui事件消息的派发处理等等,因此主线程也叫做ui线程,statustextview是在ui线程中创建的,当我们在downloadthread线程中去更新ui线程中创建的statustextview时自然会报上面的错误。android的ui控件是非线程安全的,其实很多平台的ui控件都是非线程安全的,比如c#的.net framework中的ui控件也是非线程安全的,所以不仅仅在android平台中存在从一个新线程中去更新ui线程中创建的ui控件的问题。不同的平台提供了不同的解决方案以实现跨线程跟新ui控件,android为了解决这种问题引入了handler机制。
那么handler到底是什么呢?handler是android中引入的一种让开发者参与处理线程中消息循环的机制。每个hanlder都关联了一个线程,每个线程内部都维护了一个消息队列messagequeue,这样handler实际上也就关联了一个消息队列。可以通过handler将message和runnable对象发送到该handler所关联线程的messagequeue(消息队列)中,然后该消息队列一直在循环拿出一个message,对其进行处理,处理完之后拿出下一个message,继续进行处理,周而复始。当创建一个handler的时候,该handler就绑定了当前创建hanlder的线程。从这时起,该hanlder就可以发送message和runnable对象到该handler对应的消息队列中,当从messagequeue取出某个message时,会让handler对其进行处理。
handler可以用来在多线程间进行通信,在另一个线程中去更新ui线程中的ui控件只是handler使用中的一种典型案例,除此之外,handler可以做很多其他的事情。每个handler都绑定了一个线程,假设存在两个线程threada和threadb,并且handlera绑定了 threada,在threadb中的代码执行到某处时,出于某些原因,我们需要让threada执行某些代码,此时我们就可以使用handler,我们可以在threadb中向handlera中加入某些信息以告知threada中该做某些处理了。由此可以看出,handler是thread的代言人,是多线程之间通信的桥梁,通过handler,我们可以在一个线程中控制另一个线程去做某事。
handler提供了两种方式解决我们在本文一开始遇到的问题(在一个新线程中更新主线程中的ui控件),一种是通过post方法,一种是调用sendmessage方法。
a. 使用post方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
package ispring.com.testhandler; import android.app.activity; import android.os.bundle; import android.os.handler; import android.view.view; import android.widget.button; import android.widget.textview; public class mainactivity extends activity implements button.onclicklistener { private textview statustextview = null ; //uihandler在主线程中创建,所以自动绑定主线程 private handler uihandler = new handler(); @override protected void oncreate(bundle savedinstancestate) { super .oncreate(savedinstancestate); setcontentview(r.layout.activity_main); statustextview = (textview)findviewbyid(r.id.statustextview); button btndownload = (button)findviewbyid(r.id.btndownload); btndownload.setonclicklistener( this ); system.out.println( "main thread id " + thread.currentthread().getid()); } @override public void onclick(view v) { downloadthread downloadthread = new downloadthread(); downloadthread.start(); } class downloadthread extends thread{ @override public void run() { try { system.out.println( "downloadthread id " + thread.currentthread().getid()); system.out.println( "开始下载文件" ); //此处让线程downloadthread休眠5秒中,模拟文件的耗时过程 thread.sleep( 5000 ); system.out.println( "文件下载完成" ); //文件下载完成后更新ui runnable runnable = new runnable() { @override public void run() { system.out.println( "runnable thread id " + thread.currentthread().getid()); mainactivity. this .statustextview.settext( "文件下载完成" ); } }; uihandler.post(runnable); } catch (interruptedexception e){ e.printstacktrace(); } } } } |
我们在activity中创建了一个handler成员变量uihandler,handler有个特点,在执行new handler()的时候,默认情况下handler会绑定当前代码执行的线程,我们在主线程中实例化了uihandler,所以uihandler就自动绑定了主线程,即ui线程。当我们在downloadthread中执行完耗时代码后,我们将一个runnable对象通过post方法传入到了handler中,handler会在合适的时候让主线程执行runnable中的代码,这样runnable就在主线程中执行了,从而正确更新了主线程中的ui。以下是输出结果:
通过输出结果可以看出,runnable中的代码所执行的线程id与downloadthread的线程id不同,而与主线程的线程id相同,因此我们也由此看出在执行了handler.post(runnable)这句代码之后,运行runnable代码的线程与handler所绑定的线程是一致的,而与执行handler.post(runnable)这句代码的线程(downloadthread)无关。
b. 使用sendmessage方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
package ispring.com.testhandler; import android.app.activity; import android.os.bundle; import android.os.handler; import android.os.message; import android.view.view; import android.widget.button; import android.widget.textview; public class mainactivity extends activity implements button.onclicklistener { private textview statustextview = null ; //uihandler在主线程中创建,所以自动绑定主线程 private handler uihandler = new handler(){ @override public void handlemessage(message msg) { switch (msg.what){ case 1 : system.out.println( "handlemessage thread id " + thread.currentthread().getid()); system.out.println( "msg.arg1:" + msg.arg1); system.out.println( "msg.arg2:" + msg.arg2); mainactivity. this .statustextview.settext( "文件下载完成" ); break ; } } }; @override protected void oncreate(bundle savedinstancestate) { super .oncreate(savedinstancestate); setcontentview(r.layout.activity_main); statustextview = (textview)findviewbyid(r.id.statustextview); button btndownload = (button)findviewbyid(r.id.btndownload); btndownload.setonclicklistener( this ); system.out.println( "main thread id " + thread.currentthread().getid()); } @override public void onclick(view v) { downloadthread downloadthread = new downloadthread(); downloadthread.start(); } class downloadthread extends thread{ @override public void run() { try { system.out.println( "downloadthread id " + thread.currentthread().getid()); system.out.println( "开始下载文件" ); //此处让线程downloadthread休眠5秒中,模拟文件的耗时过程 thread.sleep( 5000 ); system.out.println( "文件下载完成" ); //文件下载完成后更新ui message msg = new message(); //虽然message的构造函数式public的,我们也可以通过以下两种方式通过循环对象获取message //msg = message.obtain(uihandler); //msg = uihandler.obtainmessage(); //what是我们自定义的一个message的识别码,以便于在handler的handlemessage方法中根据what识别 //出不同的message,以便我们做出不同的处理操作 msg.what = 1 ; //我们可以通过arg1和arg2给message传入简单的数据 msg.arg1 = 123 ; msg.arg2 = 321 ; //我们也可以通过给obj赋值object类型传递向message传入任意数据 //msg.obj = null; //我们还可以通过setdata方法和getdata方法向message中写入和读取bundle类型的数据 //msg.setdata(null); //bundle data = msg.getdata(); //将该message发送给对应的handler uihandler.sendmessage(msg); } catch (interruptedexception e){ e.printstacktrace(); } } } } |
通过message与handler进行通信的步骤是:
- 1. 重写handler的handlemessage方法,根据message的what值进行不同的处理操作
- 2. 创建message对象
- 虽然message的构造函数式public的,我们还可以通过message.obtain()或handler.obtainmessage()来获得一个message对象(handler.obtainmessage()内部其实调用了message.obtain())。
- 3. 设置message的what值
- message.what是我们自定义的一个message的识别码,以便于在handler的handlemessage方法中根据what识别出不同的message,以便我们做出不同的处理操作。
- 4. 设置message的所携带的数据,简单数据可以通过两个int类型的field arg1和arg2来赋值,并可以在handlemessage中读取。
- 5. 如果message需要携带复杂的数据,那么可以设置message的obj字段,obj是object类型,可以赋予任意类型的数据。或者可以通过调用message的setdata方法赋值bundle类型的数据,可以通过getdata方法获取该bundle数据。
- 6. 我们通过handler.sendmessage(message)方法将message传入handler中让其在handlemessage中对其进行处理。
需要说明的是,如果在handlemessage中 不需要判断message类型,那么就无须设置message的what值;而且让message携带数据也不是必须的,只有在需要的时候才需要让其携带数据;如果确实需要让message携带数据,应该尽量使用arg1或arg2或两者,能用arg1和arg2解决的话就不要用obj,因为用arg1和arg2更高效。
程序的运行结果如下:
由上我们可以看出,执行handlemessage的线程与创建handler的线程是同一线程,在本示例中都是主线程。执行handlemessage的线程与执行uihandler.sendmessage(msg)的线程没有关系。
本文主要是对android中handler的作用于如何使用进行了初步介绍,如果大家想了解handler的内部实现原理,可以参见下一篇博文《深入源码解析android中的handler,message,messagequeue,looper》。