本篇紀錄如何在Android中使用 Handler + Looper + Message + Queue。
為了讓App保持快速的回應性(避免產生ANR),長期任務不可執行在 UI thread上,這些長期任務包括,網路存取,讀寫檔案,資料庫存取,圖形處理等等。
在Android中使用Thread和在一般的Java中使用方式相當類似,但有一些同步化問題必須去解決,如 Race Condition, Blocking等等
為了方便解決這些問題,Android定義訊息傳送機制來確保訊息傳送的正確性以及安全性,Android訊息傳送機制包含4個角色,各為Looper,Handler,Message,MessageQueue。
 
Message :
訊息,由一組描述和一些資料物件所組成,可藉由Handler傳送到MessageQueue中,並由Looper取出再回傳給同一個Handler,當需要傳遞參數時,可夾帶在Message中。
建立Message的方式盡量使用Message.obtain(), 或是Handler.obtainMessage(),因為訊息會被放到緩衝池以重複使用。
 
MessageQueue :
訊息佇列,透過Looper建立,內容則是訊息組成,結構為無限制的連結串列,一個Looper只能有一個MessageQueue,
訊息會藉由Handler插入,並由Looper取出傳送到相對應的Handler。
 
Looper :
管理MessageQueue,會將存放於MessageQueue中的訊息依序送出到Handler上,讓Handler作進一步的處理,
Handler必須和Looper連結,否則無法傳送或接收訊息,而Thread也必須和Looper連結,才能讓MessageQueue和Thread相關連。
簡單的說Looper是Thread和MessageQueue的協調者。一個Thread只能有一個Looper。
 
Handler :
訊息處理者,在Android訊息傳送機制中,Handler是最常使用的類別,Handler可建立訊息,插入訊息到訊息佇列中,處理訊息。
一個Looper可以有多個Handler,Looper會自動將Message傳送給對應的Handler。
 
藉由以下的範例來說明彼此之間的互動,範例會盡量少用匿名內部類別來實作,而是把各個元件拆開為單一類別,比較容易了解
範例只有一個button和一個textview組成,當點擊button時,會去擷取 https://www.google.com.tw/ 的網頁資料
並顯示到textview上。
 
首先是 MainActivity,因為網路存取是長期任務,且android在Honeycomb SDK新增了NetworkOnMainThreadException,
若在Main thread(UI thread)進行網路操作,會丟出Exception。所以把網路操作放到CustomizeWorkerThread中執行,
第18行建立 mCustomizeWorkerThread 物件
第38行建立CustomizeWorkerThread物件並連結到參考,傳入this是為了把取得的網頁資料設定回MainActivity的textview
第39行呼叫start()方法以啟動mCustomizeWorkerThread的run方法。
第48行當按鈕按下時,呼叫sendMessageToWorkerHandler方法,該方法會建立message插入到MessageQueue
第55行取得mCustomizeWorkerThread的Handler,
第56行建立Message物件,其中數字2為Message的what欄位數值,當Handler處理Message時可以依此數值判斷行為。
第57行利用Handler將Message物件插入到MessageQueue中
第69行當activity結束時也告知Looper停止處理訊息

package com.foxx.threads;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends ActionBarActivity implements OnClickListener
{
    private static final String TAG = "MainActivity";
    private Button mStartThreadButton;
    private TextView mResultTextView;
    private CustomizeWorkerThread mCustomizeWorkerThread;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initUIComponents();
        startWorkerThread();
    }
    private void initUIComponents()
    {
        mStartThreadButton = (Button) findViewById(R.id.startThread);
        mStartThreadButton.setOnClickListener(this);
        mResultTextView = (TextView) findViewById(R.id.textview);
    }
    private void startWorkerThread()
    {
        mCustomizeWorkerThread = new CustomizeWorkerThread(this);
        mCustomizeWorkerThread.start();
    }
    @Override
    public void onClick(View view)
    {
        int uiId = view.getId();
        switch (uiId) {
            case R.id.startThread:
                sendMessageToWorkerHandler();
                break;
        }
    }
    private void sendMessageToWorkerHandler()
    {
        Handler handler = mCustomizeWorkerThread.getHandler();
        if(handler != null){
            Message message = handler.obtainMessage(2);
            handler.sendMessage(message);
        }
    }
    public TextView getResultTextView()
    {
        return mResultTextView;
    }
    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        mCustomizeWorkerThread.getHandler().getLooper().quit();
    }
}

 
以下為CustomizeWorkerThread的內容,
第10行為了取代匿名內部類別,另外建立了CustomizeWorkerHandler,該Handler負責處理從Looper傳送過來的Message
第18行的run方法相當重要,在run方法中連結了Looper,Thread,Handler三者。
第20行的Looper.prepare方法會建立Looper以及在該Looper中的MessageQueue,並連結到當前的Thread(CustomizeWorkerThread)
第21行建立CustomizeWorkerHandler物件並連結Looper
第22行Looper開始循序處理MessageQueue,為無限迴圈。當Message插入到MessageQueue中,Looper就會擷取該Message並發送到相關的Handler。
第25行回傳Handler讓外部可以藉由取得的Handler插入訊息
第31行即為CustomizeWorkerHandler,繼承 Handler
第41行必須複寫HandleMessage方法,當Looper傳送Message時,該方法即會被呼叫(callback)
第43行根據Message的what數值判斷行為
第45行就是網路存取的行為,HttpPageData.getPageData 會回傳google的網頁資料
第46行把取得的網頁資料設定到MainActivity的textview中

package com.foxx.threads;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
public class CustomizeWorkerThread extends Thread
{
    private static final String TAG = "WorkerThread";
    private CustomizeWorkerHandler mHandler;
    private MainActivity mMainActivity;
    public CustomizeWorkerThread(MainActivity activity) {
        mMainActivity = activity;
    }
    @Override
    public void run()
    {
        Looper.prepare();
        mHandler = new CustomizeWorkerHandler(mMainActivity);
        Looper.loop();
    }
    public CustomizeWorkerHandler getHandler()
    {
        return mHandler;
    }
}
class CustomizeWorkerHandler extends Handler
{
    private static final String TAG = "WorkerHandler";
    private MainActivity mMainActivity;
    public CustomizeWorkerHandler(MainActivity activity) {
        mMainActivity = activity;
    }
    public void handleMessage(Message message)
    {
        switch (message.what) {
            case 2:
                String result = HttpPageData.getPageData("https://www.google.com.tw/");
                setResultToMainActivity(result);
                break;
        }
    }
    private void setResultToMainActivity(final String result)
    {
        mMainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run()
            {
                mMainActivity.getResultTextView().setText(result);
            }
        });
    }
}

 
接著簡單描述整個訊息傳送機制的互動過程,

建立訊息傳送機制

在 Activity 中會持有CustomWorkThread的類別成員(mCustomizeWorkerThread),
並在onCreate方法中建立實體,然後呼叫start方法,start方法會啟動CustomizeWorkerThread中的run方法,
該run方法會將worker thread,Looper, Handler 三者連結起來,到目前為止是整個傳遞訊息機制的建立,

發送訊息

在Activity中為了發送訊息到MessageQueue中,必須先建立訊息再藉由取得mCustomerWorkThread的Handler將訊息發送到MessageQueue中。

處理訊息

當訊息已經被發送到MessageQueue中,Looper便會自動擷取訊息,並將訊息發送到相對應的Handler,
相對應的意思為哪個Handler發送訊息,哪個Handler就會接收到訊息。
Handler藉由handleMessage方法來處理訊息。


以下為HttpPageData.java

package com.foxx.threads;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
public class HttpPageData
{
    /**
     * 取得網頁資料
     * @return : 回傳string型態的網頁資料
     */
    public static String getPageData(String httpUrl){
        java.net.CookieManager cm = new java.net.CookieManager();
        java.net.CookieHandler.setDefault(cm);
        URL u = null;
        InputStream in = null;
        InputStreamReader r = null;
        BufferedReader br = null;
        StringBuffer message = null;
        try {
           u = new URL(httpUrl);
           in = u.openStream();
           r = new InputStreamReader(in, "BIG5");//UTF-8
           br = new BufferedReader(r);
           String tempstr = null;
           message = new StringBuffer();
           while ((tempstr = br.readLine()) != null) {
               message.append(tempstr);
           }
        } catch (Exception e) {
           e.getStackTrace();
           System.out.println(e.getMessage());
        } finally {
           try {
              u = null;
              in.close();
              r.close();
              br.close();
           } catch (Exception e) {
           }
    }
        return message.toString();
    }
}