全面分解 | Android之Handler執行緒機制
阿新 • • 發佈:2020-11-15
前言
很高興遇見你~ 歡迎閱讀我的文章。
關於Handler的部落格可謂是俯拾皆是,而這也是一個老生常談的話題,可見的他非常基礎,也非常重要。但很多的部落格,卻很少有從入門開始介紹,這在我一開始學習的時候就直接給我講Looper講阻塞,非常難以理解。同時,也很少有系統地講解關於Handler的一切,知識比較零散。我希望寫一篇從入門到深入,系統地全面地講解Handler的文章,幫助大家認識Handler。
這篇文章的講解深度循序漸進,不同程式的讀者可選擇對應的部分檢視:
第一部分是對於Handler的入門概述。瞭解一個新事物,需要問三個問題:是什麼、為什麼、怎麼用。包括關於Handler的結構等都有介紹。
第二部分是在對Handler有一定的認知基礎上,對各個類進行詳細的講解和原始碼分析。
第三部分是整體的流程分析以及常見問題的解析。
最後一部分是Android對於訊息機制設計的講解以及全文總結。
文章基本涵蓋了關於Handler相關的知識,因而篇幅也比較長
考慮過把文章分割成幾篇小文章,考慮到閱讀的整體性以及方便性,最終還是集成了一篇大文章
文章成體系,全面地講解知識點,而不是把知識碎片化,否則很難真正去理解單一的知識,更不易於對整體知識的把握
讀者可自行選擇感興趣的章節閱讀
那麼,我們開始吧。
概述
什麼是Handler?
準確來說,是Handler機制,Handler只是Handler機制中的一個角色。只是我們對Handler接觸比較多,所以經常以Handler來代稱。
Handler機制是Android中基於單線訊息佇列模式的一套執行緒訊息機制。
他的本質是訊息機制,負責訊息的分發以及處理。這樣講可能有點抽象,不太容易理解。什麼是“單線訊息佇列模式”?什麼是“訊息”?
通俗點來說,每個執行緒都有一個“流水線”,我們可往這條流水線上放“訊息”,流水線的末端有工作人員會去處理這些訊息。因為流水線是單線的,所有訊息都必須按照先來後到的形式依次處理(在Handler機制中有“加急線”:同步屏障,這個後面講)。如下圖:
放什麼訊息以及怎麼處理訊息,是需要我們去自定義的。Handler機制相當於提供了這樣的一套模式,我們只需要“放訊息到流水線上”,“編寫這些訊息的處理邏輯”就可以了,流水線會源源不斷把訊息運送到末端處理。最後注意重點:每個執行緒只有一個“流水線”,他的基本範圍是執行緒,負責執行緒內的通訊以及執行緒間的通訊。每個執行緒可以看成一個廠房,每個廠房只有一個生產線。
兩個關鍵問題
瞭解Handler的作用前需要了解Handler背景下的兩個關鍵問題:
不能在非UI建立執行緒去操作UI
不能在主執行緒執行耗時任務
我們普遍的認知是:不能在非主執行緒更新UI。但這是不準確的,如果我們在子執行緒更新了UI,看看報錯資訊是什麼:
筆者留下了英語渣渣的眼淚,百度翻譯一下:
只有建立檢視層次結構的原始執行緒才能訪問其檢視。但為什麼我們一直都說是非主執行緒不能更新ui?這是因為我們的介面一般都是由主執行緒進行繪製的,所以介面的更新也就一般都限制在主執行緒內。這個異常是在viewRootIimpl.checkThread()方法中丟擲來的,那可不可以繞過他?當然可以,在他還沒創建出來的時候就可以偷偷更新ui了。閱讀過Activity啟動流程的讀者知道,ViewRootImpl是在onCreate方法之後被建立的,所以我們可以在onCreate方法中建立個子執行緒偷偷更新UI。(Actvity啟動流程解析傳送門)但還是那句話,可以,但沒必要去繞過這個限制,因為這是谷歌為了我們的程式更加安全而設計的。
為什麼不能在子執行緒去更新UI?因為這會讓介面產生不可預期的結果。例如主執行緒在繪製一個按鈕,繪製一半另一個執行緒突然過來把按鈕的大小改成兩倍大,這個時候再回去主執行緒繼續執行繪製邏輯,這個繪製的效果就會出現問題。所以UI的訪問是決不能是併發的。但,子執行緒又想更新UI,怎麼辦?加鎖。加鎖確實可以解決這個問題,但是會帶來另外的問題:介面卡頓。鎖對於效能是有消耗的,是比較重量級的操作,而ui操作講究快準狠,加鎖會讓ui操作效能大打折扣。那有什麼更好的方法?Handler就是解決這個問題的。
第二個問題,不能在主執行緒執行耗時操作。耗時操作包括網路請求、資料庫操作等等,這些操作會導致ANR(Application Not Responding)。這個是比較好理解的,沒有什麼問題,但是這兩個問題結合起來,就有大問題了。資料請求一般是耗時操作,必須在子執行緒進行請求,而當請求完成之後又必須更新UI,UI又只能在主執行緒更新,這就導致必須切換執行緒執行程式碼,上面討論了加鎖是不可取的,那麼Handler的重要性就體現出來了。
不用Handler可不可以?可以,但沒必要。Handler是谷歌設計來方便開發者切換執行緒以及處理訊息,然後你說我偏不用,我自己用Java工具類,自己弄個出來不可以嗎?那。。。請收下小的膝蓋。
為什麼要有Handler?
先給結論:
切換程式碼執行的執行緒
按順序規則地處理訊息,避免併發
阻塞執行緒,避免讓執行緒結束
延遲處理訊息
第一個作用是最明顯也是最常用的,上一部分已經講了Handler存在的必要性,android限制了不能在非UI建立執行緒去操作UI,同時不能在主執行緒執行耗時任務,所以我們一般是在子執行緒執行網路請求等耗時操作請求資料,然後再切換到主執行緒來更新UI。這個時候就必須用到Handler來切換執行緒了。上面討論過了這裡不再贅述。
這裡有一個誤區是:我們的activity是執行在主執行緒的,我們在網路請求完成之後回撥主執行緒的方法不就切換到主執行緒了嗎?咳咳,不要笑,不要覺得這種低階錯誤太離譜,很多童鞋剛開始接觸開發的時候都會犯這個思維錯誤。這其實是理解錯了執行緒這個概念。程式碼本身並沒有限制執行在哪個執行緒,程式碼執行的執行緒環境取決於你的執行邏輯是在哪個執行緒。這樣講可能還是有點抽象。例如現在有一個方法void test(){},然後兩個不同的執行緒去呼叫它:
new Thread(){
// 第一個執行緒呼叫
test();
}.start();
new Thread(){
// 第二個執行緒呼叫
test();
}
此時雖然都是test這個方法,但是他的執行邏輯是由不同的執行緒呼叫的,所以他是執行在兩個不同的執行緒環境下。而當我們想要把邏輯切換到另一個執行緒去執行的時候,就需要用到Handler來切換邏輯。
第二個作用可能看著有點懵。但其實他解決了另一個問題:併發操作。雖然切換執行緒解決了,如果主執行緒正在繪製一個按鈕,剛測量好按鈕的長寬,突然子執行緒一個新的請求過來打斷了,先停下這邊的繪製操作,把按鈕改成了兩倍大,然後邏輯切回來繼續繪製,這個時候之前的測量的長寬已經是不準確的了,繪製的結果肯定也不準確。怎麼解決?單線訊息佇列模型。在講什麼是Handler那部分簡單介紹過,就是相當於一個流水線一樣的模型。子執行緒的請求會變成一個個的訊息,然後主執行緒依次處理,那麼就不會出現繪製一半被打斷的問題了。
同時這種模型也不止用於解決ui併發問題,在ActivityThread中有一個H類,他其實就是個Handler。在ActivityThread中定義了一百多中訊息型別以及對應的處理邏輯,這樣,當需要讓ActivityThread處理某一個邏輯的時候,只需要傳送對應的訊息給他即可,而且可以保證訊息按順序執行,例如先呼叫onCreate再呼叫onResume。而如果沒有Hanlder的話,就需要讓ActivityThread有一百多個介面對外開放,同時還需要不斷進行回撥保證任務按順序執行。這顯然複雜了非常多。
我們執行一個Java程式的時候,從main方法入口,執行完成之後,馬上就退出了,但是我們android應用程式肯定是不可以的,他需要一直等待使用者的操作。而Handler機制就解決了這個問題,但訊息佇列中沒有任務的時候,他就會把執行緒阻塞,等到有新的任務的時候,再重新啟動處理訊息。
第四個作用讓延遲處理訊息得到了最佳解決方案。假如你想讓應用啟動5秒後介面彈出一個對話方塊,沒有handler的情況下,會如何處理?開一個Thread然後使用Thread.sleep讓執行緒睡眠一對應的時間對吧,但如果多個延遲任務呢?而開啟執行緒也是個比較重量級的操作且執行緒的數量有限。而可以直接給Handler傳送延遲對應時間的訊息,他會在對應時間之後準時處理該訊息(當然有特殊情況,如單件訊息處理時間過長或者同步屏障,後面會講到)。而且無論傳送多少延遲訊息都不會對效能有任何影響。同時,也是通過這個功能來記錄ANR的時間。
講這些作用可能讀者心中並沒有一個很形象的概念,也可能看完就忘了。但是關於Handler的定義不能忘:Handler機制是Android中基於單線訊息佇列模式的一套執行緒訊息機制。,上述四個作用是為了讓讀者更好地理解Handler機制。
如何使用Handler
我們平常使用Handler有兩種不同的建立方式,但總體流程是相同的:
建立Looper
使用Looper建立Handler
啟動Looper
使用Handler傳送資訊
Looper可理解為迴圈器,就像“流水線”上的滾帶,後面會詳細講到。每個執行緒只有一個Looper,通常主執行緒已經建立好了,追溯應用程式啟動流程可以知道啟動過程中呼叫了Looper.prepareMainLooper,而在子執行緒就必須使用如下方法來初始化Looper:
Looper.prepare();
第二步是建立Handler,也是最熟悉的一步。我們有兩種方法來建立Handler:傳入callBack物件和繼承。如下:
public class MainActivity extends AppComposeActivity{
...;
// 第一種方法:使用callBack建立handler
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Handler handler = Handler(Looper.myLooper(),new CallBack(){
public Boolean handleMessage(Message msg) {
TODO("Not yet implemented")
}
});
}
// 第二種方法:繼承Handler並重寫handlerMessage方法
static MyHandler extends Hanlder{
public MyHandler(Looper looper){
super(looper);
}
@Override
public void handleMessage(Message msg){
super.handleMessage(msg);
// TODO(重寫這個方法)
}
}
}
注意第二種方法,要使用靜態內部類,不然可能會造成記憶體洩露。原因是非靜態內部類會持有外部類的引用,而Handler發出的Message會持有Handler的引用。如果這個Message是個延遲的訊息,此時activity被退出了,但Message依然在“流水線”上,Message->handler->activity,那麼activity就無法被回收,導致記憶體洩露。
兩種Handler的寫法各有千秋,繼承法可以寫比較複雜的邏輯,callback法適合比價簡單的邏輯,看具體的業務來選擇。
然後再呼叫Looper的loope方法來啟動Looper:
Looper.loop();
最後就是使用Handler來發送資訊了。當我們獲得handler的例項之後,就可以通過他的sendMessage相方法和post相關方法來發送資訊,如下:
handler.sendMessage(msg);
handler.sendMessageDelayed(msg,delayTime);
handler.post(runnable);
handler.postDelayed(runnable,delayTime);
然後一般情況下是哪個Handler發出的資訊,最終由哪個Handler來處理。這樣,只要我們拿到Handler物件,就可以往對應的執行緒傳送資訊了。
Handler內部模式結構
經過前面的介紹對於Looper已經有了一定的認知,但可能對他內部的模式還不太清楚。這一部分先講解Handler的大概內部模式,目的是為下面的詳解做鋪墊,為做整體概念感知。先上圖:
Handler機制內部有三大關鍵角色:Handler,Looper,MessageQueue。其中MessageQueue是Looper內部的一個物件,MessageQueue和Looper每個執行緒有且只有一個,而Handler是可以有很多個的。他們的工作流程是:
使用者使用執行緒的Looper構建Handler之後,通過Handler的send和post方法傳送訊息
訊息會加入到MessageQueue中,等待Looper獲取處理
Looper會不斷地從MessageQueue中獲取Message然後交付給對應的Handler處理
這就是大名鼎鼎的Handler機制內部模式了,說難,其實也是很簡單。
Handler機制關鍵類
一、ThreadLocal
概述
ThreadLocal是Java中一個用於執行緒內部儲存資料的工具類。
ThreadLocal是用來儲存資料的,但是每個執行緒只能訪問到各自執行緒的資料。我們一般的用法是:
Thr