本篇紀錄如何在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(); } }