Handler機制學習小結

來源:互聯網
上載者:User

Handler機制學習小結

一、      Handler的作用

先說明為何需要Handler。主線程需要執行某個耗時操作,並根據操作的結果相應地更新UI。但是當耗時的操作在5秒內無法完成時,Android會報出ANR錯誤。導致此ANR錯誤的原因是主線程在5秒內只幹了一件事(而且還可能這一件事也沒完成),那就不能響應使用者的其它輸入事件,解決的辦法是我們可以讓子線程來幹這件耗時的事,然後把結果返回給主線程,主線程根據返回的結果來更新相應的UI(註:子線程並不與客戶互動,所以它並不直接處理使用者的輸入事件,所以幹一件事超過5秒是沒有關係的)。

這裡我們的問題轉化為一個Android中線程通訊的問題:主線程需要發個訊息告訴子線程該幹什麼,子線程幹完之後需要把工作成果反饋給主線程,主線程需要根據子線程反饋的結果來做相應的處理。這些工作都是Handler來完成的,這就是Handler存在的其中一個理由。

二、      Handler使用的流程

如何通過Handler來完成線程間的通訊呢?分為以下幾步:建立執行個體,發送事件訊息,接受事件處理結果並更新UI。

1.      我們應該在主線程中建立一個Handler執行個體

Handler handler= new Handler();Handler handler= new Handler(loop); //loop是一個Loop對象

Handler還有兩個構造方法,這裡沒有列舉。這裡需要指出,這裡列舉出的兩個方法有本質的區別,我們會在後面再討論這裡的區別。

2.      我們把要做的事封裝成一個Message對象,把這個Message對象通過Handler的方法發送出去。

Handler中分發訊息的一些方法
       

post(Runnable)        postAtTime(Runnable,long)        postDelayed(Runnable long)        sendEmptyMessage(int)        sendMessage(Message)        sendMessageAtTime(Message,long)        sendMessageDelayed(Message,long)

3.      public voidhandleMessage(Message msg)

在這裡我們可以接受到子線程完成事件後返回的結果,我們可以override這個方法來更新UI,當然如果不需要的話也可以不用這個方法,這取決於你的應用程式具體的需要。

這裡可以看到,Handler的使用非常簡單,下面我們就來看一個執行個體:我們的UI介面有兩個Button,一個用來下載,將非常耗時,我們通過Thread的sleep方法來類比這個耗時的過程,我們讓它睡眠6秒鐘,另一個Button非常簡單,只是通過Toast顯示資訊而已。

Layout檔案非常簡單,略過不提。代碼如下:

public class Handler01Activityextends Activity {      private int index = 0;    @Override    public void onCreate(BundlesavedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.main);        //兩個Button按鍵,dnloadBt和caculateBt        Button dnloadBt =(Button)findViewById(R.id.download);        Button caculateBt =(Button)findViewById(R.id.caculate);        //為一個Button綁定事件:事件到內容在下面的Runnable runnable的run方法中,我們把這個事件交給handler,利用post方法發送出去。        dnloadBt.setOnClickListener(newView.OnClickListener() {                    public void onClick(View v){                           handler.post(runnable);//第二步:通過Handler把訊息發送出去,訊息中攜帶需要處理到耗時事件                    }             });        //為這個Button綁定事件:通過Toast顯示一句話        caculateBt.setOnClickListener(newView.OnClickListener() {                    public void onClick(View v){                           // TODOAuto-generated method stub                           Toast.makeText(Handler01Activity.this,"ok " + (index++), Toast.LENGTH_SHORT).show();                    }             });    }    //第一步:在主線程中建立Handler執行個體,注意這裡調用的構造方法!!    Handler handler = new Handler();    //這裡描述了需要處理到耗時事件:Thread.sleep(6000),然後通過Toast顯示一句話    Runnable runnable = new Runnable() {             public void run() {                    // TODO Auto-generatedmethod stub                    try {                           Thread.sleep(6000);                    } catch (InterruptedExceptione) {                           // TODOAuto-generated catch block                           System.out.println(e);                    }             }      };}

簡單分析這個例子。

如果使用者如下操作:先按dnloadBt,然後立刻按下caculateBt,也就是先命令執行很耗時的操作,然後立刻執行並不耗時的操作。我們知道,如果我們不使用Handler,那麼這種操作一定會報ANR的錯誤,因為應用程式執行第一個操作需要6s,而相應第二個操作只能在6s之後,使用者的第二個操作是不能夠在5s內得到響應的,儘管第二個操作很簡單。

現在我們使用了Handler機制,那麼是否就可以避免ANR的錯誤出現呢?結果也不能,這讓我非常疑惑,因為它並沒有發揮它應有的作用。要想解決這個疑問,那麼我們還需要對Handler機制進行更深入的學習和理解。

三、      Handler的訊息管理機制

這裡先提幾個問題:主線程通過Hanler的訊息分發方法把訊息發送出去,發送到了哪裡?子線程如何獲得這些訊息?獲得這些訊息後又如何處理,以完成主線程交代的任務?任務完成之後,子線程需要把結果反饋給主線程,並且主線程會用handleMessage方法去更新UI,但是誰來做這些事?怎麼知道子線程已經完成了任務……

Fig.1 簡單Handler

圖1中描述了如下資訊:在主線程中建立Handler執行個體之後,Handler就獲得了一個Looper對象,該對象也唯一對應地擁有了一個訊息佇列。Handler在主線程調用的所有發送訊息的方法,都最終把訊息發送到了Looper中的訊息佇列中。Looper的loop方法會從訊息佇列的隊首取出訊息,並對訊息進行分類,交給相應的方法去處理(詳細過程略)。這裡存在一個需要仔細考慮的問題,那就是Handler中的Looper是怎麼來的。重點考慮對比兩種建立Handler的方法。

1.       Handler()構造方法

publicHandler() {//擷取本線程的looper對象,預設將關聯當前線程的looper //如果本線程還沒有設定looper,則會拋出異常        mLooper = Looper.myLooper();// 重要,直接把關聯looper的MQ作為自己的MQ,因此它的訊息將發送到關聯looper的MQ上        mQueue = mLooper.mQueue;        mCallback = null;}

可以看到,該方法建立Handler的過程中,looper對象的獲得是通過Looper.myLooper()方法。該方法的注釋如下:Return theLooper object associated with the current thread.  Returns null if the calling thread is notassociated with a Looper. 也就是它會返回與當前線程關聯的Looper,如果當前線程沒有關聯任何Looper,那麼則會返回空。有一點需明確:主線程一定關聯了一個Looper(關聯的過程是由系統完成的,具體可參考ActivityThread.java),而子線程一般是沒有關聯Looper的,所以我們可以在主線程中調用這個Handler的構造方法,但是在一個子線程中調用此法可能會拋出異常。

現在可以解釋前面的執行個體程式中的疑問了:由於程式中是通過這個構造方法建立Handler執行個體的,所以Handler擷取到的Looper其實是主線程的,那麼訊息佇列也是主線程的,所以Handler只是把訊息交到了主線程的訊息佇列中,實際還是主線程在處理這個耗時的訊息,所以會有ANR報錯。

2.Handler(Looper looper)構造方法

public Handler(Looper looper) {        mLooper = looper;        mQueue = looper.mQueue;        mCallback = null;}

這個構造方法很簡單,是有外部傳遞了一個looper對象,那麼自然這個Handler的訊息都會發送到這個looper對象的訊息佇列中,處理訊息也是由這個looper對象所在的進程來處理。所以,這才是我們真正想要的非同步處理方法。只有這樣才能避免我們前面的執行個體碰到的ANR報錯。接下來我們就來分析如何使用這種方式來解決我們的問題。

四、      利用Handler實現非同步處理

既然已經明確我們應該調用Handler(Looperlooper)構造方法去建立Handler執行個體,同時此Looper對象應該是非主線程的。那麼我們的想法應該是,首先建立一個子線程,然後為此子線程關聯一個Looper對象,然後把這個Looper對象交給Handler,這樣Handler發送的訊息就能交給子線程來處理了。

建立子線程簡單,關鍵是建立Looper對象。

private Looper() {        mQueue = new MessageQueue();        mRun = true;        mThread = Thread.currentThread();   }

Looper類只有這一個構造方法,但它是私人的,所以不能通過這個方法建立Looper對象。該類提供了另一個方法可以協助我們,它的方法體內調用了這個唯一的構造方法。

public static void prepare() { if (sThreadLocal.get() != null) {//試圖在有Looper的線程中再次建立Looper將拋出異常  throw new RuntimeException("Only one Looper may be created perthread");        }  sThreadLocal.set(new Looper());//核心語句}

這樣只是對該線程設定了looper,還需要調用Looper.myLooper()方法才能真正獲得該子線程的looper對象。此時,還沒有結束,我們還需要調用Looper的loop方法,這樣這個looper才能夠使用,它才可以不斷地從訊息佇列中抽取訊息並執行。

事實上,我們並不需要做的這麼複雜,因為Android已經提供了一個類,在這個類裡這些複雜的工作已經都完成了,我們只需要使用就可以。這個類就是HandlerThread,它從Thread繼承。

HandlerThread handlerThread = newHandlerThread("yutao");handlerThread.start();Handler UIhandler = new Handler(handlerThread.getLooper());

只要通過這三行代碼我們就建立了子線程,且將子線程有了自己的looper對象,並且把這個對象傳遞給了Handler,當Handler再發送訊息時,都會交給子線程來進行非同步處理啦。

五、      小結

下面簡單總結一下Thread、Handler、Looper、MessageQueue之間的關聯。

首先,一個線程最多隻能有一個Looper,也可以沒有Looper;主線程一般都具有Looper,是由系統協助我們建立完成的,可以直接使用;我們自己建立的線程是沒有Looper的,需要我們自己去建立,同時需要調用loop方法去啟動;Android提供了HandlerThread類來方便我們實現Handler的非同步處理,它是一個已經具備Looper的線程。

其次,一個線程可以建立一個或多個Handler;而每個Handler必須具備一個Looper,且在建立Handler的過程中就為Handler綁定了一個Looper;線上程中建立Handler有兩種方式(其實有4個構造方法,這裡只關心我們討論的問題,故不考慮另外兩種):new Handler()和new Handler(Loop looper);下面第一幅圖中指出,以第一種方式建立時,由於未指定looper,故預設選擇建立此Handler的線程的Looper,但是如果這個線程沒有關聯Looper,則會拋出異常,且這種方式的Handler不能實現非同步處理,那麼這種方式有什麼用途,我暫時也不懂,網上說可以安排訊息或Runnable
在某個主線程中某個地方執行;以第二種方式建立時,可以綁定另一個線程的Looper,那麼這樣就可以實現非同步處理,這種方式的應用更常見,當然前提是那個線程已經關聯Looper,否則也會拋出異常,更常見的我們會寫一個子線程繼承HandlerThread。

第三,一個Looper與一個MessageQueue一一對應,是一對一的關係;與Looper綁定的Handler的所有訊息都會發送到這個訊息佇列中,Handler能夠通過handleMessage方法處理的所有訊息都是從這個訊息佇列裡擷取的。

第四,前面已經提到,一個Handler必須綁定一個Looper,且在建立Handler時就已綁定;但一個Looper可以為多個Handler共用;這裡可能存在這樣的問題,那就是如果多個Handler把訊息發送到同一個Looper中,也就是發送到同一個訊息佇列中,那麼在Handler的handleMessage方法得到的訊息會是自己應該得到的嗎?這個問題是這麼處理的:每一個Message都有一個成員變數Handler target,所以當某個Handler發送訊息時,都會在這個訊息中指明這個訊息的target對象是這個Handler,這樣我們就能夠判斷某個訊息是哪個Handler的了。

                     

Fig.2通過newHandler()方法建立                                                      Fig.3通過newHandler(Looper)方法建立

最後,討論一下Handler中的訊息。Handler中的Message可以分為兩種,一種是有Runnable的,一種是沒有Runnable。當Handler去post一個Runnable對象時,它會被封裝成一個訊息,封裝的過程很簡單:建立一個Message有對象,並為它的成員變數Runnablecallback賦值即可。這兩種訊息的根本區別在於,在訊息佇列中排隊,輪到自己時,帶Runnable的訊息接下來會執行它的run方法,然後就結束了,而不帶Runnable的訊息將直接發送到Handler的handleMessage方法中去處理。我的理解是,當主線程有耗時工作需要處理時,把它放在Runnable中放到訊息佇列中交給子線程去處理,而子線程處理結束後,可以發送不帶Runnable的訊息到隊列中,主線程可以在handleMessage中擷取到子線程的工作成果並相應更新UI(此說法未得到驗證,純屬個人理解)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.