Android中Handler的使用

孫群發表於2015-07-29

在Android開發中,我們經常會遇到這樣一種情況:在UI介面上進行某項操作後要執行一段很耗時的程式碼,比如我們在介面上點選了一個”下載“按鈕,那麼我們需要執行網路請求,這是一個耗時操作,因為不知道什麼時候才能完成。為了保證不影響UI執行緒,所以我們會建立一個新的執行緒去執行我們的耗時的程式碼。當我們的耗時操作完成時,我們需要更新UI介面以告知使用者操作完成了。所以我們可能會寫出如下的程式碼:

package ispring.com.testhandler;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;


public class MainActivity extends Activity implements Button.OnClickListener {

    private TextView statusTextView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        statusTextView = (TextView)findViewById(R.id.statusTextView);
        Button btnDownload = (Button)findViewById(R.id.btnDownload);
        btnDownload.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        DownloadThread downloadThread = new DownloadThread();
        downloadThread.start();
    }

    class DownloadThread extends Thread{
        @Override
        public void run() {
            try{
                System.out.println("開始下載檔案");
                //此處讓執行緒DownloadThread休眠5秒中,模擬檔案的耗時過程
                Thread.sleep(5000);
                System.out.println("檔案下載完成");
                //檔案下載完成後更新UI
                MainActivity.this.statusTextView.setText("檔案下載完成");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

上面的程式碼演示了單擊”下載“按鈕後會啟動一個新的執行緒去執行實際的下載操作,執行完畢後更新UI介面。但是在實際執行到程式碼MainActivity.this.statusTextView.setText(“檔案下載完成”)時,會報錯如下,系統崩潰退出:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
錯誤的意思是隻有建立View的原始執行緒才能更新View。出現這樣錯誤的原因是Android中的View不是執行緒安全的,在Android應用啟動時,會自動建立一個執行緒,即程式的主執行緒,主執行緒負責UI的展示、UI事件訊息的派發處理等等,因此主執行緒也叫做UI執行緒,statusTextView是在UI執行緒中建立的,當我們在DownloadThread執行緒中去更新UI執行緒中建立的statusTextView時自然會報上面的錯誤。Android的UI控制元件是非執行緒安全的,其實很多平臺的UI控制元件都是非執行緒安全的,比如C#的.Net Framework中的UI控制元件也是非執行緒安全的,所以不僅僅在Android平臺中存在從一個新執行緒中去更新UI執行緒中建立的UI控制元件的問題。不同的平臺提供了不同的解決方案以實現跨執行緒跟新UI控制元件,Android為了解決這種問題引入了Handler機制。

那麼Handler到底是什麼呢?Handler是Android中引入的一種讓開發者參與處理執行緒中訊息迴圈的機制。每個Hanlder都關聯了一個執行緒,每個執行緒內部都維護了一個訊息佇列MessageQueue,這樣Handler實際上也就關聯了一個訊息佇列。可以通過Handler將Message和Runnable物件傳送到該Handler所關聯執行緒的MessageQueue(訊息佇列)中,然後該訊息佇列一直在迴圈拿出一個Message,對其進行處理,處理完之後拿出下一個Message,繼續進行處理,周而復始。當建立一個Handler的時候,該Handler就繫結了當前建立Hanlder的執行緒。從這時起,該Hanlder就可以傳送Message和Runnable物件到該Handler對應的訊息佇列中,當從MessageQueue取出某個Message時,會讓Handler對其進行處理。

Handler可以用來在多執行緒間進行通訊,在另一個執行緒中去更新UI執行緒中的UI控制元件只是Handler使用中的一種典型案例,除此之外,Handler可以做很多其他的事情。每個Handler都繫結了一個執行緒,假設存在兩個執行緒ThreadA和ThreadB,並且HandlerA繫結了 ThreadA,在ThreadB中的程式碼執行到某處時,出於某些原因,我們需要讓ThreadA執行某些程式碼,此時我們就可以使用Handler,我們可以在ThreadB中向HandlerA中加入某些資訊以告知ThreadA中該做某些處理了。由此可以看出,Handler是Thread的代言人,是多執行緒之間通訊的橋樑,通過Handler,我們可以在一個執行緒中控制另一個執行緒去做某事。

Handler提供了兩種方式解決我們在本文一開始遇到的問題(在一個新執行緒中更新主執行緒中的UI控制元件),一種是通過post方法,一種是呼叫sendMessage方法。

a. 使用post方法,程式碼如下:

package ispring.com.testhandler;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;


public class MainActivity extends Activity implements Button.OnClickListener {

    private TextView statusTextView = null;

    //uiHandler在主執行緒中建立,所以自動繫結主執行緒
    private Handler uiHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        statusTextView = (TextView)findViewById(R.id.statusTextView);
        Button btnDownload = (Button)findViewById(R.id.btnDownload);
        btnDownload.setOnClickListener(this);
        System.out.println("Main thread id " + Thread.currentThread().getId());
    }

    @Override
    public void onClick(View v) {
        DownloadThread downloadThread = new DownloadThread();
        downloadThread.start();
    }

    class DownloadThread extends Thread{
        @Override
        public void run() {
            try{
                System.out.println("DownloadThread id " + Thread.currentThread().getId());
                System.out.println("開始下載檔案");
                //此處讓執行緒DownloadThread休眠5秒中,模擬檔案的耗時過程
                Thread.sleep(5000);
                System.out.println("檔案下載完成");
                //檔案下載完成後更新UI
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Runnable thread id " + Thread.currentThread().getId());
                        MainActivity.this.statusTextView.setText("檔案下載完成");
                    }
                };
                uiHandler.post(runnable);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

我們在Activity中建立了一個Handler成員變數uiHandler,Handler有個特點,在執行new Handler()的時候,預設情況下Handler會繫結當前程式碼執行的執行緒,我們在主執行緒中例項化了uiHandler,所以uiHandler就自動繫結了主執行緒,即UI執行緒。當我們在DownloadThread中執行完耗時程式碼後,我們將一個Runnable物件通過post方法傳入到了Handler中,Handler會在合適的時候讓主執行緒執行Runnable中的程式碼,這樣Runnable就在主執行緒中執行了,從而正確更新了主執行緒中的UI。以下是輸出結果:
這裡寫圖片描述

通過輸出結果可以看出,Runnable中的程式碼所執行的執行緒ID與DownloadThread的執行緒ID不同,而與主執行緒的執行緒ID相同,因此我們也由此看出在執行了Handler.post(Runnable)這句程式碼之後,執行Runnable程式碼的執行緒與Handler所繫結的執行緒是一致的,而與執行Handler.post(Runnable)這句程式碼的執行緒(DownloadThread)無關。

b. 使用sendMessage方法,程式碼如下:

package ispring.com.testhandler;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;


public class MainActivity extends Activity implements Button.OnClickListener {

    private TextView statusTextView = null;

    //uiHandler在主執行緒中建立,所以自動繫結主執行緒
    private Handler uiHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 1:
                    System.out.println("handleMessage thread id " + Thread.currentThread().getId());
                    System.out.println("msg.arg1:" + msg.arg1);
                    System.out.println("msg.arg2:" + msg.arg2);
                    MainActivity.this.statusTextView.setText("檔案下載完成");
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        statusTextView = (TextView)findViewById(R.id.statusTextView);
        Button btnDownload = (Button)findViewById(R.id.btnDownload);
        btnDownload.setOnClickListener(this);
        System.out.println("Main thread id " + Thread.currentThread().getId());
    }

    @Override
    public void onClick(View v) {
        DownloadThread downloadThread = new DownloadThread();
        downloadThread.start();
    }

    class DownloadThread extends Thread{
        @Override
        public void run() {
            try{
                System.out.println("DownloadThread id " + Thread.currentThread().getId());
                System.out.println("開始下載檔案");
                //此處讓執行緒DownloadThread休眠5秒中,模擬檔案的耗時過程
                Thread.sleep(5000);
                System.out.println("檔案下載完成");
                //檔案下載完成後更新UI
                Message msg = new Message();
                //雖然Message的建構函式式public的,我們也可以通過以下兩種方式通過迴圈物件獲取Message
                //msg = Message.obtain(uiHandler);
                //msg = uiHandler.obtainMessage();

                //what是我們自定義的一個Message的識別碼,以便於在Handler的handleMessage方法中根據what識別
                //出不同的Message,以便我們做出不同的處理操作
                msg.what = 1;

                //我們可以通過arg1和arg2給Message傳入簡單的資料
                msg.arg1 = 123;
                msg.arg2 = 321;
                //我們也可以通過給obj賦值Object型別傳遞向Message傳入任意資料
                //msg.obj = null;
                //我們還可以通過setData方法和getData方法向Message中寫入和讀取Bundle型別的資料
                //msg.setData(null);
                //Bundle data = msg.getData();

                //將該Message傳送給對應的Handler
                uiHandler.sendMessage(msg);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

通過Message與Handler進行通訊的步驟是:
1. 重寫Handler的handleMessage方法,根據Message的what值進行不同的處理操作
2. 建立Message物件
雖然Message的建構函式式public的,我們還可以通過Message.obtain()或Handler.obtainMessage()來獲得一個Message物件(Handler.obtainMessage()內部其實呼叫了Message.obtain())。
3. 設定Message的what值
Message.what是我們自定義的一個Message的識別碼,以便於在Handler的handleMessage方法中根據what識別出不同的Message,以便我們做出不同的處理操作。
4. 設定Message的所攜帶的資料,簡單資料可以通過兩個int型別的field arg1和arg2來賦值,並可以在handleMessage中讀取。
5. 如果Message需要攜帶複雜的資料,那麼可以設定Message的obj欄位,obj是Object型別,可以賦予任意型別的資料。或者可以通過呼叫Message的setData方法賦值Bundle型別的資料,可以通過getData方法獲取該Bundle資料。
6. 我們通過Handler.sendMessage(Message)方法將Message傳入Handler中讓其在handleMessage中對其進行處理。
需要說明的是,如果在handleMessage中 不需要判斷Message型別,那麼就無須設定Message的what值;而且讓Message攜帶資料也不是必須的,只有在需要的時候才需要讓其攜帶資料;如果確實需要讓Message攜帶資料,應該儘量使用arg1或arg2或兩者,能用arg1和arg2解決的話就不要用obj,因為用arg1和arg2更高效。
程式的執行結果如下:
這裡寫圖片描述

由上我們可以看出,執行handleMessage的執行緒與建立Handler的執行緒是同一執行緒,在本示例中都是主執行緒。執行handleMessage的執行緒與執行uiHandler.sendMessage(msg)的執行緒沒有關係。

本文主要是對Android中Handler的作用於如何使用進行了初步介紹,如果大家想了解Handler的內部實現原理,可以參見下一篇博文《深入原始碼解析Android中的Handler,Message,MessageQueue,Looper》

相關文章