Android Handler的使用方式和注意事項

DonKingLiang發表於2017-05-08

今天給大家講解的是在Android開發中如何使用Handler和使用Handler時要注意的一些細節問題。本篇文章是作為我上一篇文章《Android原始碼分析--Handler機制的實現與工作原理》的補充。雖然是補充,但是兩篇文章所講的內容不同:一個是原理的分析,一個是使用的講解。如果你還沒有看過上一篇文章,建議你先去看一看,只有瞭解了Handler的原理,才能更好的使用它。而且我們今天所講的內容也是建立在上一篇文章的基礎上的。
Handler的最大作用就是執行緒的切換,至於Handler切換執行緒的原理和實現,上一篇文章已有詳細的講解,這裡就不多說了。下面我們看如何把一個訊息從一個執行緒傳送到另一個執行緒。

    //在主執行緒建立一個Handler物件。
    //重寫Handler的handleMessage方法,這個就是接收並處理訊息的方法。
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //msg就是子執行緒傳送過來的訊息。
        }
    };

    //開啟一個子執行緒
    new Thread(new Runnable() {
            @Override
            public void run() {
                //在子執行緒傳送一個訊息。
                Message msg = new Message();
                handler.sendMessage(msg);
            }
        }).start();複製程式碼

上面就是一個簡單的Handler使用例子。我們在主執行緒建立一個Handler,他的handleMessage()方法是執行在主執行緒的,當我們在子執行緒傳送一個訊息的時候,handleMessage()會接收到訊息,這樣我們就把訊息由子執行緒傳送到了主執行緒。上面的程式碼中,Handler傳送的訊息是一個空訊息,什麼資料也沒有,如果我們只是單純的傳送一個空訊息,可以使用Handler自己的傳送空訊息的方法:

handler.sendEmptyMessage(what);複製程式碼

這兩種傳送空訊息的效果是一樣的。至於這裡的what是什麼我們後面會說到。
Handler也可以傳送帶有資料的訊息。Message物件有一個Object型別的obj屬性,就是用來攜帶訊息資料的。我們只需要把要傳送的資料賦值給obj就可以了,然後在處理訊息的時候再把資料取出來。

    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            Log.i(TAG,"Handler 傳送過來的訊息是:" + msg.obj);
        }
    };

    new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.obj = "我是訊息資料";
                handler.sendMessage(msg);

            }
        }).start();複製程式碼

上面的程式碼中,msg攜帶一個字串資料:"我是訊息資料",在handleMessage()方法中把這個資料取出來。除了obj以外,Message還有兩個int型別的屬性:arg1、arg2可以用來傳送一些簡單的資料。
現在我們看到的所有的訊息都是在Handler的handleMessage()方法中處理,如果我要傳送很多個訊息,每個訊息的資料都不一樣,訊息的處理邏輯也不一樣,那麼在handleMessage()方法中如何去判斷哪個訊息是哪個呢?這時Message的what屬性就派上用場了,what屬性就是上面傳送空訊息時我們看到的what,它是一int型別,它是用來標識訊息的。

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {

            switch (msg.what) {
                case 1:
                    Log.i(TAG, "第一個訊息是:" + msg.obj);
                    break;

                case 2:
                    Log.i(TAG, "第二個訊息是:" + msg.obj);
                    break;
            }
        }
    };

    new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg1 = new Message();
                msg1.obj = "第一個訊息";
                msg1.what = 1;
                handler.sendMessage(msg1);

                Message msg2 = new Message();
                msg2.obj = "第二個訊息";
                msg2.what = 2;
                handler.sendMessage(msg2);
            }
        }).start();複製程式碼

上面我們在子執行緒中傳送了兩個訊息,並且給兩個訊息設定了它的what,在處理訊息的時候就可以通過msg的what來判斷是哪個訊息了。
除了用sendMessage傳送Message訊息以外,Handler還可以post一個Runnable。

    new Thread(new Runnable() {
            @Override
            public void run() {
                //在子執行緒post一個Runnable物件
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        //這裡是訊息處理的方法
                        //這裡執行在主執行緒。
                    }
                });
            }
        }).start();複製程式碼

其實post()方法和sendMessage()方法的邏輯是一樣的,post()方法中的Runnable會被封裝成Message,然後傳送出去。下面看它的原始碼:

    public final boolean post(Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }複製程式碼

從原始碼中我們看到,Runnable會被封裝成Message,然後還是用sendMessage的方式傳送出去。而且封裝的邏輯也很簡單,直接把Runnable賦值給Message的callback就可以了。在上一篇文章中我介紹了訊息處理的三個方法,Message自己的callback優先順序是最高的,所以這個訊息是由自己的callback也就是Runnable的run()方法處理的,而不再是Handler的handleMessage()方法。
前面我們所舉的例子中,都是訊息的處理是在主執行緒的。其實不然,訊息的處理事實上是執行在負責管理訊息佇列(MessageQueue)的Looper所在的執行緒的,而不一定是主執行緒。這一點我在上一篇文章也反覆的提到了,如果還不瞭解這一點的請認真閱讀一下我的上一篇文章。
如果我們建立一個Handler物件而沒有給它指定它的Looper,那麼它預設會使用當前執行緒的Looper。前面我們所舉的例子都是在主執行緒直接建立Handler物件的,所以它的Looper就是主執行緒的Looper,它的訊息自然也就是在主執行緒處理了。那麼我們也可以在子執行緒使用Handler,下面可一個例子:

    //宣告Handler;
    Handler handler;
    new Thread(new Runnable() {
        @Override
        public void run() {
        //建立當前執行緒的Looper
            Looper.prepare();
            //在子執行緒建立handler物件
            handler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                //這裡是訊息處理,它是執行在子執行緒的
                }
           };
           //開啟Looper的訊息輪詢
           Looper.loop();
       }
   }).start();

   mBanner.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
       //在主執行緒傳送一個訊息到子執行緒
           Message msg = new Message();
           handler.sendMessage(msg);
       }
   });複製程式碼

在上面的例子中,我們在子執行緒建立handler物件,handler的Looper就是子執行緒的Looper,所以訊息的處理也就是在子執行緒處理的。這就是在子執行緒使用Handler的方式。在這裡有幾個需要注意的點:(很重要)
1、在子執行緒使用Handler前一定要先為子執行緒建立Looper,建立的方式是直接呼叫Looper.prepare()方法。前面我們說過建立Handler物件時如果沒有給它指定Looper,那麼它預設會使用當前執行緒的Looper,而執行緒預設是沒有Looper的,所以使用前一定要先建立Looper。
2、在同一個執行緒裡,Looper.prepare()方法不能被呼叫兩次。因為同一個執行緒裡,最多隻能有一個Looper物件。
3、只有呼叫了Looper.loop()方法,Handler機制才能正常工作。 Looper負責管理MessageQueue,它的loop()方法負責從MessageQueue裡取出訊息並交給Handler處理,所以如果沒有呼叫Looper.loop()方法,訊息就不會被取出和處理。
4、Looper.loop()方法一定要在呼叫了Looper.prepare()方法之後呼叫。那是因為如果當前執行緒還沒有Looper,是不能呼叫Looper.loop()方法開啟訊息輪詢的,否則會報錯。
5、不要在主執行緒呼叫Looper.prepare()方法。這是因為在Android系統建立主執行緒的時候就已經呼叫了Looper.prepare()方法和Looper.loop()方法,這也是為什麼我們在主執行緒使用Handler時不需要呼叫這兩個方法的原因。
6、當我們在子執行緒使用Handler時,如果Handler不再需要傳送和處理訊息,那麼一定要退出子執行緒的訊息輪詢。因為Looper.loop()方法裡是一個死迴圈,如果我們不主動結束它,那麼它就會一直執行,子執行緒也會一直執行而不會結束。退出訊息輪詢的方法是:

    Looper.myLooper().quit();
    Looper.myLooper().quitSafely();複製程式碼

上面的例子都是用執行緒自己的Looper來建立Handler,我們也可以用指定的Looper來建立Handler。

    new Thread(new Runnable() {
        @Override
        public void run() {
            //獲取主執行緒的Looper
            Looper looper = Looper.getMainLooper();
            //用主執行緒的Looper建立Handler
            handler = new Handler(looper) {
                @Override
                public void handleMessage(Message msg) {
                //這裡是執行在主執行緒的
                }
            };
        }
    }).start();複製程式碼

上面的例子中,我們雖然是在子線建立Handler,但因為用的是主執行緒的Looper,所以訊息的處理是在主執行緒的,這跟在主執行緒建立Handler是一樣的。因為這裡並沒有使用到子執行緒的Looper,所以不要呼叫Looper.prepare()和Looper.loop()方法。
以上我們所說的是Handler切換執行緒的使用。Handler除了提供post()方法和sendMessage()方法以外,還提供了一系列的傳送訊息的方法。比如延時傳送訊息和定時傳送訊息:

    //延時傳送訊息
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    public final boolean postDelayed(Runnable r, long delayMillis);

    //定時傳送訊息
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    public final boolean postAtTime(Runnable r, long uptimeMillis);
    public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);複製程式碼

通過這些方法,可以實現延時執行方法和定時執行方法的功能。如下面的例子:

    Handler handler = new Handler();
     handler.postDelayed(new Runnable() {
         @Override
         public void run() {
             Log.i(TAG, "延時1000毫秒列印");
         }
     },1000);複製程式碼

上面的例子並沒有涉及到執行緒的切換,只是利用了Handler延時傳送訊息的功能達到延時列印。所以Handler的使用不僅僅是切換執行緒。更多的方法使用就不一一舉例了。
小知識點:
1、使用Message.obtain()來獲取一個訊息。前面我們的例子中獲取一個訊息都是用new的方式直接建立,我這樣做只是為了方便大家的理解而已。在使用中不推薦用這種方式來獲取一個訊息,而是使用Message.obtain()方法。

Message msg = Message.obtain();複製程式碼

Android會為Message提供一個快取池,把使用過的訊息快取起來,方便下次使用。我們用Message.obtain()方法獲取一個訊息時,會先從快取池獲取,如果快取池沒有訊息,才會去建立訊息。這樣做可以優化記憶體。
2、同一個Message不要傳送兩次。如下面的程式碼是有問題的:

    //同一個Message傳送了兩次
    Message msg = Message.obtain();
    handler.sendMessage(msg);
    handler.sendMessage(msg);複製程式碼

這是因為所以的訊息都是傳送到MessageQueue存放,然後等待處理的。如果一個Message物件在MessageQueue裡,再次把它存到MessageQueue時就會報錯。
3、Android已經提供了很多實現了Handler的類和方法,方便我們使用。如Activity類的runOnUiThread()方法,View的post()方法,HandlerThread類等,關於這些知識,大家可以查閱相關資料,這裡就不做講解了,因為他們的實現其實跟我們前面說的是一樣的。

文章已同步到我的CSDN部落格blog.csdn.net/u010177022/…

相關文章