AsyncTask的隱蔽陷阱
先來看一個(gè)實(shí)例
這個(gè)例子很簡單,展示了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());
}
}
}
這個(gè)例子在Android2.3中無法正常運(yùn)行,在執(zhí)行onProgressUpdate()和onPostExecute()時(shí)會(huì)報(bào)出異常

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及以上的版本中運(yùn)行就正常(3.0版本未測試)。

從2.3運(yùn)行時(shí)的Stacktrace來看原因是在非UI線程中操作了UI組件。不對呀,神奇啊,AsyncTask#onProgressUpdate()和AsyncTask#onPostExecute()的文檔明明寫著這二個(gè)回調(diào)是在UI線程里面的嘛,怎么還會(huì)報(bào)出這樣的異常呢!
原因分析
AsyncTask設(shè)計(jì)出來執(zhí)行異步任務(wù)卻又能與主線程通訊,它的內(nèi)部有一個(gè)InternalHandler是繼承自Handler的靜態(tài)成員sHandler,這個(gè)sHandler就是用來與主線程通訊的。看下這個(gè)對象的聲明:private static final InternalHandler sHandler = new InternalHandler();而InternalHandler又是繼承自Handler的。所以本質(zhì)上講sHandler就是一個(gè)Handler對象。Handler是用來與線程通訊用的,它必須與Looper和線程綁定一起使用,創(chuàng)建Handler時(shí)必須指定Looper,如果不指定Looper對象則使用調(diào)用棧所在的線程,如果調(diào)用棧線程沒有Looper會(huì)報(bào)出異常??磥磉@個(gè)sHandler是與調(diào)用new InternalHandler()的線程所綁定,它又是靜態(tài)私有的,也就是與第一次創(chuàng)建AsyncTask對象的線程綁定。所以,如果是在主線程中創(chuàng)建的AsyncTask對象,那么其sHandler就與主線程綁定,這是正常的情況。在此例子中AsyncTask是在衍生線程里創(chuàng)建的,所以其sHandler就與衍生線程綁定,因此,它自然不能操作UI元素,會(huì)在onProgressUpdate()和onPostExecute()中拋出異常。
以上例子有異常的原因就是在衍生線程中創(chuàng)建了SimpleAsyncTask對象。至于為什么在4.0版本上沒有問題,是因?yàn)?.0中在ActivityThread.main()方法中,會(huì)進(jìn)行BindApplication的動(dòng)作,這時(shí)會(huì)用AsyncTask對象,也會(huì)創(chuàng)建sHandler對象,這是主線程所以sHandler是與主線程綁定的。后面再創(chuàng)建AsyncTask對象時(shí),因?yàn)閟Handler已經(jīng)初始化完了,不會(huì)再次初始化。至于什么是BindApplication,為什么會(huì)進(jìn)行BindApplication的動(dòng)作不影響這個(gè)問題的討論。
AsyncTask的缺陷及修改方法
這其實(shí)是AsyncTask的隱藏的Bug,它不應(yīng)該這么依賴開發(fā)者,應(yīng)該強(qiáng)加條件限制,以保證第一次AsyncTask對象是在主線程中創(chuàng)建:
1. 在InternalHandler的構(gòu)造中檢查當(dāng)前線程是否為主線程,然后拋出異常,顯然這并不是最佳實(shí)踐。
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構(gòu)造時(shí)把主線程的MainLooper傳給
new IntentHandler() {
super(Looper.getMainLooper());
}
會(huì)有人這樣寫嗎,你會(huì)問?通常情況是不會(huì)的,沒有人會(huì)故意在衍生線程中創(chuàng)建AsyncTask。但是假如有一個(gè)叫Worker的類,用來完成異步任務(wù)從網(wǎng)絡(luò)上下載圖片,然后顯示,還有一個(gè)WorkerScheduler來分配任務(wù),WorkerScheduler也是運(yùn)行在單獨(dú)線程中,Worker用AsyncTask來實(shí)現(xiàn),WorkScheduler會(huì)在接收到請求時(shí)創(chuàng)建Worker去完成請求,這時(shí)就會(huì)出現(xiàn)在WorkerScheduler線程中---衍生線程---創(chuàng)建AsyncTask對象。這種Bug極其隱蔽,很難發(fā)現(xiàn)。
如何限制調(diào)用者的線程
正常情況下一個(gè)Java應(yīng)用一個(gè)進(jìn)程,且有一個(gè)線程,入口即是main方法。安卓應(yīng)用程序本質(zhì)上也是Java應(yīng)用程序,它的主入口在ActivityThread.main(),在main()方法中會(huì)調(diào)用Looper.prepareMainLooper(),這就初始化了主線程的Looper,且Looper中保存有主線程的Looper對象mMainLooper,它也提供了方法來獲取主線程的Looper,getMainLooper()。所以如果需要?jiǎng)?chuàng)建一個(gè)與主線程綁定的Handler,就可以用new Handler(Looper.getMainLooper())來保證它確實(shí)與主線程綁定。
如果想要保證某些方法僅能在主線程中調(diào)用就可以檢查調(diào)用者的Looper對象:
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new RuntimeException("This method can only be called in main thread");
}
Handler,Looper,MessageQueue機(jī)制
線程與線程間的交互協(xié)作
線程與線程之間雖然共享內(nèi)存空間,也即可以訪問進(jìn)程的堆空間,但是線程有自己的棧,運(yùn)行在一個(gè)線程中的方法調(diào)用全部都是在線程自己的調(diào)用棧中。通俗來講西線程就是一個(gè)run()方法及其內(nèi)部所調(diào)用的方法。這里面的所有方法調(diào)用都是獨(dú)立于其他線程的,由于方法調(diào)用的關(guān)系,一個(gè)方法調(diào)用另外的方法,那么另外的方法也發(fā)生在調(diào)用者的線程里。所以,線程是時(shí)序上的概念,本質(zhì)上是一列方法調(diào)用。
那么線程之間要想?yún)f(xié)作,或者想改變某個(gè)方法所在的線程(為了不阻塞自己線程),就只能是向另外一個(gè)線程發(fā)送一個(gè)消息,然后return;另外線程收到消息后就去執(zhí)行某些操作。如果是簡單的操作可以用一個(gè)變量來標(biāo)識,比如A線程主需要B線程做某些事時(shí),可以把某個(gè)對象obj設(shè)置值,B則當(dāng)看到obj != null時(shí)就去做事,這種線程交互協(xié)作在《Java編程思想》中有大量示例。
Android中的ITC-Inter Thread Communication
注意:當(dāng)然Handler也可以用做一個(gè)線程內(nèi)部的消息循環(huán),不必非與另外的線程通信,但這里重點(diǎn)討論的是線程與線程之間的事情。
Android當(dāng)中做了一個(gè)特別的限制就是非主線程不能操作UI元素,而一個(gè)應(yīng)用程序是不可能不創(chuàng)衍生線程的,這樣一來主線程與衍生線程之間就必須進(jìn)行通信。由于這種通信很頻繁,所以不可能全用變量來標(biāo)識,程序?qū)⒆兊檬只靵y。這個(gè)時(shí)候消息隊(duì)列就變得有十分有必要,也就是在每個(gè)線程中建立一個(gè)消息隊(duì)列。當(dāng)A需要B時(shí),A向B發(fā)一個(gè)消息,此過程實(shí)質(zhì)為把消息加入到B的消息隊(duì)列中,A就此return,B并不專門等待某個(gè)消息,而是循環(huán)的查看其消息隊(duì)列,看到有消息后就去執(zhí)行。
整套ITC的基本思想是:定義一個(gè)消息對象,把需要的數(shù)據(jù)放入其中,把消息的處理的方法也定義好作為回調(diào)放到消息中,然后把這個(gè)消息發(fā)送另一個(gè)線程上;另外的線程在循環(huán)處理其隊(duì)列里的消息,看到消息時(shí)就對消息調(diào)用附在其上的回調(diào)來處理消息。這樣一來可以看出,這僅僅是改變了處理消息的執(zhí)行時(shí)序:正常是當(dāng)場處理,這種則是封裝成一個(gè)消息丟給另外的線程,在某個(gè)不確定的時(shí)間被執(zhí)行;另外的線程也僅提供CPU時(shí)序,對于消息是什么和消息如何處理它完全不干預(yù)。簡言之就是把一個(gè)方法放到另外一個(gè)線程里去調(diào)用,進(jìn)而這個(gè)方法的調(diào)用者的調(diào)用棧(call stack)結(jié)束,這個(gè)方法的調(diào)用棧轉(zhuǎn)移到了另外的線程中。
那么這個(gè)機(jī)制改變的到底是什么呢?從上面看它僅是讓一個(gè)方法(消息的處理)安排到了另外一個(gè)線程里去做(異步處理),不是立刻馬上同步的做,它改變的是CPU的執(zhí)行時(shí)序(execution sequence)。
那么消息隊(duì)列存放在哪里呢?不能放在堆空間里(直接new MessageQueue()),這樣的話對象的引用容易丟失,針對線程來講也不易維護(hù)。Java支持線程的本地存儲(chǔ)ThreadLocal,通過ThreadLocal對象可以把對象放到線程的空間上,每個(gè)線程都有了屬于自己的對象。因此,可以為每個(gè)需要通信的線程創(chuàng)建一個(gè)消息隊(duì)列并放到其本地存儲(chǔ)中。
基于這個(gè)模型還可以擴(kuò)展,比如給消息定義優(yōu)先級等。

MessageQueue
以隊(duì)列的方式來存儲(chǔ)消息,主要是二個(gè)操作一個(gè)是入列enqueueMessage,一個(gè)是出列next(),需要保證的是線程安全,因?yàn)槿肓型ǔJ橇硗獾木€程在調(diào)用。
MessageQueue是一個(gè)十分接近底層的機(jī)制,所以不方便開發(fā)者直接使用,要想使用此MessageQueue必須做二個(gè)方面工作,一個(gè)是目標(biāo)線程端:創(chuàng)建,與線程關(guān)聯(lián),運(yùn)轉(zhuǎn)起來;另一個(gè)就是隊(duì)列線程的客戶端:創(chuàng)建消息,定義回調(diào)處理,發(fā)送消息到隊(duì)列。Looper和Handler就是對MessageQueue的封裝:Looper是給目標(biāo)線程用的:用途是創(chuàng)建MessageQueue,將MessageQueue與線程關(guān)聯(lián)起來,并讓MessageQueue運(yùn)轉(zhuǎn)起來,且Looper有保護(hù)機(jī)制,讓一個(gè)線程僅能創(chuàng)建一個(gè)MessageQueue對象;而Handler則是給隊(duì)列客戶端用的:用來創(chuàng)建消息,定義回調(diào)和發(fā)送消息。
因?yàn)長ooper對象封裝了目標(biāo)隊(duì)列線程及其隊(duì)列,所以對隊(duì)列線程的客戶端來講,Looper對象就代表著一個(gè)擁有MessageQueue的線程,和這個(gè)線程的MessageQueue。也即當(dāng)你構(gòu)建Handler對象時(shí)用的是Looper對象,而當(dāng)你檢驗(yàn)?zāi)硞€(gè)線程是否是預(yù)期線程時(shí)也用Looper對象。
Looper內(nèi)幕
Looper的任務(wù)是創(chuàng)建消息隊(duì)列MessageQueue,放到線程的ThreadLocal中(與線程關(guān)聯(lián)),并且讓MessageQueue運(yùn)轉(zhuǎn)起來,處于Ready的狀態(tài),并要提供供接口以停止消息循環(huán)。它主要有四個(gè)接口:
•public static void Looper.prepare()
這個(gè)方法是為線程創(chuàng)建一個(gè)Looper對象和MessageQueue對象,并把Looper對象通過ThreadLocal放到線程空間里去。需要注意的是這個(gè)方法每個(gè)線程只能調(diào)用一次,通常的做法是在線程run()方法的第一句,但只要保證在loop()前面即可。
•public static void Looper.loop()
這個(gè)方法要在prepare()這后調(diào)用,是讓線程的MessageQueue運(yùn)轉(zhuǎn)起來,一旦調(diào)用此方法,線程便會(huì)無限循環(huán)下去(while (true){...}),無Message時(shí)休眠,有Message入隊(duì)時(shí)喚醒處理,直到quit()調(diào)用為止。它的簡化實(shí)現(xiàn)就是:
loop() {
while (true) {
Message msg = mQueue.next();
if msg is a quit message, then
return;
msg.processMessage(msg)
}
}
•public void Looper.quit()
讓線程結(jié)束MessageQueue的循環(huán),終止循環(huán),run()方法會(huì)結(jié)束,線程也會(huì)停止,因此它是對象的方法,意即終止某個(gè)Looper對象。一定要記得在不需要線程的時(shí)候調(diào)用此方法,否則線程是不會(huì)終止退出的,進(jìn)程也就會(huì)一直運(yùn)行,占用著資源。如果有大量的線程未退出,進(jìn)程最終會(huì)崩掉。
•public static Looper Looper.myLooper()
這個(gè)是獲得調(diào)用者所在線程所擁有的Looper對象的方法。
還有二個(gè)接口是與主線程有關(guān)的:
•一個(gè)是專門為主線程準(zhǔn)備的
public static void Looper.prepareMainLooper();
這個(gè)方法只給主線程初始化Looper用的,它僅在ActivityThread.main()方法中調(diào)用,其他地方或其他線程不可以調(diào)用,如果在主線程中調(diào)用會(huì)有異常拋出,因?yàn)橐粋€(gè)線程只能創(chuàng)建一個(gè)Looper對象。但是如在其他線程中調(diào)用此方法,會(huì)改變mainLooper,接下來的getMainLooper就會(huì)返回它而非真正的主線程的Looper對象,這不會(huì)有異常拋出,也不會(huì)有明顯的錯(cuò)誤,但是程序?qū)⒉荒苷9ぷ鳎驗(yàn)樵驹O(shè)計(jì)在主線程中運(yùn)行的方法將轉(zhuǎn)到這個(gè)線程里面,會(huì)產(chǎn)生很詭異的Bug。這里L(fēng)ooper.prepareMainThread()的方法中應(yīng)該加上判斷:
public void prepareMainLooper() {
if (getMainLooper() != null) {
throw new RuntimeException("Looper.prepareMainthread() can ONLY be called by Frameworks");
}
//...
}
以防止其他線程非法調(diào)用,光靠文檔約束力遠(yuǎn)不夠。
•另外一個(gè)就是獲取主線程Looper的接口:
public static Looper Looper.getMainLooper()
這個(gè)主要用在檢查線程合法性,也即保證某些方法只能在主線程里面調(diào)用。但這并不保險(xiǎn),如上面所說,如果一個(gè)衍生線程調(diào)用了prepareMainLooper()就會(huì)把真正的mMainLooper改變,此衍生線程就可以通過上述檢測,導(dǎo)致getMainLooper() != myLooper()的檢測變得不靠譜了。所以ViewRoot的方法是用Thread來檢測:mThread != Thread.currentThread();其mThread是在系統(tǒng)創(chuàng)建ViewRoot時(shí)通過Thread.currentThread()獲得的,這樣的方法來檢測是否是主線程更加靠譜一些,因?yàn)樗鼪]有依賴外部而是相信自己保存的Thread的引用。
Message對象
消息Message是僅是一個(gè)數(shù)據(jù)結(jié)構(gòu),是信息的載體,它與隊(duì)列機(jī)制是無關(guān)的,封裝著要執(zhí)行的動(dòng)作和執(zhí)行動(dòng)作的必要信息,what, arg1, arg2, obj可以用來傳送數(shù)據(jù);而Message的回調(diào)則必須通過Handler來定義,為什么呢?因?yàn)镸essage僅是一個(gè)載體,它不能自己跑到目標(biāo)MessageQueue上面去,它必須由Handler來操作,把Message放到目標(biāo)隊(duì)列上去,既然它需要Handler來統(tǒng)一的放到MessageQueue上,也可以讓Handler來統(tǒng)一定義處理消息的回調(diào)。需要注意的是同一個(gè)Message對象只能使用一次,因?yàn)樵谔幚硗晗⒑髸?huì)把消息回收掉,所以Message對象僅能使用一次,嘗試再次使用時(shí)MessageQueue會(huì)拋出異常。
Handler對象
它被設(shè)計(jì)出來目的就是方便隊(duì)列線程客戶端的操作,隱藏直接操作MessageQueue的復(fù)雜性。Handler最主要的作用是把消息發(fā)送到與此Handler綁定的線程的MessageQueue上,因此在構(gòu)建Handler的時(shí)候必須指定一個(gè)Looper對象,如果不指定則通過Looper獲取調(diào)用者線程的Looper對象。它有很多重載的send*Message和post方法,可以以多種方式來向目標(biāo)隊(duì)列發(fā)送消息,廷時(shí)發(fā)送,或者放到隊(duì)列的頭部等等;
它還有二個(gè)作用,一個(gè)是創(chuàng)建Message對象通過obtain*系統(tǒng)方法,另一個(gè)就是定義處理Message的回調(diào)mCallback和handleMessage,由于一個(gè)Handler可能不止發(fā)送一個(gè)消息,而這些消息通常共享此Handler的回調(diào)方法,所以在handleMessage或者mCallback中就要區(qū)分這些不同的消息,通常是以Message.what來區(qū)分,當(dāng)然也可以用其他字段,只要能區(qū)別出不同的Message即可。需要指明的是,消息隊(duì)列中的消息本身是獨(dú)立的,互不相干的,消息的命名空間是在Handler對象之中的,因?yàn)镸essage是由Handler發(fā)送和處理的,所以只有同一個(gè)Handler對象需要區(qū)別不同的Message對象。廣義上講,如果一個(gè)消息自己定義有處理方法,那么所有的消息都是互不相干的,當(dāng)從隊(duì)列取出消息時(shí)就調(diào)用其上的回調(diào)方法,不會(huì)有命名上的沖突,但由Handler發(fā)出的消息的回調(diào)處理方法都是Handler.handleMessage或Handler.mCallback,所以就會(huì)有影響了,但影響的范圍也令局限在同一個(gè)Handler對象。
因?yàn)镠andler的作用是向目標(biāo)隊(duì)列發(fā)送消息和定義處理消息的回調(diào)(處理消息),它僅是依賴于線程的MessageQueue,所以Handler可以有任意多個(gè),都綁定到某個(gè)MessageQueue上,它并沒有個(gè)數(shù)限制。而MessageQueue是有個(gè)數(shù)限制的,每個(gè)線程只能有一個(gè),MessageQueue通過Looper創(chuàng)建,Looper存儲(chǔ)在線程的ThreadLocal中,Looper里作了限制,每個(gè)線程只能創(chuàng)建一個(gè)。但是Handler無此限制,Handler的創(chuàng)建通過其構(gòu)造函數(shù),只需要提供一個(gè)Looper對象即可,所以它沒有個(gè)數(shù)限制。