標籤:
在Android編程的過程中,如果在Activity中某個操作會運行比較長的時間,比如:下載檔案。這個時候如果在主線程中直接下載檔案,會造成Activity卡死的現象;而且如果時間超過5秒,會有ANR報錯。
在這種情況下, 可以使用Handler來處理。
涉及到的類主要有:Handler、Thread、Message、MessageQueue、Looper、HandlerThread
如果是針對上面的情況,可以只使用Handler、Message和Thread就可以解決。在Thread中處理下載檔案的過程,並在結束後,向Handler發送訊息(Message)。Handler在接收到訊息後對介面進行修改。
如果只是不想在介面線程中進行下載檔案的操作,那隻需要建立一個新的線程來處理下載檔案的過程就可以,為什麼還要使用Handler呢?因為Android的介面線程是不安全的,所以如果直接在Thread中修改介面會報錯,所以要使用Handler來處理。當在介面線程中擷取Handler時,Handler將和介面線程在同一個線程中,所以通過Handler修改介面將不會報錯。
1、Handler要重寫handleMessage方法,用來處理修改介面的邏輯。
handler = new Handler() { @Override public void handleMessage(Message msg) { // TODO Auto-generated method stub super.handleMessage(msg); switch (msg.what) { case MessageType.ProgressType : int arg1 = msg.arg1; if (arg1 >= maxProgress) { Toast.makeText(MainActivity.this, "Progress is OK", Toast.LENGTH_SHORT); return; } else if (arg1 < 0) { arg1 = 0; } progressBar.setProgress(arg1); break; default : break; } } };
說明:
- 此處直接使用沒有參數的Handler建構函式,表示擷取當前線程的Looper,而主線程在建立的時候預設產生了一個Looper,所以在主線程的Activity中,可以直接使用Handler的無參建構函式。
- 如果在主線程中想要擷取主線程的Looper,可以使用Looper.myLooper()方法擷取。
- 在其他線程中要想擷取主線程的Looper,需要使用Looper.getMainLooper()方法擷取,在主線程中也可以使用這個方法擷取。
- 在其他線程中,如果直接使用Looper.myLooper()方法,將擷取到null。
2、Thread中要在下載完檔案後(我使用了TimerTask和Timer來實現100毫秒進度加1),向Handler發送訊息。
public class MyProgressTimerTask extends TimerTask { private Handler handler; private int count; public MyProgressTimerTask(Handler handler) { this.handler = handler; count = 0; } @Override public void run() { // TODO Auto-generated method stub count++; Message message = handler.obtainMessage(); message.what = MessageType.ProgressType; message.arg1 = count; handler.sendMessage(message); Looper looper = Looper.myLooper(); if (null == looper) { System.out.println(">>>>>>>>>>>>>>>>Looper.myLooper is null<<<<<<<<<<<<<<"); } }}
3、在點擊開始按鈕時,啟動Timer;點擊停止時,關閉Timer。也可以不使用Timer,而是使用Thread,在Thread中sleep一段時間,依然可以達到預期的效果。同時,兩種方式都可以對Activity進行操作,不會有死機的感覺。
progressBar = (ProgressBar) findViewById(R.id.progressBar); maxProgress = progressBar.getMax(); btnStartProgress = (Button) findViewById(R.id.btnStartProgress); btnStartProgress.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub task = new MyProgressTimerTask(handler); timer = new Timer(); timer.schedule(task, 1000, 100); } }); btnStopProgress = (Button) findViewById(R.id.btnStopProgres); btnStopProgress.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub if (task != null) { task.cancel(); } if (timer != null) { timer.cancel(); } task = null; timer = null; } });
注意:在產生Message的時候推薦使用Handler.obtainMessage()和Message.obtain()方法,因為這兩個方法(其實最終都是調用Message.obtain()方法)不是簡單的new一個對象。分析代碼可以看出,它會先判斷訊息池中是否有可用的訊息(由sPool指示開始Message的Message鏈表),如果沒有才會去建立新的Message。
主線程:
Activity的繪製是在主線程中完成的,而主線程在建立的時候,就會建立Looper對象,所以介面線程使用Handler的時候,不需要考慮建立Looper的問題。那是不是Activity等組件的所有操作都在主線程中進行呢?通過實驗發現,基本可以這樣理解,當然如果在組件中建立新的線程,在新的線程的操作當然不可能在主線程中進行(比如new一個Thread,在Thread中計算1——100000的和)。除了這樣的操作外,可以理解為都在主線程中處理,即使這個組件是由new Thread調用startActivity建立的。
我寫了這樣一段代碼。首先在啟動的Activity中,啟動一個新的線程,在這個新的線程中分別啟動一個Activity、Service、BroadcastReceiver。在Activity的onCreate、onStart、onResume、onPause、onStop、onDestroy、onRestart、onActivityResult方法中列印當前線程,結果列印出的都是主線程。在Service的onCreate中和BroadcastReceiver的onReceive中也是相同的效果。所以基本可以說組件的操作都是在主線程中進行的。
因此,如果Handler主要用途是修改介面的話,那HandlerThread就不需要使用了。但是如果考慮到這樣的情境,在新啟動的線程A中,任務還是太多,又必須啟動其他線程,而線程A又需要獲得其他線程的返回結果。這時,使用HandlerThread可能就能很方便的解決問題了。Looper的使用應該很少了,HandlerThread的作用就是屏蔽掉Looper,直接使用Looper比較複雜,需要先使用Looper.prepare,然後在所有操作結束後調用Looper.loop使其迴圈讀取訊息佇列中的訊息。使用HandlerThread後,程式將極度簡化。
參考資料 :
深入理解Android訊息處理系統——Looper、Handler、Thread
深入剖析Android訊息機制
Android HandlerThread 的使用及其Demo
Android Handler 非同步呼叫修改介面與主線程