Handler訊息傳遞機制

bby發表於2018-04-12

一、Handler訊息傳遞機制和深入認識(在一個執行緒中,可以有多個Handler嗎?):

(一)、引入:

子執行緒沒有辦法對UI介面上的內容進行操作,如果操作,將丟擲異常:CalledFromWrongThreadException

為了實現子執行緒中操作UI介面,Android中引入了Handler訊息傳遞機制,目的是打破對主執行緒的依賴性。

什麼是Handler?

handler通俗一點講就是用來在各個執行緒之間傳送資料的處理物件,再簡單點講就是一個用來處理不同執行緒間通訊的類。

在任何執行緒中,只要獲得了另一個執行緒的handler,則可以通過 handler.sendMessage(message)方法向那個執行緒傳送資料。基於這個機制,我們在處理多執行緒的時候可以新建一個thread,這個thread擁有UI執行緒中的一個handler。當thread處理完一些耗時的操作後通過傳遞過來的handler向UI執行緒傳送資料,由UI執行緒去更新介面。 主執行緒:執行所有UI元件,它通過一個訊息佇列來完成此任務。裝置會將使用者的每項操作轉換為訊息,並將它們放入正在執行的訊息佇列中。主執行緒位於一個迴圈中,並處理每條訊息。如果任何一個訊息用時超過5秒,Android將丟擲ANR。所以一個任務用時超過5秒,應該在一個獨立執行緒中完成它,或者延遲處理它,當主執行緒空閒下來再返回來處理它。

(二)、常用類:(Handler、Looper、Message、MessageQueue)

  1. Message:訊息,被傳遞和處理的資料。其中包含了訊息ID,訊息處理物件以及處理的資料等,由MessageQueue統一列隊,終由Handler處理。
  2. Handler:處理者,負責Message的傳送及處理。使用Handler時,需要實現handleMessage(Message msg)方法來對特定的Message進行處理,例如更新UI等。Handler類的主要作用:(有兩個主要作用)1)、在工作執行緒中傳送訊息;2)、在主執行緒中獲取、並處理訊息。
  3. MessageQueue:訊息佇列,本質是一個資料結構,用來存放Handler傳送過來的訊息,並按照FIFO規則執行。當然,存放Message並非實際意義的儲存,而是將Message串聯起來,等待Looper的抽取。
  4. Looper:訊息泵或迴圈器,不斷從MessageQueue中抽取Message。因此,一個MessageQueue需要一個Looper。
  5. Thread:執行緒,負責排程整個訊息迴圈,即訊息迴圈的執行場所。

(三)、Handler、Looper、Message、MessageQueue之間的關係:

  1. Looper和MessageQueue一一對應,建立一個Looper的同時,會建立一個MessageQueue;
  2. 而Handler與它們的關係,只是簡單的聚集關係,即Handler裡會引用當前執行緒裡的特定Looper和MessageQueue;
  3. 在一個執行緒中,只能有一個Looper和MessageQueue,但是可以有多個Handler,而且這些Handler可以共享一個Looper和MessageQueue;
  4. Message被存放在MessageQueue中,一個MessageQueue中可以包含多個Message物件。

【備註:】 Looper物件用來為一個執行緒開啟一個訊息迴圈,從而操作MessageQueue; 預設情況下,Android建立的執行緒沒有開啟訊息迴圈Looper,但是主執行緒例外。 系統自動為主執行緒建立Looper物件,開啟訊息迴圈; 所以主執行緒中使用new來建立Handler物件。而子執行緒中不能直接new來建立Handler物件就會異常。 子執行緒中建立Handler物件,步驟如下:

Looper.prepare();
Handler handler = new Handler() {
//handlemessage(){}
}
Looper.loop();
複製程式碼

(四)、Handler類中常用方法:

  1. handleMessage() 用在主執行緒中,構造Handler物件時,重寫handleMessage()方法。該方法根據工作執行緒返回的訊息標識,來分別執行不同的操作。
  2. sendEmptyMessage() 用在工作執行緒中,傳送空訊息。
  3. sendMessage() 用在工作執行緒中,立即傳送訊息。

(五)、Message訊息類中常用屬性:

  1. arg1 用來存放整型資料
  2. arg2 用來存放整型資料
  3. obj 用來存放Object資料
  4. what 用於指定使用者自定義的訊息程式碼,這樣便於主執行緒接收後,根據訊息程式碼不同而執行不同的相應操作。

【重點】:使用Message需要注意4點: 1、Message雖然也可以通過new來獲取,但是通常使用Message.obtain()或Handler.obtainMessage()方法來從訊息池中獲得空訊息物件,以節省資源; 2、如果一個Message只需要攜帶簡單的int型資料,應優先使用arg1和arg2屬性來傳遞資料,這樣比其他方式節省記憶體; 3、儘可能使用Message.what來標識資訊,以便用不同的方式處理Message; 4、如果需要從工作執行緒返回很多資料資訊,可以藉助Bundle物件將這些資料集中到一起,然後存放到obj屬性中,再返回到主執行緒。

(六)、示例程式碼一:

private Handler handler = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text_main_info = (TextView) findViewById(R.id.text_main_info);
        pDialog = new ProgressDialog(MainActivity.this);
        pDialog.setMessage("Loading...");
        image_main = (ImageView) findViewById(R.id.image_main);

        // 主執行緒中的handler物件會處理工作執行緒中傳送的Message。根據Message的不同編號進行相應的操作。
        handler = new Handler() {
                public void handleMessage(android.os.Message msg) {
                        // 工作執行緒中要傳送的資訊全都被放到了Message物件中,也就是上面的引數msg中。要進行操作就要先取出msg中傳遞的資料。
                        switch (msg.what) {
                        case 0:
                                // 工作執行緒傳送what為0的資訊代表執行緒開啟了。主執行緒中相應的顯示一個進度對話方塊
                                pDialog.show();
                                break;
                        case 1:
                                // 工作執行緒傳送what為1的資訊代表要執行緒已經將需要的資料載入完畢。本案例中就需要將該資料獲取到,顯示到指定ImageView控制元件中即可。
                                image_main.setImageBitmap((Bitmap) msg.obj);
                                break;
                        case 2:
                                // 工作執行緒傳送what為2的資訊代表工作執行緒結束。本案例中,主執行緒只需要將進度對話方塊取消即可。
                                pDialog.dismiss();
                                break;
                        }
                }
        };

        new Thread(new Runnable() {
                @Override
                public void run() {
                        // 當工作執行緒剛開始啟動時,希望顯示進度對話方塊,此時讓handler傳送一個空資訊即可。
                        // 當傳送這個資訊後,主執行緒會回撥handler物件中的handleMessage()方法。handleMessage()方法中
                        // 會根據message的what種類來執行不同的操作。
                        handler.sendEmptyMessage(0);

                        // 工作執行緒執行訪問網路,載入網路圖片的任務。
                        byte[] data = HttpClientHelper.loadByteFromURL(urlString);
                        // 工作執行緒將網路訪問獲取的位元組陣列生成Bitmap點陣圖。
                        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0,
                                        data.length);
                        // 工作執行緒將要傳送給主執行緒的資訊都放到一個Message資訊物件中。
                        // 而Message物件的構建建議使用obtain()方法生成,而不建議用new來生成。
                        Message msgMessage = Message.obtain();
                        // 將需要傳遞到主執行緒的資料放到Message物件的obj屬性中,以便於傳遞到主執行緒。
                        msgMessage.obj = bitmap;
                        // Message物件的what屬性是為了區別資訊種類,而方便主執行緒中根據這些類別做相應的操作。
                        msgMessage.what = 1;
                        // handler物件攜帶著Message中的資料返回到主執行緒
                        handler.sendMessage(msgMessage);

                        // handler再發出一個空資訊,目的是告訴主執行緒工作執行緒的任務執行完畢。一般主執行緒會接收到這個訊息後,
                        // 將進度對話方塊關閉
                        handler.sendEmptyMessage(2);
                }
        }).start();
}
複製程式碼

(七)、示例程式碼二:圖片定時切換

1、思路:利用多執行緒,子執行緒每隔2秒傳送一個訊息給主執行緒,主執行緒中Handler接收訊息,並更新ImageView中的圖片。這樣就實現了迴圈切換的動態效果。

handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what) {
                case 0:
                        image_main_pic.setImageResource(imageId[position++]);
                        if (position >= imageId.length) {
                                position = 0;
                        }
                        break;
                default:
                        break;
                }
        }
};

// 第一種解決辦法:利用Thread和Thread的sleep
// new Thread(new Runnable() {
// @Override
// public void run() {
// while (flag) {
// try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// handler.sendEmptyMessage(0);
// }
// }
// }).start();

// 第二種解決辦法:利用Timer定時器和定時器的schedule()方法。
//schedule()方法中有三個引數:
/*第一個:表示定時任務TimerTask。 TimerTask 類實現了Runnable介面,所以要new  TimerTask(),一定要實現run()方法。
第二個:表示第一次執行前的等待延遲時間;
第三個:表示兩次定時任務執行之間的間隔時間。*/
new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
                handler.sendEmptyMessage(0);
                //sendEmptyMessage()方法等同於以下幾句話。所以。如果只傳送一個what,就可以使用sendEmptyMessage()。這樣更簡單。
                //Message message = Message.obtain();
                // Message message2 = handler.obtainMessage();
                //message.what = 0;
                //handler.sendMessage(message);
        }
}, 1, 1500);
複製程式碼

二、Handler、Looper原始碼分析:

(一)、Handler的概念:

  1. Handler是用於傳送和處理訊息和一個執行緒的MessageQueue相關聯的Runable物件。
  2. 每個Handler例項關聯到一個單一執行緒和執行緒的messagequeue。
  3. 當您建立一個Handler,從你建立它的時候開始,它就繫結到建立它的執行緒以及對應的訊息佇列,handler將傳送訊息到訊息佇列,並處理從訊息佇列中取出的訊息。

Handler的主要用途有兩個:

(1)、在將來的某個時刻執行訊息或一個runnable;

(2)、為執行在不同執行緒中的多個任務排隊。

主要依靠以下方法來完成訊息排程:
  ● post(Runnable)、
  ● postAtTime(Runnable, long)、
  ● postDelayed(Runnable, long)、
  ● sendEmptyMessage(int)、
  ● sendMessage(Message)、
  ● sendMessageAtTime(Message)、
  ● sendMessageDelayed(Message, long)
【備註:】
  ● post方法是當到Runable物件到達就被插入到訊息佇列;
  ● sendMessage方法允許你把一個包含有資訊的Message插入訊息佇列,它會在Handler的handlerMessage(Message)方法中執行(該方法要求在Handler的子類中實現)。
  ● 當Handler post或者send訊息的時候,可以在訊息佇列準備好的時候立刻執行,或者指定一個延遲處理或絕對時間對它進行處理,後兩個是實現了timeout、ticks或者其他timing-based的行為。
  ● 當你的應用建立一個程式時,其主執行緒(UI執行緒)會執行一個訊息佇列,負責管理優先順序最高的應用程式物件(Activity、廣播接收器等)和任何他們建立的windows。你也可以建立自己的執行緒,通過handler與主執行緒進行通訊,在新建立的執行緒中handler通過呼叫post或sendMessage方法,將傳入的Runnable或者Message插入到訊息佇列中,並且在適當的時候得到處理。
複製程式碼

(二)、Handler的用法:

當你例項化一個Handler的時候可以使用Callback介面來避免寫自定義的Handler子類。這裡的機制類似與Thread與runable介面的關係。 在Handler裡面,子類要處理訊息的話必須重寫handleMessage()這個方法,因為在handler裡面它是個空方法:

(三)、Handler、Looper、Message、MessageQueue訊息傳遞機制原始碼分析:

A、Handler.java:(3個屬性,10個方法)
3個屬性:
  ● final MessageQueue mQueue; 封裝好的Message被handler傳送出去,其實就是放到了MessageQueue訊息佇列中。
  ● final Looper mLooper; 每個handler都有一個looper為其不斷接收訊息佇列中的訊息,並返回給handler。
  ● final Callback mCallback; 被handler接收到的訊息要通過Callback介面中的handleMessage()方法來進行處理。

10個方法:
  ● public boolean handleMessage(Message msg); Callback介面中的handleMessage()方法,用來處理返回給handler的訊息的。
  ● public final Message obtainMessage() 獲取Message物件,其本質還是呼叫Message類的obtain()方法來獲取訊息物件。
  ● public final boolean sendMessage(Message msg) 傳送訊息
  ● public final boolean sendEmptyMessage(int what) 傳送只有what屬性的訊息
  ● public final boolean post(Runnable r) 傳送訊息
  ● public final boolean postAtTime(Runnable r, long uptimeMillis) 定時傳送訊息
  ● public final boolean postDelayed(Runnable r, long delayMillis) 延遲傳送訊息
  ● public void dispatchMessage(Message msg) 分發訊息。當Looper迴圈接收訊息佇列中的訊息時,就在不斷呼叫handler的分發訊息方法,從而觸發Callback介面中的訊息處理方法handleMessage()來對訊息進行處理。
  ● public final boolean sendMessageDelayed(Message msg, long delayMillis) sendMessage()就是在呼叫延時傳送訊息的方法。
  ● public boolean sendMessageAtTime(Message msg, long uptimeMillis) 延時傳送訊息的方法就是在呼叫定時傳送訊息的方法。
  ● private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) 將訊息壓入訊息佇列中

B、MessageQueue.JAVA:(1個方法)
  ● final boolean enqueueMessage(Message msg, long when) handler傳送的訊息正是通過該方法被加進了訊息佇列中。因為訊息佇列中有訊息,從而觸發了Looper通過loop()方法迴圈接收所有的訊息。

C、Looper.JAVA:(3個屬性,5個方法)
3個屬性:
  ●     static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();   正是由於該執行緒本地變數,保證了一個執行緒中只能儲存一個Looper物件,也就是說一個Thread中只能有一個Looper物件。
  ●     final MessageQueue  mQueue;    正是由於MessagQueue中的訊息,才觸發了Looper的loop()方法。
  ●     private static Looper mMainLooper = null;    該屬性是主執行緒中自動建立的Looper物件。

5個方法:
  ● public static void prepare()            每個handler所在的執行緒都必須有一個Looper提前準備好。
  ● public static void prepareMainLooper()         主執行緒中的的Looper已經被自動準備好。而該方法不需要手工呼叫。
  ● public static Looper getMainLooper()            獲取主執行緒中的Looper物件
  ● public static void loop()                 Looper的作用就是迴圈接收訊息佇列中的訊息。該方法中執行了一個無限迴圈。
  ● public static Looper myLooper()       該方法的作用是從執行緒本地變數中獲取到當前執行緒的Looper物件。

D、Message.java:(10個屬性,7個方法)
10個屬性:
  ● public int what; 該屬性一般用來標識訊息執行的狀態。
  ● public int arg1; 用來存放整型資料的屬性。
  ● public int arg2; 用來存放整型資料的屬性。
  ● public Object obj; 用來存放複雜訊息資料的屬性。
  ● Bundle data; 用來存放複雜訊息資料的屬性。
  ● Handler target; 標識該訊息要被髮送給哪個Handler物件。
  ● Message sPool; 訊息池物件。
  ● int sPoolSize; 記錄訊息池中剩餘訊息的數量。
  ● int MAX_POOL_SIZE=50; 訊息池中最大訊息數量。
  ● Messenger replyTo; 定義訊息的信使物件,將來可以用於跨APP的訊息傳遞。

7個方法:
  ● public static Message obtain() 從訊息池中獲取訊息物件。
  ● public void recycle() 往訊息池中歸還訊息物件。
  ● public void setTarget(Handler target) 給訊息設定接收該訊息的目標handler物件。
  ● public Handler getTarget() 獲取訊息的接收handler物件。
  ● public void sendToTarget() 將訊息傳送到目標handler物件。其本質是呼叫handler物件的sendMessage()方法來傳送當前訊息物件。
  ● public void setData(Bundle data) 將Bundle物件設定到message物件中Bundle屬性中。
  ● public Bundle getData() 從訊息物件中獲取Bundle屬性的資料。
複製程式碼

相關文章