Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> AsyncTask陷阱之:Handler,Looper與MessageQueue的詳解

AsyncTask陷阱之:Handler,Looper與MessageQueue的詳解

編輯:關於Android編程

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對象即可,所以它沒有個數限制。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved