全面理解Handler-1:理解訊息佇列,手寫訊息佇列

MDove發表於2018-09-29

前言

Handler機制這個話題,算是爛大街的內容。但是為什麼偏偏重拿出來“炒一波冷飯”呢?因為自己發現這“冷飯”好像吃的不是很明白。最近在思考幾個問題,發現以之前對Handler機制的瞭解是在過於淺顯。什麼問題?

  • Handler機制存在的意義是什麼?能否用其他方式替換?
  • Looper.loop();是一個死迴圈,為什麼沒有阻塞主執行緒?用什麼樣的方式解決死迴圈的問題?

如果透徹的瞭解Handler,以及執行緒的知識。是肯定不會有這些疑問的,因為以上問題本身就存在問題。

就這倆個小問題,就發現自己在學習道路上的不紮實,所以這段時間重新理解了一下Handler。先預告一小下下,關於Handler的內容將是一個系列文章,今天這一篇內容重點在於Handler的理解,以及對訊息佇列的思考。

正文

1、Handler機制為了什麼?

我們都知道,在Android開發中,無法在子執行緒中更新UI。

我們先思考一個問題?為什麼不能在子執行緒更新UI。如果看過View繪製的原始碼,我們都知道不能在子執行緒更新UI的原因是:ViewRootImpl中有這麼一個方法:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
            "Only the original thread that created a view hierarchy can touch its views.");
    }
}
複製程式碼

很明顯這是人為限制的一個操作。那我們在思考,為什麼谷歌開發Android系統時要這麼限制?

其實不難推測出來。對於執行緒來說,我們都知道執行緒與執行緒之間是記憶體共享的。所以如果某一時刻多個子執行緒同時去更新UI,那麼對於繪製UI來說便成為了一個不安全的操作。為了保證UI繪製的正確性,此時勢必要增加鎖,以同步的方式去控制這個問題。

然而加鎖的方式顯然是一種犧牲效能的方式。

那麼還有沒有其他方案呢?很顯然,最終谷歌選擇了只能在主執行緒更新UI,應運而生的Handler機制被創造出來了。但是它也不是什麼新概念,說白了就是訊息佇列。實現原理也很簡單:只允許一個執行緒(主執行緒)去更新UI,子執行緒將訊息放到訊息佇列中,由主執行緒去輪詢訊息佇列,拿出訊息並執行。

這也就是我們的Handler機制。

2、訊息佇列

這種單執行緒 + 訊息佇列的模型其實應用很廣。比如在Web前端之中,對於JavaScript來說,被設計時就決定了單執行緒模型。假設如果 Javascript 被設計為多執行緒的程式,那麼操作 DOM 必然會涉及到資源的競爭。此時只能加鎖,那麼在 Client 端中跑這麼一門語言的程式,資源消耗和效能都將是不樂觀的。但是如果設計成單執行緒,並輔以完善的非同步佇列來實現,那麼執行成本就會比多執行緒的設計要小很多了。

所以我們可以看到,Handler機制的思路可以說是一個頗為常見的設計。

既然本質是訊息佇列,是不是我們自己也可以寫一套訊息佇列來感受一下Handler的設計思路呢?沒錯,接下來讓我們一起實現一套簡單的訊息佇列:

3、手寫訊息佇列

我們先來捋一捋思路:

Looper中建立了MessageQueue,Handler之中又通過ThreadLocal拿到主執行緒new出來的Looper,因此Handler就持有了MessageQueue,又因此執行緒間是記憶體共享的,所以子執行緒可以通過Handler去往MessageQueue之中傳送Message。

Looper.loop()死迴圈輪詢MessageQueue,拿到Message就回撥其對應的方法。

這樣整個Handler機制就運轉起來了。接下來我們就依靠這個思路,實現自己的訊息佇列,為了程式碼更簡潔,以及和Handler機制產生區別,我這裡省略一些操作比如ThreadLocal之類的。

3.1、程式碼實現

程式碼結束後有解釋

public class MainMQ {
    private MyMessageQueue mMQ;

    public static void main(String[] args) {
        new MainMQ().fun();
    }

    public void fun() {
        mMQ = new MyMessageQueue();
        System.out.println("當前執行緒id:" + Thread.currentThread().getId());
        new Thread(new Runnable() {
            @Override
            public void run() {
	            // 省略try-catch
                Thread.sleep(3000);
                mMQ.post(new MyMessage(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("執行此條訊息的執行緒id:" + Thread.currentThread().getId());
                    }
                }));
            }
        }).start();

        loop();
        System.out.println("死迴圈了,我永遠也不被執行~");
    }

    public void loop() {
        while (true) {
	        // 省略try-catch
            MyMessage next = mMQ.next();
            if (next == null) {
                continue;
            }
            Runnable runnable = next.getRunnable();
            if (runnable != null) {
                runnable.run();
            
        }
    }
}
複製程式碼

這裡沒有使用Looper這種思想,因為Looper本質就是使用ThreadLocal建立一個和主執行緒唯一關聯的Looper例項,並以此保證MessageQueue的唯一性。

知道這個原理之後,這個demo。直接在主執行緒中new MessageQueue(),是同樣的道理,然後呼叫loop()方法死迴圈輪詢MessageQueue中的Message,不為null則執行。

main()方法中start了一個子執行緒,然後sleep3秒後,往MessageQueue中post Message。效果很簡單,我猜很多小夥伴已經猜到了:

全面理解Handler-1:理解訊息佇列,手寫訊息佇列

貼一下MessageQueue和Message

public class MyMessageQueue {
    private final Queue<MyMessage> mQueue = new ArrayDeque<>();

    public void post(MyMessage message) {
        synchronized (this) {
            notify();
            mQueue.add(message);
        }
    }

    public MyMessage next() {
        while (true) {
            synchronized (this) {
	            // 省略try-catch
                if (!mQueue.isEmpty()) {
                    return mQueue.poll();
                }
                wait(); 
            }
        }
    }
}
複製程式碼
public class MyMessage {
    private Runnable mRunnable;

    public MyMessage(Runnable runnable) {
        mRunnable = runnable;
    }

    public Runnable getRunnable() {
        return mRunnable;
    }
}
複製程式碼

3.2、思考存在的問題

細心的小夥伴,可能有留意到loop()方法執行後有這麼一行程式碼,然後效果圖中並沒有被列印:

System.out.println("死迴圈了,我永遠也不被執行~");
複製程式碼

當然這是必然的,畢竟我們的loop()是一個死迴圈,後邊的程式碼是不可能被執行的。其實我們ActivityThread中呼叫了Looper.loop()之後,也沒有任何程式碼了。

這裡可能有小夥伴有疑問了。loop()死迴圈了,那麼我們在主執行緒中的生命週期回撥怎麼辦?豈不也不被執行了?其實不然,通過上述的訊息佇列,我們就能看出:我們在手寫的這個demo中,loop啟動前start了一個子執行緒,由子執行緒傳送Message交由loop去執行。保證了訊息的流暢性。

那是不是我們Android中的loop也是這種思路?沒錯,main中的loop啟動前,的確會起一個子執行緒......

不要著急,關於這個問題,讓我們下篇文章再展開~

結尾

今天這篇文章是全面理解Handler機制的第一篇,內容大多並沒有直切到Handler機制本身,而是從外部去思考Handler的設計。而接下來的內容則是對Handler內部原始碼進行剖析了。

希望可以對小夥伴們有所幫助,如果感覺有收穫,歡迎點贊,收藏,關注呦~

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,以及我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公眾號:IT面試填坑小分隊

相關文章