Android Handler 異步調用修改界面與主線程


 

在Android編程的過程中,如果在Activity中某個操作會運行比較長的時間,比如:下載文件。這個時候如果在主線程中直接下載文件,會造成Activity卡死的現象;而且如果時間超過5秒,會有ANR報錯。

在這種情況下, 可以使用Thread來處理,而如果在這期間需要根據Thread中的操作來更新界面,就需要使用Handler來處理。

 

涉及到的類主要有:Handler、Thread、Message、MessageQueue、Looper、HandlerThread

  如果是針對上面的情況,可以只使用Handler、Message和Thread就可以解決。在Thread中處理下載文件的過程,並在結束后,向Handler發送消息(Message)。Handler在接收到消息后對界面進行修改。

  如果只是不想在界面線程中進行下載文件的操作,那只需要創建一個新的線程來處理下載文件的過程就可以,為什么還要使用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。這樣可以減少頻繁創建銷毀消息的消耗。

 

二、不在主線程中創建Handler對象(使用Handler和Looper)

  如果創建Handler的操作不在主線程中,那么在構造Handler對象的時候,就需要傳遞一個Looper對象。因為如果不傳遞Looper對象,Handler將調用Looper.myLooper()方法,然后判斷是否獲取到了Looper對象,如果沒有,將拋出異常。而只有主線程在創建的時候才會自動創建Looper對象,所以在非主線程中,調用這樣的Handler構造函數,一定會拋出異常。

  當然,上面為了說明的方便,把事情說的太死。其實在非主線程中構造Handler對象的時候,還是可以不傳遞Looper對象,但是在調用Handler構造函數之前,一定要調用Looper.prepare方法。因為Looper沒有public的構造函數,只能通過Looper.prepare來創建Looper對象。prepare方法在創建了Looper對象之后,會將這個Looper對象與當前線程相關聯,所以之后Hanlder中再調用Looper.myLooper()方法就可以獲取到Looper對象了。prepare方法也不能多次調用,如果線程已經關聯了Looper對象,再調用prepare方法,將拋出異常。

  然后進行一些初始化信息,比如構造Handler對象。

  最后調用Looper.loop方法。需要注意的是這個方法將循環讀取MessageQueue中的信息,並處理這些信息。這個方法下面的代碼暫時將不會執行,直到調用Looper對象的quit方法,所以所有的初始化代碼一定要放在Looper.loop()之前。

 

三、用HandlerThread創建線程

  上面說明的是在非主線程中,如果要使用Handler的話,需要使用上面的說法。其實Android提供了更加簡潔方便的方式——使用HandlerThread來創建新的線程。

  如果已經知道在新線程中需要使用Handler對象,則可以用HandlerThread來創建新線程。可以調用HandlerThread對象的getLooper方法獲取這個線程關聯的Looper對象。然后將這個Looper對象傳遞給Handler對象。

  因為HandlerThread重寫了Thread的run方法,在run方法中,調用了Looper.prepare、Looper.loop方法,所以不需要自己來實現這部分代碼。

  HandlerThread提供了一個方法onLooperPrepared回調方法,可以重寫這個方法,進行一些初始化的工作,比如構造Handler對象(此時,可以不傳遞Looper對象)。這個方法處在Looper.prepare()和Looper.loop()方法之間。

 

主線程:

  Activity的繪制是在主線程中完成的,而主線程在創建的時候,就會創建Looper對象,所以界面線程使用Handler的時候,不需要考慮創建Looper的問題。那是不是Activity等組件的所有操作都在主線程中進行呢?通過實驗發現,基本可以這樣理解,即使這個組件是由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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM