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