AsyncTask陷阱之:Handler,Looper与MessageQueue的详解
程序员文章站
2023-11-20 18:19:22
asynctask的隐蔽陷阱先来看一个实例这个例子很简单,展示了asynctask的一种极端用法,挺怪的。复制代码 代码如下:public class asynctaskt...
asynctask的隐蔽陷阱
先来看一个实例
这个例子很简单,展示了asynctask的一种极端用法,挺怪的。
public class asynctasktrapactivity extends activity {
private simpleasynctask asynctask;
private looper mylooper;
private textview status;
@override
public void oncreate(bundle icicle) {
super.oncreate(icicle);
asynctask = null;
new thread(new runnable() {
@override
public void run() {
looper.prepare();
mylooper = looper.mylooper();
status = new textview(getapplication());
asynctask = new simpleasynctask(status);
looper.loop();
}
}).start();
try {
thread.sleep(1000);
} catch (interruptedexception e) {
e.printstacktrace();
}
layoutparams params = new layoutparams(layoutparams.fill_parent, layoutparams.fill_parent);
setcontentview((textview) status, params);
asynctask.execute();
}
@override
public void ondestroy() {
super.ondestroy();
mylooper.quit();
}
private class simpleasynctask extends asynctask<void, integer, void> {
private textview mstatuspanel;
public simpleasynctask(textview text) {
mstatuspanel = text;
}
@override
protected void doinbackground(void... params) {
int prog = 1;
while (prog < 101) {
systemclock.sleep(1000);
publishprogress(prog);
prog++;
}
return null;
}
// not okay, will crash, said it cannot touch textview
@override
protected void onpostexecute(void result) {
mstatuspanel.settext("welcome back.");
}
// okay, because it is called in #execute() which is called in main thread, so it runs in main thread.
@override
protected void onpreexecute() {
mstatuspanel.settext("before we go, let me tell you something buried in my heart for years...");
}
// not okay, will crash, said it cannot touch textview
@override
protected void onprogressupdate(integer... values) {
mstatuspanel.settext("on our way..." + values[0].tostring());
}
}
}
11-03 09:13:10.501: e/androidruntime(762): fatal exception: thread-10
11-03 09:13:10.501: e/androidruntime(762): android.view.viewroot$calledfromwrongthreadexception: only the original thread that created a view hierarchy can touch its views.
11-03 09:13:10.501: e/androidruntime(762): at android.view.viewroot.checkthread(viewroot.java:2990)
11-03 09:13:10.501: e/androidruntime(762): at android.view.viewroot.requestlayout(viewroot.java:670)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.checkforrelayout(textview.java:6477)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.settext(textview.java:3220)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.settext(textview.java:3085)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.settext(textview.java:3060)
11-03 09:13:10.501: e/androidruntime(762): at com.hilton.effectiveandroid.os.asynctasktrapactivity$simpleasynctask.onprogressupdate(asynctasktrapactivity.java:110)
11-03 09:13:10.501: e/androidruntime(762): at com.hilton.effectiveandroid.os.asynctasktrapactivity$simpleasynctask.onprogressupdate(asynctasktrapactivity.java:1)
11-03 09:13:10.501: e/androidruntime(762): at android.os.asynctask$internalhandler.handlemessage(asynctask.java:466)
11-03 09:13:10.501: e/androidruntime(762): at android.os.handler.dispatchmessage(handler.java:130)
11-03 09:13:10.501: e/androidruntime(762): at android.os.looper.loop(looper.java:351)
11-03 09:13:10.501: e/androidruntime(762): at com.hilton.effectiveandroid.os.asynctasktrapactivity$1.run(asynctasktrapactivity.java:56)
11-03 09:13:10.501: e/androidruntime(762): at java.lang.thread.run(thread.java:1050)
11-03 09:13:32.823: e/dalvikvm(762): [dvm] mmap return base = 4585e000
但在android4.0及以上的版本中运行就正常(3.0版本未测试)。
从2.3运行时的stacktrace来看原因是在非ui线程中操作了ui组件。不对呀,神奇啊,asynctask#onprogressupdate()和asynctask#onpostexecute()的文档明明写着这二个回调是在ui线程里面的嘛,怎么还会报出这样的异常呢!
原因分析
asynctask设计出来执行异步任务却又能与主线程通讯,它的内部有一个internalhandler是继承自handler的静态成员shandler,这个shandler就是用来与主线程通讯的。看下这个对象的声明:private static final internalhandler shandler = new internalhandler();而internalhandler又是继承自handler的。所以本质上讲shandler就是一个handler对象。handler是用来与线程通讯用的,它必须与looper和线程绑定一起使用,创建handler时必须指定looper,如果不指定looper对象则使用调用栈所在的线程,如果调用栈线程没有looper会报出异常。看来这个shandler是与调用new internalhandler()的线程所绑定,它又是静态私有的,也就是与第一次创建asynctask对象的线程绑定。所以,如果是在主线程中创建的asynctask对象,那么其shandler就与主线程绑定,这是正常的情况。在此例子中asynctask是在衍生线程里创建的,所以其shandler就与衍生线程绑定,因此,它自然不能操作ui元素,会在onprogressupdate()和onpostexecute()中抛出异常。
以上例子有异常的原因就是在衍生线程中创建了simpleasynctask对象。至于为什么在4.0版本上没有问题,是因为4.0中在activitythread.main()方法中,会进行bindapplication的动作,这时会用asynctask对象,也会创建shandler对象,这是主线程所以shandler是与主线程绑定的。后面再创建asynctask对象时,因为shandler已经初始化完了,不会再次初始化。至于什么是bindapplication,为什么会进行bindapplication的动作不影响这个问题的讨论。
asynctask的缺陷及修改方法
这其实是asynctask的隐藏的bug,它不应该这么依赖开发者,应该强加条件限制,以保证第一次asynctask对象是在主线程中创建:
1. 在internalhandler的构造中检查当前线程是否为主线程,然后抛出异常,显然这并不是最佳实践。
new internalhandler() {
if (looper.mylooper() != looper.getmainlooper()) {
throw new runtimeexception("asynctask must be initialized in main thread");
}
11-03 08:56:07.055: e/androidruntime(890): fatal exception: thread-10
11-03 08:56:07.055: e/androidruntime(890): java.lang.exceptionininitializererror
11-03 08:56:07.055: e/androidruntime(890): at com.hilton.effectiveandroid.os.asynctasktrapactivity$1.run(asynctasktrapactivity.java:55)
11-03 08:56:07.055: e/androidruntime(890): at java.lang.thread.run(thread.java:1050)
11-03 08:56:07.055: e/androidruntime(890): caused by: java.lang.runtimeexception: asynctask must be initialized in main thread
11-03 08:56:07.055: e/androidruntime(890): at android.os.asynctask$internalhandler.<init>(asynctask.java:455)
11-03 08:56:07.055: e/androidruntime(890): at android.os.asynctask.<clinit>(asynctask.java:183)
11-03 08:56:07.055: e/androidruntime(890): ... 2 more
2. 更好的做法是在internalhandler构造时把主线程的mainlooper传给
new intenthandler() {
super(looper.getmainlooper());
}
会有人这样写吗,你会问?通常情况是不会的,没有人会故意在衍生线程中创建asynctask。但是假如有一个叫worker的类,用来完成异步任务从网络上下载图片,然后显示,还有一个workerscheduler来分配任务,workerscheduler也是运行在单独线程中,worker用asynctask来实现,workscheduler会在接收到请求时创建worker去完成请求,这时就会出现在workerscheduler线程中---衍生线程---创建asynctask对象。这种bug极其隐蔽,很难发现。
如何限制调用者的线程
正常情况下一个java应用一个进程,且有一个线程,入口即是main方法。安卓应用程序本质上也是java应用程序,它的主入口在activitythread.main(),在main()方法中会调用looper.preparemainlooper(),这就初始化了主线程的looper,且looper中保存有主线程的looper对象mmainlooper,它也提供了方法来获取主线程的looper,getmainlooper()。所以如果需要创建一个与主线程绑定的handler,就可以用new handler(looper.getmainlooper())来保证它确实与主线程绑定。
如果想要保证某些方法仅能在主线程中调用就可以检查调用者的looper对象:
if (looper.mylooper() != looper.getmainlooper()) {
throw new runtimeexception("this method can only be called in main thread");
}
handler,looper,messagequeue机制
线程与线程间的交互协作
线程与线程之间虽然共享内存空间,也即可以访问进程的堆空间,但是线程有自己的栈,运行在一个线程中的方法调用全部都是在线程自己的调用栈中。通俗来讲西线程就是一个run()方法及其内部所调用的方法。这里面的所有方法调用都是独立于其他线程的,由于方法调用的关系,一个方法调用另外的方法,那么另外的方法也发生在调用者的线程里。所以,线程是时序上的概念,本质上是一列方法调用。
那么线程之间要想协作,或者想改变某个方法所在的线程(为了不阻塞自己线程),就只能是向另外一个线程发送一个消息,然后return;另外线程收到消息后就去执行某些操作。如果是简单的操作可以用一个变量来标识,比如a线程主需要b线程做某些事时,可以把某个对象obj设置值,b则当看到obj != null时就去做事,这种线程交互协作在《java编程思想》中有大量示例。
android中的itc-inter thread communication
注意:当然handler也可以用做一个线程内部的消息循环,不必非与另外的线程通信,但这里重点讨论的是线程与线程之间的事情。
android当中做了一个特别的限制就是非主线程不能操作ui元素,而一个应用程序是不可能不创衍生线程的,这样一来主线程与衍生线程之间就必须进行通信。由于这种通信很频繁,所以不可能全用变量来标识,程序将变得十分混乱。这个时候消息队列就变得有十分有必要,也就是在每个线程中建立一个消息队列。当a需要b时,a向b发一个消息,此过程实质为把消息加入到b的消息队列中,a就此return,b并不专门等待某个消息,而是循环的查看其消息队列,看到有消息后就去执行。
整套itc的基本思想是:定义一个消息对象,把需要的数据放入其中,把消息的处理的方法也定义好作为回调放到消息中,然后把这个消息发送另一个线程上;另外的线程在循环处理其队列里的消息,看到消息时就对消息调用附在其上的回调来处理消息。这样一来可以看出,这仅仅是改变了处理消息的执行时序:正常是当场处理,这种则是封装成一个消息丢给另外的线程,在某个不确定的时间被执行;另外的线程也仅提供cpu时序,对于消息是什么和消息如何处理它完全不干预。简言之就是把一个方法放到另外一个线程里去调用,进而这个方法的调用者的调用栈(call stack)结束,这个方法的调用栈转移到了另外的线程中。
那么这个机制改变的到底是什么呢?从上面看它仅是让一个方法(消息的处理)安排到了另外一个线程里去做(异步处理),不是立刻马上同步的做,它改变的是cpu的执行时序(execution sequence)。
那么消息队列存放在哪里呢?不能放在堆空间里(直接new messagequeue()),这样的话对象的引用容易丢失,针对线程来讲也不易维护。java支持线程的本地存储threadlocal,通过threadlocal对象可以把对象放到线程的空间上,每个线程都有了属于自己的对象。因此,可以为每个需要通信的线程创建一个消息队列并放到其本地存储中。
基于这个模型还可以扩展,比如给消息定义优先级等。
messagequeue
以队列的方式来存储消息,主要是二个操作一个是入列enqueuemessage,一个是出列next(),需要保证的是线程安全,因为入列通常是另外的线程在调用。
messagequeue是一个十分接近底层的机制,所以不方便开发者直接使用,要想使用此messagequeue必须做二个方面工作,一个是目标线程端:创建,与线程关联,运转起来;另一个就是队列线程的客户端:创建消息,定义回调处理,发送消息到队列。looper和handler就是对messagequeue的封装:looper是给目标线程用的:用途是创建messagequeue,将messagequeue与线程关联起来,并让messagequeue运转起来,且looper有保护机制,让一个线程仅能创建一个messagequeue对象;而handler则是给队列客户端用的:用来创建消息,定义回调和发送消息。
因为looper对象封装了目标队列线程及其队列,所以对队列线程的客户端来讲,looper对象就代表着一个拥有messagequeue的线程,和这个线程的messagequeue。也即当你构建handler对象时用的是looper对象,而当你检验某个线程是否是预期线程时也用looper对象。
looper内幕
looper的任务是创建消息队列messagequeue,放到线程的threadlocal中(与线程关联),并且让messagequeue运转起来,处于ready的状态,并要提供供接口以停止消息循环。它主要有四个接口:
•public static void looper.prepare()
这个方法是为线程创建一个looper对象和messagequeue对象,并把looper对象通过threadlocal放到线程空间里去。需要注意的是这个方法每个线程只能调用一次,通常的做法是在线程run()方法的第一句,但只要保证在loop()前面即可。
•public static void looper.loop()
这个方法要在prepare()这后调用,是让线程的messagequeue运转起来,一旦调用此方法,线程便会无限循环下去(while (true){...}),无message时休眠,有message入队时唤醒处理,直到quit()调用为止。它的简化实现就是:
loop() {
while (true) {
message msg = mqueue.next();
if msg is a quit message, then
return;
msg.processmessage(msg)
}
}
•public void looper.quit()
让线程结束messagequeue的循环,终止循环,run()方法会结束,线程也会停止,因此它是对象的方法,意即终止某个looper对象。一定要记得在不需要线程的时候调用此方法,否则线程是不会终止退出的,进程也就会一直运行,占用着资源。如果有大量的线程未退出,进程最终会崩掉。
•public static looper looper.mylooper()
这个是获得调用者所在线程所拥有的looper对象的方法。
还有二个接口是与主线程有关的:
•一个是专门为主线程准备的
public static void looper.preparemainlooper();
这个方法只给主线程初始化looper用的,它仅在activitythread.main()方法中调用,其他地方或其他线程不可以调用,如果在主线程中调用会有异常抛出,因为一个线程只能创建一个looper对象。但是如在其他线程中调用此方法,会改变mainlooper,接下来的getmainlooper就会返回它而非真正的主线程的looper对象,这不会有异常抛出,也不会有明显的错误,但是程序将不能正常工作,因为原本设计在主线程中运行的方法将转到这个线程里面,会产生很诡异的bug。这里looper.preparemainthread()的方法中应该加上判断:
public void preparemainlooper() {
if (getmainlooper() != null) {
throw new runtimeexception("looper.preparemainthread() can only be called by frameworks");
}
//...
}
以防止其他线程非法调用,光靠文档约束力远不够。
•另外一个就是获取主线程looper的接口:
public static looper looper.getmainlooper()
这个主要用在检查线程合法性,也即保证某些方法只能在主线程里面调用。但这并不保险,如上面所说,如果一个衍生线程调用了preparemainlooper()就会把真正的mmainlooper改变,此衍生线程就可以通过上述检测,导致getmainlooper() != mylooper()的检测变得不靠谱了。所以viewroot的方法是用thread来检测:mthread != thread.currentthread();其mthread是在系统创建viewroot时通过thread.currentthread()获得的,这样的方法来检测是否是主线程更加靠谱一些,因为它没有依赖外部而是相信自己保存的thread的引用。
message对象
消息message是仅是一个数据结构,是信息的载体,它与队列机制是无关的,封装着要执行的动作和执行动作的必要信息,what, arg1, arg2, obj可以用来传送数据;而message的回调则必须通过handler来定义,为什么呢?因为message仅是一个载体,它不能自己跑到目标messagequeue上面去,它必须由handler来操作,把message放到目标队列上去,既然它需要handler来统一的放到messagequeue上,也可以让handler来统一定义处理消息的回调。需要注意的是同一个message对象只能使用一次,因为在处理完消息后会把消息回收掉,所以message对象仅能使用一次,尝试再次使用时messagequeue会抛出异常。
handler对象
它被设计出来目的就是方便队列线程客户端的操作,隐藏直接操作messagequeue的复杂性。handler最主要的作用是把消息发送到与此handler绑定的线程的messagequeue上,因此在构建handler的时候必须指定一个looper对象,如果不指定则通过looper获取调用者线程的looper对象。它有很多重载的send*message和post方法,可以以多种方式来向目标队列发送消息,廷时发送,或者放到队列的头部等等;
它还有二个作用,一个是创建message对象通过obtain*系统方法,另一个就是定义处理message的回调mcallback和handlemessage,由于一个handler可能不止发送一个消息,而这些消息通常共享此handler的回调方法,所以在handlemessage或者mcallback中就要区分这些不同的消息,通常是以message.what来区分,当然也可以用其他字段,只要能区别出不同的message即可。需要指明的是,消息队列中的消息本身是独立的,互不相干的,消息的命名空间是在handler对象之中的,因为message是由handler发送和处理的,所以只有同一个handler对象需要区别不同的message对象。广义上讲,如果一个消息自己定义有处理方法,那么所有的消息都是互不相干的,当从队列取出消息时就调用其上的回调方法,不会有命名上的冲突,但由handler发出的消息的回调处理方法都是handler.handlemessage或handler.mcallback,所以就会有影响了,但影响的范围也令局限在同一个handler对象。
因为handler的作用是向目标队列发送消息和定义处理消息的回调(处理消息),它仅是依赖于线程的messagequeue,所以handler可以有任意多个,都绑定到某个messagequeue上,它并没有个数限制。而messagequeue是有个数限制的,每个线程只能有一个,messagequeue通过looper创建,looper存储在线程的threadlocal中,looper里作了限制,每个线程只能创建一个。但是handler无此限制,handler的创建通过其构造函数,只需要提供一个looper对象即可,所以它没有个数限制。
先来看一个实例
这个例子很简单,展示了asynctask的一种极端用法,挺怪的。
复制代码 代码如下:
public class asynctasktrapactivity extends activity {
private simpleasynctask asynctask;
private looper mylooper;
private textview status;
@override
public void oncreate(bundle icicle) {
super.oncreate(icicle);
asynctask = null;
new thread(new runnable() {
@override
public void run() {
looper.prepare();
mylooper = looper.mylooper();
status = new textview(getapplication());
asynctask = new simpleasynctask(status);
looper.loop();
}
}).start();
try {
thread.sleep(1000);
} catch (interruptedexception e) {
e.printstacktrace();
}
layoutparams params = new layoutparams(layoutparams.fill_parent, layoutparams.fill_parent);
setcontentview((textview) status, params);
asynctask.execute();
}
@override
public void ondestroy() {
super.ondestroy();
mylooper.quit();
}
private class simpleasynctask extends asynctask<void, integer, void> {
private textview mstatuspanel;
public simpleasynctask(textview text) {
mstatuspanel = text;
}
@override
protected void doinbackground(void... params) {
int prog = 1;
while (prog < 101) {
systemclock.sleep(1000);
publishprogress(prog);
prog++;
}
return null;
}
// not okay, will crash, said it cannot touch textview
@override
protected void onpostexecute(void result) {
mstatuspanel.settext("welcome back.");
}
// okay, because it is called in #execute() which is called in main thread, so it runs in main thread.
@override
protected void onpreexecute() {
mstatuspanel.settext("before we go, let me tell you something buried in my heart for years...");
}
// not okay, will crash, said it cannot touch textview
@override
protected void onprogressupdate(integer... values) {
mstatuspanel.settext("on our way..." + values[0].tostring());
}
}
}
这个例子在android2.3中无法正常运行,在执行onprogressupdate()和onpostexecute()时会报出异常
复制代码 代码如下:
11-03 09:13:10.501: e/androidruntime(762): fatal exception: thread-10
11-03 09:13:10.501: e/androidruntime(762): android.view.viewroot$calledfromwrongthreadexception: only the original thread that created a view hierarchy can touch its views.
11-03 09:13:10.501: e/androidruntime(762): at android.view.viewroot.checkthread(viewroot.java:2990)
11-03 09:13:10.501: e/androidruntime(762): at android.view.viewroot.requestlayout(viewroot.java:670)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.view.view.requestlayout(view.java:8316)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.checkforrelayout(textview.java:6477)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.settext(textview.java:3220)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.settext(textview.java:3085)
11-03 09:13:10.501: e/androidruntime(762): at android.widget.textview.settext(textview.java:3060)
11-03 09:13:10.501: e/androidruntime(762): at com.hilton.effectiveandroid.os.asynctasktrapactivity$simpleasynctask.onprogressupdate(asynctasktrapactivity.java:110)
11-03 09:13:10.501: e/androidruntime(762): at com.hilton.effectiveandroid.os.asynctasktrapactivity$simpleasynctask.onprogressupdate(asynctasktrapactivity.java:1)
11-03 09:13:10.501: e/androidruntime(762): at android.os.asynctask$internalhandler.handlemessage(asynctask.java:466)
11-03 09:13:10.501: e/androidruntime(762): at android.os.handler.dispatchmessage(handler.java:130)
11-03 09:13:10.501: e/androidruntime(762): at android.os.looper.loop(looper.java:351)
11-03 09:13:10.501: e/androidruntime(762): at com.hilton.effectiveandroid.os.asynctasktrapactivity$1.run(asynctasktrapactivity.java:56)
11-03 09:13:10.501: e/androidruntime(762): at java.lang.thread.run(thread.java:1050)
11-03 09:13:32.823: e/dalvikvm(762): [dvm] mmap return base = 4585e000
但在android4.0及以上的版本中运行就正常(3.0版本未测试)。
从2.3运行时的stacktrace来看原因是在非ui线程中操作了ui组件。不对呀,神奇啊,asynctask#onprogressupdate()和asynctask#onpostexecute()的文档明明写着这二个回调是在ui线程里面的嘛,怎么还会报出这样的异常呢!
原因分析
asynctask设计出来执行异步任务却又能与主线程通讯,它的内部有一个internalhandler是继承自handler的静态成员shandler,这个shandler就是用来与主线程通讯的。看下这个对象的声明:private static final internalhandler shandler = new internalhandler();而internalhandler又是继承自handler的。所以本质上讲shandler就是一个handler对象。handler是用来与线程通讯用的,它必须与looper和线程绑定一起使用,创建handler时必须指定looper,如果不指定looper对象则使用调用栈所在的线程,如果调用栈线程没有looper会报出异常。看来这个shandler是与调用new internalhandler()的线程所绑定,它又是静态私有的,也就是与第一次创建asynctask对象的线程绑定。所以,如果是在主线程中创建的asynctask对象,那么其shandler就与主线程绑定,这是正常的情况。在此例子中asynctask是在衍生线程里创建的,所以其shandler就与衍生线程绑定,因此,它自然不能操作ui元素,会在onprogressupdate()和onpostexecute()中抛出异常。
以上例子有异常的原因就是在衍生线程中创建了simpleasynctask对象。至于为什么在4.0版本上没有问题,是因为4.0中在activitythread.main()方法中,会进行bindapplication的动作,这时会用asynctask对象,也会创建shandler对象,这是主线程所以shandler是与主线程绑定的。后面再创建asynctask对象时,因为shandler已经初始化完了,不会再次初始化。至于什么是bindapplication,为什么会进行bindapplication的动作不影响这个问题的讨论。
asynctask的缺陷及修改方法
这其实是asynctask的隐藏的bug,它不应该这么依赖开发者,应该强加条件限制,以保证第一次asynctask对象是在主线程中创建:
1. 在internalhandler的构造中检查当前线程是否为主线程,然后抛出异常,显然这并不是最佳实践。
复制代码 代码如下:
new internalhandler() {
复制代码 代码如下:
if (looper.mylooper() != looper.getmainlooper()) {
throw new runtimeexception("asynctask must be initialized in main thread");
}
复制代码 代码如下:
11-03 08:56:07.055: e/androidruntime(890): fatal exception: thread-10
11-03 08:56:07.055: e/androidruntime(890): java.lang.exceptionininitializererror
11-03 08:56:07.055: e/androidruntime(890): at com.hilton.effectiveandroid.os.asynctasktrapactivity$1.run(asynctasktrapactivity.java:55)
11-03 08:56:07.055: e/androidruntime(890): at java.lang.thread.run(thread.java:1050)
11-03 08:56:07.055: e/androidruntime(890): caused by: java.lang.runtimeexception: asynctask must be initialized in main thread
11-03 08:56:07.055: e/androidruntime(890): at android.os.asynctask$internalhandler.<init>(asynctask.java:455)
11-03 08:56:07.055: e/androidruntime(890): at android.os.asynctask.<clinit>(asynctask.java:183)
11-03 08:56:07.055: e/androidruntime(890): ... 2 more
2. 更好的做法是在internalhandler构造时把主线程的mainlooper传给
复制代码 代码如下:
new intenthandler() {
super(looper.getmainlooper());
}
会有人这样写吗,你会问?通常情况是不会的,没有人会故意在衍生线程中创建asynctask。但是假如有一个叫worker的类,用来完成异步任务从网络上下载图片,然后显示,还有一个workerscheduler来分配任务,workerscheduler也是运行在单独线程中,worker用asynctask来实现,workscheduler会在接收到请求时创建worker去完成请求,这时就会出现在workerscheduler线程中---衍生线程---创建asynctask对象。这种bug极其隐蔽,很难发现。
如何限制调用者的线程
正常情况下一个java应用一个进程,且有一个线程,入口即是main方法。安卓应用程序本质上也是java应用程序,它的主入口在activitythread.main(),在main()方法中会调用looper.preparemainlooper(),这就初始化了主线程的looper,且looper中保存有主线程的looper对象mmainlooper,它也提供了方法来获取主线程的looper,getmainlooper()。所以如果需要创建一个与主线程绑定的handler,就可以用new handler(looper.getmainlooper())来保证它确实与主线程绑定。
如果想要保证某些方法仅能在主线程中调用就可以检查调用者的looper对象:
复制代码 代码如下:
if (looper.mylooper() != looper.getmainlooper()) {
throw new runtimeexception("this method can only be called in main thread");
}
handler,looper,messagequeue机制
线程与线程间的交互协作
线程与线程之间虽然共享内存空间,也即可以访问进程的堆空间,但是线程有自己的栈,运行在一个线程中的方法调用全部都是在线程自己的调用栈中。通俗来讲西线程就是一个run()方法及其内部所调用的方法。这里面的所有方法调用都是独立于其他线程的,由于方法调用的关系,一个方法调用另外的方法,那么另外的方法也发生在调用者的线程里。所以,线程是时序上的概念,本质上是一列方法调用。
那么线程之间要想协作,或者想改变某个方法所在的线程(为了不阻塞自己线程),就只能是向另外一个线程发送一个消息,然后return;另外线程收到消息后就去执行某些操作。如果是简单的操作可以用一个变量来标识,比如a线程主需要b线程做某些事时,可以把某个对象obj设置值,b则当看到obj != null时就去做事,这种线程交互协作在《java编程思想》中有大量示例。
android中的itc-inter thread communication
注意:当然handler也可以用做一个线程内部的消息循环,不必非与另外的线程通信,但这里重点讨论的是线程与线程之间的事情。
android当中做了一个特别的限制就是非主线程不能操作ui元素,而一个应用程序是不可能不创衍生线程的,这样一来主线程与衍生线程之间就必须进行通信。由于这种通信很频繁,所以不可能全用变量来标识,程序将变得十分混乱。这个时候消息队列就变得有十分有必要,也就是在每个线程中建立一个消息队列。当a需要b时,a向b发一个消息,此过程实质为把消息加入到b的消息队列中,a就此return,b并不专门等待某个消息,而是循环的查看其消息队列,看到有消息后就去执行。
整套itc的基本思想是:定义一个消息对象,把需要的数据放入其中,把消息的处理的方法也定义好作为回调放到消息中,然后把这个消息发送另一个线程上;另外的线程在循环处理其队列里的消息,看到消息时就对消息调用附在其上的回调来处理消息。这样一来可以看出,这仅仅是改变了处理消息的执行时序:正常是当场处理,这种则是封装成一个消息丢给另外的线程,在某个不确定的时间被执行;另外的线程也仅提供cpu时序,对于消息是什么和消息如何处理它完全不干预。简言之就是把一个方法放到另外一个线程里去调用,进而这个方法的调用者的调用栈(call stack)结束,这个方法的调用栈转移到了另外的线程中。
那么这个机制改变的到底是什么呢?从上面看它仅是让一个方法(消息的处理)安排到了另外一个线程里去做(异步处理),不是立刻马上同步的做,它改变的是cpu的执行时序(execution sequence)。
那么消息队列存放在哪里呢?不能放在堆空间里(直接new messagequeue()),这样的话对象的引用容易丢失,针对线程来讲也不易维护。java支持线程的本地存储threadlocal,通过threadlocal对象可以把对象放到线程的空间上,每个线程都有了属于自己的对象。因此,可以为每个需要通信的线程创建一个消息队列并放到其本地存储中。
基于这个模型还可以扩展,比如给消息定义优先级等。
messagequeue
以队列的方式来存储消息,主要是二个操作一个是入列enqueuemessage,一个是出列next(),需要保证的是线程安全,因为入列通常是另外的线程在调用。
messagequeue是一个十分接近底层的机制,所以不方便开发者直接使用,要想使用此messagequeue必须做二个方面工作,一个是目标线程端:创建,与线程关联,运转起来;另一个就是队列线程的客户端:创建消息,定义回调处理,发送消息到队列。looper和handler就是对messagequeue的封装:looper是给目标线程用的:用途是创建messagequeue,将messagequeue与线程关联起来,并让messagequeue运转起来,且looper有保护机制,让一个线程仅能创建一个messagequeue对象;而handler则是给队列客户端用的:用来创建消息,定义回调和发送消息。
因为looper对象封装了目标队列线程及其队列,所以对队列线程的客户端来讲,looper对象就代表着一个拥有messagequeue的线程,和这个线程的messagequeue。也即当你构建handler对象时用的是looper对象,而当你检验某个线程是否是预期线程时也用looper对象。
looper内幕
looper的任务是创建消息队列messagequeue,放到线程的threadlocal中(与线程关联),并且让messagequeue运转起来,处于ready的状态,并要提供供接口以停止消息循环。它主要有四个接口:
•public static void looper.prepare()
这个方法是为线程创建一个looper对象和messagequeue对象,并把looper对象通过threadlocal放到线程空间里去。需要注意的是这个方法每个线程只能调用一次,通常的做法是在线程run()方法的第一句,但只要保证在loop()前面即可。
•public static void looper.loop()
这个方法要在prepare()这后调用,是让线程的messagequeue运转起来,一旦调用此方法,线程便会无限循环下去(while (true){...}),无message时休眠,有message入队时唤醒处理,直到quit()调用为止。它的简化实现就是:
复制代码 代码如下:
loop() {
while (true) {
message msg = mqueue.next();
if msg is a quit message, then
return;
msg.processmessage(msg)
}
}
•public void looper.quit()
让线程结束messagequeue的循环,终止循环,run()方法会结束,线程也会停止,因此它是对象的方法,意即终止某个looper对象。一定要记得在不需要线程的时候调用此方法,否则线程是不会终止退出的,进程也就会一直运行,占用着资源。如果有大量的线程未退出,进程最终会崩掉。
•public static looper looper.mylooper()
这个是获得调用者所在线程所拥有的looper对象的方法。
还有二个接口是与主线程有关的:
•一个是专门为主线程准备的
public static void looper.preparemainlooper();
这个方法只给主线程初始化looper用的,它仅在activitythread.main()方法中调用,其他地方或其他线程不可以调用,如果在主线程中调用会有异常抛出,因为一个线程只能创建一个looper对象。但是如在其他线程中调用此方法,会改变mainlooper,接下来的getmainlooper就会返回它而非真正的主线程的looper对象,这不会有异常抛出,也不会有明显的错误,但是程序将不能正常工作,因为原本设计在主线程中运行的方法将转到这个线程里面,会产生很诡异的bug。这里looper.preparemainthread()的方法中应该加上判断:
复制代码 代码如下:
public void preparemainlooper() {
if (getmainlooper() != null) {
throw new runtimeexception("looper.preparemainthread() can only be called by frameworks");
}
//...
}
以防止其他线程非法调用,光靠文档约束力远不够。
•另外一个就是获取主线程looper的接口:
public static looper looper.getmainlooper()
这个主要用在检查线程合法性,也即保证某些方法只能在主线程里面调用。但这并不保险,如上面所说,如果一个衍生线程调用了preparemainlooper()就会把真正的mmainlooper改变,此衍生线程就可以通过上述检测,导致getmainlooper() != mylooper()的检测变得不靠谱了。所以viewroot的方法是用thread来检测:mthread != thread.currentthread();其mthread是在系统创建viewroot时通过thread.currentthread()获得的,这样的方法来检测是否是主线程更加靠谱一些,因为它没有依赖外部而是相信自己保存的thread的引用。
message对象
消息message是仅是一个数据结构,是信息的载体,它与队列机制是无关的,封装着要执行的动作和执行动作的必要信息,what, arg1, arg2, obj可以用来传送数据;而message的回调则必须通过handler来定义,为什么呢?因为message仅是一个载体,它不能自己跑到目标messagequeue上面去,它必须由handler来操作,把message放到目标队列上去,既然它需要handler来统一的放到messagequeue上,也可以让handler来统一定义处理消息的回调。需要注意的是同一个message对象只能使用一次,因为在处理完消息后会把消息回收掉,所以message对象仅能使用一次,尝试再次使用时messagequeue会抛出异常。
handler对象
它被设计出来目的就是方便队列线程客户端的操作,隐藏直接操作messagequeue的复杂性。handler最主要的作用是把消息发送到与此handler绑定的线程的messagequeue上,因此在构建handler的时候必须指定一个looper对象,如果不指定则通过looper获取调用者线程的looper对象。它有很多重载的send*message和post方法,可以以多种方式来向目标队列发送消息,廷时发送,或者放到队列的头部等等;
它还有二个作用,一个是创建message对象通过obtain*系统方法,另一个就是定义处理message的回调mcallback和handlemessage,由于一个handler可能不止发送一个消息,而这些消息通常共享此handler的回调方法,所以在handlemessage或者mcallback中就要区分这些不同的消息,通常是以message.what来区分,当然也可以用其他字段,只要能区别出不同的message即可。需要指明的是,消息队列中的消息本身是独立的,互不相干的,消息的命名空间是在handler对象之中的,因为message是由handler发送和处理的,所以只有同一个handler对象需要区别不同的message对象。广义上讲,如果一个消息自己定义有处理方法,那么所有的消息都是互不相干的,当从队列取出消息时就调用其上的回调方法,不会有命名上的冲突,但由handler发出的消息的回调处理方法都是handler.handlemessage或handler.mcallback,所以就会有影响了,但影响的范围也令局限在同一个handler对象。
因为handler的作用是向目标队列发送消息和定义处理消息的回调(处理消息),它仅是依赖于线程的messagequeue,所以handler可以有任意多个,都绑定到某个messagequeue上,它并没有个数限制。而messagequeue是有个数限制的,每个线程只能有一个,messagequeue通过looper创建,looper存储在线程的threadlocal中,looper里作了限制,每个线程只能创建一个。但是handler无此限制,handler的创建通过其构造函数,只需要提供一个looper对象即可,所以它没有个数限制。
推荐阅读
-
AsyncTask陷阱之:Handler,Looper与MessageQueue的详解
-
Android开发笔记之:消息循环与Looper的详解
-
Android开发笔记之:Handler Runnable与Thread的区别详解
-
Android消息通信机制Handler详解,Handler,Looper,MessageQueue,源码解析,讲解这几个类怎么配合工作的
-
Android中的消息系统————Handler,MessageQueue与Looper
-
Android的线程通信:消息机制原理(Message,Handler,MessageQueue,Looper),异步任务AsyncTask,使用JSON
-
Android之Handler与AsyncTask的那些事