欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

AsyncTask陷阱之:Handler,Looper与MessageQueue的详解

程序员文章站 2023-12-09 23:04:45
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());
        }
    }
}

这个例子在android2.3中无法正常运行,在执行onprogressupdate()和onpostexecute()时会报出异常

AsyncTask陷阱之:Handler,Looper与MessageQueue的详解

复制代码 代码如下:

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版本未测试)。

AsyncTask陷阱之:Handler,Looper与MessageQueue的详解

从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对象可以把对象放到线程的空间上,每个线程都有了属于自己的对象。因此,可以为每个需要通信的线程创建一个消息队列并放到其本地存储中。
基于这个模型还可以扩展,比如给消息定义优先级等。

AsyncTask陷阱之:Handler,Looper与MessageQueue的详解

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对象即可,所以它没有个数限制。