前言
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。效果很簡單,我猜很多小夥伴已經猜到了:
貼一下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內部原始碼進行剖析了。
希望可以對小夥伴們有所幫助,如果感覺有收穫,歡迎點贊,收藏,關注呦~