Android 中線程間通信原理分析:Looper, MessageQueue, Handler

在我們去討論Handler,Looper,MessageQueue的關係之前,我們需要先問兩個問題:

  1. 這一套東西搞出來是爲了解決什麼問題呢?

  2. 如果讓我們來解決這個問題該怎麼做?

以上者兩個問題,是我最近總結出來的,在我們學習瞭解一個新的技術之前,最好是先能回答這兩個問題,這樣你才能對你正在學習的東西有更深刻的認識。

第一個問題:google的程序員們搞出這一套東西是爲了解決什麼問題的?這個問題很顯而易見,爲了解決線程間通信的問題。我們都知道,Android的UI/View這一套系統是運行在主線程的,並且這個主線程是死循環的,來看看具體的證據吧。

public final class ActivityThread {
    public static void main(String[] args) {
        
        //...
        
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}

如上面的代碼示例所示,ActivityThread.main()方法作爲Android程序的入口,裏面我省略了一些初始化的操作,然後就執行了一句Looper.loop()方法,就沒了,再下一行就拋異常了。

loop()方法裏面實際上就是一個死循環,一直在執行着,不斷的從一個MQ(MessageQueue,後面我都縮寫成MQ了)去取消息,如果有的話,那麼就執行它或者讓它的發送者去處理它。

一般來說,主線程循環中都是執行着一些快速的UI操作,當你有手touch屏幕的時候,系統會產生事件,UI會處理這些事件,這些事件都會在主線程中執行,並快速的響應着UI的變化。如果主線程上發生一些比較耗時的操作,那麼它後面的方法就無法得到執行了,那麼就會出現卡頓,不流暢。

因此,Android並不希望你在主線程去做一些耗時的操作,這裏對“耗時”二字進行樸素的理解就行了,就是執行起來需要消耗的時間比較多的操作。比如讀寫文件,小的文件也許很快,但你無法預料文件的大小,再比如訪問網絡,再比如你需要做一些複雜的計算等等。

爲了不阻礙主線程流暢的執行,我們就必須在需要的時候把耗時的操作放到其他線程上去,當其他線程完成了工作,再給一個通知(或許還帶着數據)給到主線程,讓主線程去更新UI什麼的,當然了,如果你要的耗時操作只是默默無聞的完成就行了,並不需要通知UI,那麼你完全不需要給通知給到UI線程。這就是線程間的通信,其他線程做耗時操作,完成了告訴UI線程,讓它進行更新。爲了解決這個問題,Android系統給我們提供了這樣一套方案來解決。

第二個問題:如果讓我們來想一套方案來解決這個線程間通信的問題,該怎麼做呢?

先看看我們現在已經有的東西,我們有一個一直在循環的主線程,它實現起來大概是這個樣子:

public class OurSystem {
    public static void main(String [] args) {
        for (;;) {
            //do something...
        }
    }
}

爲什麼主線程要一直死循環的執行呢?

關於這一點,我個人並沒有特別透徹的認知,但我猜測,對於有GUI的系統/程序,應該都有一個不斷循環的主線程,因爲這個GUI程序肯定是要跟人進行交互的,也就是說,需要等待用戶的輸入,比如觸碰屏幕,動動鼠標,敲敲鍵盤什麼的,這些事件肯定是硬件層先獲得一個響應/信號,然後會不斷的向上封裝傳遞。

如果說我們一碰屏幕,一碰鼠標,就開啓一個新線程去處理UI上的變化,首先,這當然是可以的!UI在什麼線程上更新其實都是可以的嘛,並不是說一定要在主線程上更新,這是系統給我設的一個套子。然後,問題也會複雜的多,如果我們快速的點擊2下鼠標,那麼一瞬間就開啓了兩個新線程去執行,那麼這兩個線程的執行順序呢?兩個獨立的線程,我們是無法保證說先啓動的先執行。

所以第一個問題就是執行順序的問題。

第二個問題就是同步,幾個相互獨立的線程如果要處理同一個資源,那麼造成的結果都是令人困惑,不受控制的。另一方面強行給所有的操作加上同步鎖,在效率上也會有問題。

爲了解決順序執行的問題,非常容易就想到的一種方案是事件隊列,各種各樣的事件先進入到一個隊列中,然後有個東西會不斷的從隊列中獲取,這樣第一個事件一定在第二個事件之前被執行,這樣就保證了順序,如果我們把這個“取事件”的步驟放在一個線程中去做,那麼也順便解決了資源同步的問題。

因此,對於GUI程序會有一個一直循環的(主)線程,可能就是這樣來的吧。

這是一個非常純淨的死循環,我們想要做一些事情的話,就得讓它從一個隊列裏面獲取一些事情來做,就像打印機一樣。因此我們再編寫一個消息隊列類,來存放消息。消息隊列看起來應該是這樣:

public class OurMessageQueue() {
    private LinkedList<Message> mQueue = new LinkedList<Message>();
    
    // 放進去一條消息
    public void enQueue() {
        //...
    }
    
    // 取出一條消息
    public Message deQueue() {
        //...
    }
    
    // 判斷是否爲空隊列
    public boolean isEmpty() {
        //...
    }
}

接下來我們的循環就需要改造成能從消息隊列裏獲取消息,並能夠根據消息來做些事情了:

public class OurSystem {
    public static void main(String [] args) {
        
        // 初始化消息隊列
        OurMessageQueue mq = ...
    
        for (;;) {
            if (!mq.isEmpty()) {
                Message msg = mq.deQueue();
                //do something...
            }
        }
    }
}

現在我們假象一下,我們需要點擊一下按鈕,然後去下載一個超級大的文件,下載完成後,我們再讓主線程顯示文件的大小。

首先,按一下按鈕,這個事件應該會被觸發到主線程來(具體怎麼來的我還尚不清楚,但應該是先從硬件開始,然後插入到消息隊列中,主線程的循環就能獲取到了),然後主線程開啓一個新的異步線程來進行下載,下載完成後再通知主線程來更新,代碼看上去是這樣的:

// 腦補的硬件設備……
public class OurDevice {
    
    // 硬件設備可能有一個回調
    public void onClick() {
    
        // 先拿到同一個消息隊列,並把我們要做的事情插入隊列中
        OurMessageQueue mq = ...
        Message msg = Message.newInstance("download a big file");
        mq.enQueue(msg);
    }
}

然後,我們的主線程循環獲取到了消息:

public class OurSystem {
    public static void main(String [] args) {
        
        // 初始化消息隊列
        OurMessageQueue mq = ...
    
        for (;;) {
            if (!mq.isEmpty()) {
                Message msg = mq.deQueue();
                
                // 是一條通知我們下載文件的消息
                if (msg.isDownloadBigFile()) {
                
                    // 開啓新線程去下載文件
                    new Thread(new Runnable() {
                        void run() {
                            // download a big file, may cast 1 min...
                            // ...
                            // ok, we finished download task.
                            
                            // 獲取到同一個消息隊列
                            OurMessageQueue mq = ...
                            
                            // 消息入隊
                            mq.enQueue(Message.newInstance("finished download"));
                        }
                    }).start();
                }
                
                // 是一條通知我們下載完成的消息
                if (msg.isFilishedDownload()) {
                    // update UI!
                }
            }
        }
    }
}

注意,主線程循環獲取到消息的時候,顯示對消息進行的判斷分類,不同的消息應該有不同的處理。在我們獲取到一個下載文件的消息時,開啓了一個新的線程去執行,耗時操作與主線程就被隔離到不同的執行流中,當完成後,新線程中用同一個消息隊列發送了一個通知下載完成的消息,主線程循環獲取到後,裏面就可以更新UI。

這樣就是一個我隨意腦補的,簡單的跨線程通信的方案。

有如下幾點是值得注意的:

  • 主線程是死循環的從消息隊列中獲取消息。

  • 我們要將消息發送到主線程的消息隊列,我們需要通過某種方法能獲取到主線程的消息隊列對象

  • 消息(Message)的結構應該如何設計呢?

Android 中的線程間通信方案

Looper

android.os.Looper from Grepcode

Android中有一個Looper對象,顧名思義,直譯過來就是循環的意思,Looper也確實幹了維持循環的事情。

Looper的代碼是非常簡單的,去掉註釋也就300多行。在官方文檔的註釋中,它推薦我們這樣來使用它:

class LooperThread extends Thread {
    public Handler mHandler;

    public void run() {
        Looper.prepare();

        mHandler = new Handler() {
            public void handleMessage(Message msg) {
              // process incoming messages here
            }
        };

        Looper.loop();
    }
}

先來看看prepare方法幹了什麼。

Looper.prepare()

public static void prepare() {
    prepare(true);
}

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));
}

注意prepare(boolean)方法中,有一個sThreadLocal變量,這個變量有點像一個哈希表,它的key是當前的線程,也就是說,它可以存儲一些數據/引用,這些數據/引用是與當前線程是一一對應的,在這裏的作用是,它判斷一下當前線程是否有Looper這個對象,如果有,那麼就報錯了,"Only one Looper may be created per thread",一個線程只允許創建一個Looper,如果沒有,就new一個新的塞進這個哈希表中。然後它調用了Looper的構造方法。

Looper 的構造方法

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

Looper的構造方法中,很關鍵的一句,它new了一個MessageQueue對象,並自己維持了這個MQ的引用。

此時prepare()方法的工作就結束了,接下來需要調用靜態方法loop()來啓動循環。

Looper.loop()

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        msg.target.dispatchMessage(msg);

        //...
    }
}

loop()方法,我做了省略,省去了一些不關心的部分。剩下的部分非常的清楚了,首先調用了靜態方法myLooper()獲取一個Looper對象。

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

myLooper()同樣是靜態方法,它是直接從這個ThreadLocal中去獲取,這個剛剛說過了,它就類似於一個哈希表,key是當前線程,因爲剛剛prepare()的時候,已經往裏面set了一個Looper,那麼此時應該是可以get到的。拿到當前線程的Looper後,接下來,final MessageQueue queue = me.mQueue;拿到與這個Looper對應的MQ,拿到了MQ後,就開啓了死循環,對消息隊列進行不停的獲取,當獲取到一個消息後,它調用了Message.target.dispatchMessage()方法來對消息進行處理。

Looper的代碼看完了,我們得到了幾個信息:

  • Looper調用靜態方法prepare()來進行初始化,一個線程只能創建一個與之對應的LooperLooper初始化的時候會創建一個MQ,因此,有了這樣的對應關係,一個線程對應一個Looper,一個Looper對應一個MQ。可以說,它們三個是在一條線上的。

  • Looper調用靜態方法loop()開始無限循環的取消息,MQ調用next()方法來獲取消息

MessageQueue

android.os.MessageQueue from Grepcode

對於MQ的源碼,簡單的看一下,構造函數與next()方法就好了。

MQ的構造方法

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

MQ的構造方法簡單的調用了nativeInit()來進行初始化,這是一個jni方法,也就是說,可能是在JNI層維持了它這個消息隊列的對象。

MessageQueue.next()

Message next() {
    
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (false) Log.v("MessageQueue", "Returning message: " + msg);
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
        }

    }
}

next()方法的代碼有些長,我作了一些省略,請注意到,這個方法也有一個死循環,這樣做的效果就是,在Looper的死循環中,調用了next(),而next()這裏也在死循環,表面上看起來,方法就阻塞在Looper的死循環中的那一行了,知道next()方法能返回一個Message對象出來。

簡單瀏覽MQ的代碼,我們得到了這些信息:

  • MQ的初始化是交給JNI去做的

  • MQ的next()方法是個死循環,在不停的訪問MQ,從中獲取消息出來返回給Looper去處理。

Message

android.os.Message from Grepcode

Message對象是MQ中隊列的element,也是Handler發送,接收處理的一個對象。對於它,我們需要了解它的幾個成員屬性即可。

Message的成員變量可以分爲三個部分:

  • 數據部分:它包括what(int)arg1(int)arg2(int)obj(Object)data(Bundle)等,一般用這些來傳遞數據。

  • 發送者(target):它有一個成員變量叫target,它的類型是Handler的,這個成員變量很重要,它標記了這個Message對象本身是誰發送的,最終也會交給誰去處理。

  • callback:它有一個成員變量叫callback,它的類型是Runnable,可以理解爲一個可以被執行的代碼片段。

Handler

android.os.Handler from Grepcode

Handler對象是在API層面供給開發者使用最多的一個類,我們主要通過這個類來進行發送消息與處理消息。

Handler的構造方法(初始化)

通常我們調用沒有參數的構造方法來進行初始化,使用起來大概是這樣的:

Handler mHandler = new Handler() {
    handleMessage(Message msg) {
        //...
    }
}

沒有參數的構造方法最終調用了一個兩個參數的構造方法,它的部分源碼如下:

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()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

注意到,它對mLooper成員變量進行了賦值,通過Looper.myLooper()方法獲取到當前線程對應的Looper對象。上面已經提到過,如果Looper調用過prepare()方法,那麼這個線程對應了一個Looper實例,這個Looper實例也對應了一個MQ,它們三者之間是一一對應的關係。

然後它通過mLooper對象,獲取了一個MQ,存在自己的mQueue成員變量中。

Handler的初始化代碼說明了一點,Handler所初始化的地方(所在的線程),就是從將這個線程對應的Looper的引用賦值給Handler,讓Handler也持有

對於主線程來說,我們在主線程的執行流中,new一個Handler對象,Handler對象都是持有主線程的Looper(也就是Main Looper)對象的。

同樣的,如果我們在一個新線程,不調用Looper.prepare()方法去啓動一個Looper,直接new一個Handler對象,那麼它就會報錯。像這樣

new Thread(new Runnable() {
        @Override
        public void run() {
            //Looper.prepare(); 

            //因爲Looper沒有初始化,所以Looper.myLooper()不能獲取到一個Looper對象
            Handler h = new Handler();
            h.sendEmptyMessage(112);

        }
     }).start();

以上代碼運行後會報錯:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

小結Handler的初始化會獲取到當前線程的Looper對象,並通過Looper拿到對應的MQ對象,如果當前線程的執行流並沒有執行過Looper.prepare(),則無法創建Handler對象

Handler.sendMessage()

sendMessage消息有各種各樣的形式或重載,最終會調用到這個方法:

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

它又調用了enqueueMessage方法:

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

注意到它對Messagetarget屬性進行了賦值,這樣這條消息就知道自己是被誰發送的了。然後將消息加入到隊列中。

Handler.dispatchMessage()

Message對象進入了MQ後,很快的會被MQ的next()方法獲取到,這樣Looper的死循環中就能得到一個Message對象,回顧一下,接下來,就調用了Message.target.dispatchMessage()方法對這條消息進行了處理。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}

public void handleMessage(Message msg) {
    //這個方法是空實現,讓客戶端程序員去覆寫實現自己的邏輯
}

dispatchMessage方法有兩個分支,如果callbackRunnable)不是null,則直接執行callback.run()方法,如果callbacknull,則將msg作爲參數傳給handleMessage()方法去處理,這樣就是我們常見的處理方法了。

Message.target與Handler

特別需要注意Message中的target成員變量,它是指向自己的發送者,這一點意味着什麼呢?

意味着:一個有Looper的線程可以有很多個Handler,這些Handler都是不同的對象,但是它們都可以將Message對象發送到同一個MQ中,Looper不斷的從MQ中獲取這些消息,並將消息交給它們的發送者去處理。一個MQ是可以對應多個Handler的(多個Handler都可以往同一個MQ中消息入隊)

下圖可以簡要的概括下它們之間的關係。

總結

  • Looper調用prepare()進行初始化,創建了一個與當前線程對應的Looper對象(通過ThreadLocal實現),並且初始化了一個與當前Looper對應的MessageQueue對象。

  • Looper調用靜態方法loop()開始消息循環,通過MessageQueue.next()方法獲取Message對象。

  • 當獲取到一個Message對象時,讓Message的發送者(target)去處理它。

  • Message對象包括數據,發送者(Handler),可執行代碼段(Runnable)三個部分組成。

  • Handler可以在一個已經Looper.prepare()的線程中初始化,如果線程沒有初始化Looper,創建Handler對象會失敗

  • 一個線程的執行流中可以構造多個Handler對象,它們都往同一個MQ中發消息,消息也只會分發給對應的Handler處理。

  • Handler將消息發送到MQ中,Messagetarget域會引用自己的發送者,Looper從MQ中取出來後,再交給發送這個MessageHandler去處理。

  • Message可以直接添加一個Runnable對象,當這條消息被處理的時候,直接執行Runnable.run()方法。

發佈了6 篇原創文章 · 獲贊 0 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章