標籤:
當android應用程式運行時,一個主線程被建立(也稱作UI線程),此線程主要負責處理UI相關的事件,由於Android採用UI單執行緒模式,所以只能在主線程中對UI元素進行操作,如果在非UI線程直接對UI進行了操作,則會報錯,另外,對於運算量較大的操作和IO操作,我們需要新開線程來處理這些工作,以免阻塞UI線程,子線程與主線程之間是怎樣進行通訊的呢?此時就要採用訊息迴圈機制(Looper)與Handler進行處理。
一、基本概念
Looper:每一個線程都可以產生一個Looper,用來管理線程的Message,Looper對象會建立一個MessgaeQueue資料結構來存放message。
Handler:與Looper溝通的對象,可以push訊息或者runnable對象到MessgaeQueue,也可以從MessageQueue得到訊息。
查看其建構函式:
Handler()
Default constructor associates this handler with the queue for the current thread.//如不指定Looper參數則預設利用當前線程的Looper建立
Handler(Looper looper)
Use the provided queue instead of the default one.//使用指定的Looper對象建立Handler
線程A的Handler對象引用可以傳遞給別的線程,讓別的線程B或C等能送訊息來給線程A。
線程A的Message Queue裡的訊息,只有線程A所屬的對象可以處理。
注意:Android裡沒有global的MessageQueue,不同進程(或APK之間)不能通過MessageQueue交換訊息。
二、Handler通過Message通訊的基本方式
使用Looper.myLooper可以取得當前線程的Looper對象。
使用mHandler = new Handler(Looper.myLooper()); 可產生用來處理當前線程的Handler對象。
使用mHandler = new Handler(Looper.getMainLooper()); 可誕生用來處理main線程的Handler對象。
使用Handler傳遞訊息對象時將訊息封裝到一個Message對象中,Message對象中主要欄位如下:
Message對象可以通過Message類的建構函式獲得,但Google推薦使用Message.obtain()方法獲得,該方法會從全域的對象池裡返回一個可複用的Messgae執行個體,API中解釋如下:
Message()
Constructor (but the preferred way to get a Message is to call Message.obtain()).
Handler發出訊息時,既可以指定訊息被接受後馬上處理,也可以指定經過一定時間間隔之後被處理,如sendMessageDelayed(Message msg, long delayMillis),具體請參考API。
Handler訊息被發送出去之後,將由handleMessage(Message msg)方法處理。
注意:在Android裡,新誕生一個線程,並不會自動建立其Message Loop可以通過調用Looper.prepare()為該線程建立一個MessageQueue,再調用Looper.loop()進行訊息迴圈。
下面舉例說明:
在main.xml中定義兩個button及一個textview
public class HandlerTestActivity extends Activity implements OnClickListener{ public TextView tv; private myThread myT; Button bt1, bt2; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); bt1 = (Button)findViewById(R.id.a); bt2 = (Button)findViewById(R.id.b); tv = (TextView)findViewById(R.id.tv); bt1.setId(1);//為兩個button設定ID,此ID用於後面判斷是哪個button被按下 bt2.setId(2); bt1.setOnClickListener(this);//增加監聽器 bt2.setOnClickListener(this); } @Override public void onClick(View v) { switch(v.getId()){//按鍵事件響應,如果是第一個按鍵將啟動一個新線程 case 1: myT = new myThread(); myT.start(); break; case 2: finish(); break; default: break; } } class myThread extends Thread { private EHandler mHandler; public void run() { Looper myLooper, mainLooper; myLooper = Looper.myLooper();//得到當前線程的Looper mainLooper = Looper.getMainLooper();//得到UI線程的Looper String obj; if(myLooper == null) { //判斷當前線程是否有訊息迴圈Looper mHandler = new EHandler(mainLooper); obj = "current thread has no looper!";//當前Looper為空白,EHandler用mainLooper物件建構 } else { mHandler = new EHandler(myLooper);//當前Looper不為空白,EHandler用當前線程的Looper物件建構 obj = "This is from current thread."; } mHandler.removeMessages(0);//清空訊息佇列裡的內容 Message m = mHandler.obtainMessage(1, 1, 1, obj); mHandler.sendMessage(m);//發送訊息 } } class EHandler extends Handler { public EHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { //訊息處理函數 tv.setText((String)msg.obj);//設定TextView內容 } }}
程式運行後點擊TestLooper按鍵,TextView輸出如下,說明新建立的線程裡Looper為空白,也就說明了新建立的線程並不會自己建立Message Looper。
修改myThread類:
class myThread extends Thread { private EHandler mHandler; public void run() { Looper myLooper, mainLooper; Looper.prepare(); myLooper = Looper.myLooper(); mainLooper = Looper.getMainLooper(); ...... ...... mHandler.sendMessage(m); Looper.loop(); }}
Looper.prepare為當前線程建立一個Message Looper,Looper.loop()開啟訊息迴圈。這樣修改是OK呢?
答案是否定的!運行時Logcat將拋出CalledFromWrongThreadException異常錯誤,提示如下:
意思就是說“只有原始建立這個視圖層次的線程才能修改它的視圖”,本例中的TextView是在UI線程(主線程)中建立,因此,myThread線程不能修改其顯示內容!
一般的做法是在子線程裡擷取主線程裡的Handler對象,然後通過該對象向主線程的訊息佇列裡發送訊息,進行通訊。
如果子線程想修改主線程的UI,可以通過發送Message給主線程的訊息佇列,主線程經行判斷處理再對UI經行操作,具體可以參考之前的代碼。
三、Handler通過runnable通訊的基本方式
我們可以通過Handler的post方法實現線程間的通訊,API中關於post方法說明如下
public final boolean post (Runnable r)
Causes the Runnable r to be added to the message queue. The runnable will be run on the thread to which this handler is attached.
Post方法將安排runnable對象在主線程的某個位置運行,但是並不會開啟一個新的線程,驗證代碼如下:
public class HandlerTestActivity extends Activity { private Handler handlerTest; Runnable runnableTest = new Runnable() { public void run() { String runID = String.valueOf(Thread.currentThread().getId());//輸出Runnable 線程的ID號 Log.v("Debug",runID); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); handlerTest = new Handler(); String mainID = String.valueOf(Thread.currentThread().getId()); Log.v("Debug",mainID);//輸出主線程的ID號 handlerTest.post(runnableTest); }}
Logcat裡輸出如下:
說明只是把runnable裡的run方法放到UI線程裡運行,並不會建立新線程
因此我們可以在子線程中將runnable加入到主線程的MessageQueue,然後主線程將調用runnable的方法,可以在此方法中更新主線程UI。
將之前的代碼修改如下:
public class HandlerTestActivity extends Activity { private Handler handlerTest; private TextView tv; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); tv = (TextView)findViewById(R.id.tv);//TextView初始化為空白 handlerTest = new Handler(); myThread myT = new myThread(); myT.start();//開啟子線程 } class myThread extends Thread{ public void run(){ handlerTest.post(runnableTest);//子線程將runnable加入訊息佇列 } } Runnable runnableTest = new Runnable() { public void run() { tv.setText("此資訊由子線程輸出!"); } };}
相當於在主線程中調用了runnalbe的run方法,更改了TextView的UI!
Android線程間通訊機制