詳解Android中Handler的使用方法

在Android開發中,咱們常常會遇到這樣一種狀況:在UI界面上進行某項操做後要執行一段很耗時的代碼,好比咱們在界面上點擊了一個」下載「按鈕,那麼咱們須要執行網絡請求,這是一個耗時操做,由於不知道何時才能完成。爲了保證不影響UI線程,因此咱們會建立一個新的線程去執行咱們的耗時的代碼。當咱們的耗時操做完成時,咱們須要更新UI界面以告知用戶操做完成了。因此咱們可能會寫出以下的代碼:java

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機制。android

那麼Handler究竟是什麼呢?Handler是Android中引入的一種讓開發者參與處理線程中消息循環的機制。每一個Hanlder都關聯了一個線程,每一個線程內部都維護了一個消息隊列MessageQueue,這樣Handler實際上也就關聯了一個消息隊列。能夠經過Handler將Message和Runnable對象發送到該Handler所關聯線程的MessageQueue(消息隊列)中,而後該消息隊列一直在循環拿出一個Message,對其進行處理,處理完以後拿出下一個Message,繼續進行處理,周而復始。當建立一個Handler的時候,該Handler就綁定了當前建立Hanlder的線程。從這時起,該Hanlder就能夠發送Message和Runnable對象到該Handler對應的消息隊列中,當從MessageQueue取出某個Message時,會讓Handler對其進行處理。spring

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。如下是輸出結果: 
app

經過輸出結果能夠看出,Runnable中的代碼所執行的線程ID與DownloadThread的線程ID不一樣,而與主線程的線程ID相同,所以咱們也由此看出在執行了Handler.post(Runnable)這句代碼以後,運行Runnable代碼的線程與Handler所綁定的線程是一致的,而與執行Handler.post(Runnable)這句代碼的線程(DownloadThread)無關。ide

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進行通訊的步驟是: 
post

  • 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)的線程沒有關係。

相關文章
相關標籤/搜索