AsyncTask陷阱之:Handler,Looper与MessageQueue的详解
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()时会报出异常
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对象即可,所以它没有个数限制。