1. 程式人生 > >Android中Handler的使用

Android中Handler的使用

hierarchy only oop ide set public 構造 展示 class

在Android開發中,我們常常會遇到這樣一種情況:在UI界面上進行某項操作後要運行一段非常耗時的代碼,比方我們在界面上點擊了一個”下載“button,那麽我們須要運行網絡請求,這是一個耗時操作。由於不知道什麽時候才幹完畢。為了保證不影響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(); } } } }

上面的代碼演示了單擊”下載“button後會啟動一個新的線程去運行實際的下載操作。運行完畢後更新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》。

Android中Handler的使用