1. 程式人生 > >Handler執行機制中必須明白的幾個問題

Handler執行機制中必須明白的幾個問題

概述

我在看完Handler的原始碼後有兩個感覺,一是貌似明白了很多東西,二是當問到具體問題時感覺還是模模糊糊。下面我們就帶著問題再看一次原始碼,力爭把這塊知識點搞的明明白白。

問題有:

  1. 在UI執行緒中有幾個Looper物件?有幾個MessageQueue物件?有幾個Handler物件?有幾個Message物件?
  2. 怎麼保證只有一個Looper物件的?
  3. 怎麼保證只有一個MessageQueue物件的?
  4. 為什麼傳送訊息在子執行緒,而處理訊息就變成主執行緒了,在哪兒跳轉的?
  5. looper物件只有一個,在分發訊息時怎麼區分不同的handler?
  6. 能不能在子執行緒中建立Handler物件?
  7. 怎麼在子執行緒中得到主執行緒中handler物件?

如果上面七個問題你都能清晰的回答,那麼恭喜你,你對Handler的理解已經很透徹了。如果你有時間,還是希望你能繼續往下看,可以給我找錯誤,找到我理解偏差的地方,找到有獎哦。或者您也可以提出問題讓我回答,大家共同學習。

在UI執行緒中有幾個Looper物件?有幾個MessageQueue物件?有幾個Handler物件?有幾個Message物件?

在UI執行緒中只有一個Looper物件,只有一個MessageQueue物件。但可以有很多個handler物件。可以有很多個Message物件。

怎麼保證只有一個Looper物件的?

在Handler機制中使用Looper物件的地方有三個,一是在ActivityThread類中使用Looper.prepareMainLooper()建立Looper物件,二是在hanlder類中使用Looper物件,三是在Looper的loop方法中使用Looper物件處理訊息。若這三個Looper物件是同一個就證明了在UI執行緒只有一個Looper物件。

一,Looper.prepareMainLooper()建立Looper物件

prepareMainLooper方法的原碼是:

    public static void prepareMainLooper() {
        prepare(false);//建立Looper物件,並儲存到ThreadLocal中
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared."
); } sMainLooper = myLooper();//得到Looper物件,並儲存到Looper類的靜態欄位中 } }

下面看prepare方法的原碼:

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

分析:在第一次呼叫sThreadLocal.get()方法得到的一定是null,所以此時的重點是建立Looper物件,並放入sThreadLocal中儲存起來。
這兒要明確兩點:
1. sThreadLocal是Looper類的靜態欄位,所以只有一個sThreadLocal物件。
2. prepare方法在UI執行緒被呼叫,所以只有在Ui執行緒才能從sThreadLocal物件中獲取到looper物件。
下面看myLooper方法的原碼:

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

分析:這個方法很簡單,目的就是得到UI執行緒中的Looper物件。注意這個方法是靜態方法,得到的Looper物件就是在UI執行緒中建立的Looper物件。

二,在Hanlder類中使用Looper物件

我們知道在建立Handler物件時使用的是無參構造,但無參構造方法呼叫了下面的構造方法:

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

分析:由原碼可知,Handler類中有mLooper欄位和mQueue欄位。mLooper欄位賦值時通過Looper.myLooper()方法,我們從上面知道這個方法返回的值就是UI執行緒中建立的Looper物件,所以此時的Looper物件和Ui執行緒中建立的Looper物件是同一個。

三,在Looper的loop方法中使用Looper物件

在Looper類的loop方法中獲取Looper物件的程式碼是:

final Looper me = myLooper();

很明顯通過myLooper方法得到的Looper物件就是UI執行緒中建立的Looper物件。

怎麼保證只有一個MessageQueue物件的?

首先我們找一下MessageQueue物件是在哪兒建立的?
我們知道在在Looper的prepare方法中建立了Looper物件,並放入到ThreadLocal中,具體程式碼是:

sThreadLocal.set(new Looper(quitAllowed));

那麼我們來看一下Looper的構造方法:

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

在Looper的構造方法中建立了MessageQueue物件,並賦值給mQueue欄位。因為Looper物件只有一個,那麼Messagequeue物件肯定只有一個。

下面我們再多學習一點,找到使用MessageQueue使用的地方。MessageQueue被兩個地方使用,一是在handler的sendMessage中傳送訊息。二是在looper類的loop方法中取出訊息。

  1. 在看Handler的構造方法中我們知道,在構造方法中通過Looper.myPrepare方法得到mLooper物件,又通過mLooper.mQueue得到Messagequeue物件。所以此時這個MessageQueue物件就是在Looper的構造方法中建立的物件。
  2. 在Looper的loop方法中也是首先通過Looper.myPrepare()方法得到Looper物件,然後得到MessageQueue物件。

為什麼傳送訊息在子執行緒,而處理訊息就變成主執行緒了,在哪兒跳轉的?

傳送訊息使用的是Handler的sendMessage方法,這個方法最終呼叫的是enqueueMessage方法,enqueueMessage方法的原碼是:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

這個方法中呼叫的是MessageQueue的enqueueMessage方法,只要queue物件是UI執行緒中的MessageQueue物件,那麼就能被Ui執行緒中的Looper從訊息佇列中取出來,然後就在主執行緒執行了。我們知道Handler物件中的mQueue就是UI執行緒中的訊息佇列物件,所以在處理訊息時就是主執行緒了。

looper物件只有一個,在分發訊息時怎麼區分不同的handler?

關於這個問題主要看三點:
一是Message類中有個欄位target,這個欄位是Handler型別的。
二在Handler的enqueueMessage方法中有這麼一句程式碼:msg.target = this;即把handler物件與Message繫結在了一起。
三在Looper類的looper方法中分發訊息的程式碼是:msg.target.dispatchMessage(msg);

此時我們就明白了:在傳送訊息時handler物件與Message物件繫結在了一起。在分發訊息時首先取出Message物件,然後就可以得到與它繫結在一起的Handler物件了。

能不能在子執行緒中建立Handler物件?

這個問題並不是簡單的能不能的問題,考察的是對Handler的深入理解。答案肯定是可以的,但是僅僅使用無參構造是不可以的,還需要做其他的操作。

一,僅僅使用無參構造為什麼不能建立Handler物件?

我們知道Handler的無參構造最終呼叫了是另一個構造方法,下面還是看這個構造方法的原碼:

    public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
    }

分析:在上面程式碼中我們看到,如果得到的Looper物件是null就會丟擲異常。為什麼在子執行緒中Looper.myLooper方法會返回null呢?原因就是ThreadLocal的特性了。Looper.myLooper方法的原碼是從ThreadLocal中得到Looper物件,而在Looper.prepare方法中Looper物件是在UI執行緒中放入到ThreadLocal中的,所以在子執行緒中是得不到Looper物件的。在handler中沒有looper物件就會丟擲異常。

為什麼在handler中沒有looper物件就會丟擲異常?

我們知道handler傳送訊息最終呼叫的是MessageQueue的enqueueMessage方法,如果沒有Looper物件,肯定得不到MessageQueue物件,在傳送訊息時一定會丟擲nullPointException。所以系統在前面就進行了攔截,只要沒有Looper物件就不讓程式碼繼續執行。

怎麼在子執行緒中建立Handler物件?

在UI執行緒之所以可以直接建立建立Handler物件,是因為在Ui執行緒已經有了Looper物件,所以只要我們在子執行緒中建立Looper物件後就可以建立handler物件了。使用示例如下:

 new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();//建立looper物件
                Looper.loop();//開啟子執行緒中的訊息迴圈
                Handler handler = new Handler(){//建立Handler物件
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                    }
                };
            }
        }).start();

此時就在子執行緒中維護了一套訊息迴圈系統。
注意:這套訊息迴圈系統與UI執行緒中的Handler機制沒有任何關係,這兒的訊息不能與Ui執行緒中的訊息佇列進行通訊。但子執行緒中的訊息迴圈系統也有很大的作用,當需要處理很多訊息時可以讓訊息按序列執行。在圖片載入框架Picasso中就在子執行緒中維護了一套訊息迴圈系統,感興趣的小夥伴可以自行檢視Picasso的原碼。

怎麼在子執行緒中得到主執行緒中handler物件?

其實handler物件沒有主執行緒和子執行緒之分,有區分的是Looper物件,如果Looper物件是主執行緒中的,那麼handler就是主執行緒中的。
Handler有下面一個構造方法:

    public Handler(Looper looper) {
        this(looper, null, false);
    }

這個構造方法中接收一個Looper物件。只要我們能到子執行緒中得到主執行緒的Looper物件,那麼就可以實現在子執行緒中得到主執行緒中的handler物件了。
在Looper類中有getMainLooper方法,這個方法的原始碼是:

    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

這個方法返回的是在主執行緒中建立過的Looper物件。所以這個方法無論在哪兒呼叫得到的都是主執行緒中的Looper物件,此時我們就可以在子執行緒中建立主執行緒的handler物件了,程式碼如下:

new Thread(new Runnable() {
     @Override
     public void run() {
         Handler handler = new Handler(Looper.getMainLooper());//得到UI執行緒中的handler物件
         handler.post(new Runnable() {
            @Override
             public void run() {
                //這兒寫邏輯程式碼
             }
         });
     }
 }).start();

注:使用這種方法可以輕鬆的從子執行緒跳轉到UI執行緒,完全不依賴於Activity或Application。圖片載入框架Picasso,網路請求控制元件Volley都是通過這種方法在工具類內部實現了更新UI。