【朝花夕拾】Handler篇(二)
前言
轉載請宣告,轉自【https://www.cnblogs.com/andy-songwei/p/11438492.html】,謝謝。
Handler的身影總是時不時出現在工作,筆試,面試中,可見其對於Android的重要性。一年前寫過一篇文章【朝花夕拾】Handler篇,隨著這一年來對Handler更多的認識和理解,本文對Handler知識點做的一些補充。
一、為什麼要引入Handler
Handler的主要作用是切換執行緒,將執行緒切換到Handler所使用的Looper所線上程中去,我們大部分的開發者通常使用Handler是用於子執行緒通知主執行緒更新UI,我們需要明確的是更新UI只是Handler的其中一個作用而已。
那麼為什麼只能在主執行緒中更新UI,而不能在子執行緒中完成呢?因為Android系統規定,只能在主執行緒中訪問UI,如果在子執行緒中訪問UI,程式就會報錯。在訪問UI的時候,系統會呼叫ViewRootImpl類中的checkThread方法,如下所示:
1 //=======ViewRootImpl.java======= 2 final Thread mThread; 3 ...... 4 public ViewRootImpl(Context context, Display display) { 5 mThread = Thread.currentThread(); 6 } 7 ...... 8 void checkThread() { 9 if (mThread != Thread.currentThread()) { 10 throw new CalledFromWrongThreadException( 11 "Only the original thread that created a view hierarchy can touch its views."); 12 } 13 } 14 ......
我們知道ViewRootImpl是View體系根View DecorView與Activity的PhoneWindow之間的紐帶,最初ViewRootImpl例項化的時候,是在主執行緒中完成的。所以,上述checkThread方法可以用來判斷當前是否為主執行緒,不是則報異常,該異常應該比較常見的了。
那麼,系統為什麼不允許在子執行緒中訪問UI呢?這是因為UI控制元件不是執行緒安全的,如果在多執行緒中併發訪問可能會導致UI控制元件處於不可預期的狀態。那麼為什麼系統不對UI控制元件的訪問加上鎖機制呢?主要是因為如果加上鎖機制會有兩個缺點:1)使訪問邏輯變得複雜。2)降低訪問UI的效率,因為鎖機制會阻塞某些執行緒的執行。所以,最簡單且高效的方法就是採用單執行緒模型來處理UI操作,對於開發者來說也不是很麻煩,只需要通過Handler切換一下UI訪問的執行執行緒即可。
所以Handler的出現,就解決了子執行緒中不能訪問UI的問題。
二、Handler回撥所線上程問題
在對Handler理解不深入的時候,一直沒有認真注意過new一個Handler後,回撥方法所在的執行緒問題,總以為任何時候都可以在回撥方法中更新UI。事實上,之所以會有這樣的錯誤認識,是因為我們使用Handler的時候基本上都用於更新UI了,就犯了經驗主義錯誤。實際上,回撥方法所線上程,和傳送訊息的handler使用的Looper所在的執行緒一致。下面我們先通過一些實驗開看看結果。
1、在子執行緒中使用main Looper的情況
1 private void testHandler() { 2 Log.i("songzheweiwang", "thread1=" + Thread.currentThread()); 3 new Thread(new Runnable() { 4 @Override 5 public void run() { 6 new Handler(Looper.getMainLooper()).post(new Runnable() { 7 @Override 8 public void run() { 9 Log.i("songzheweiwang", "thread2=" + Thread.currentThread()); 10 } 11 }); 12 } 13 }).start(); 14 }
在主執行緒中呼叫如上方法,對應的log如下,可見回撥方法是在主執行緒中:
1 08-31 12:48:49.342 9414-9414/com.example.demos I/songzheweiwang: thread1=Thread[main,5,main] 2 08-31 12:48:49.373 9414-9414/com.example.demos I/songzheweiwang: thread2=Thread[main,5,main]
2、在子執行緒中使用子執行緒Looper的情況
1 private void testHandler() { 2 Log.i("songzheweiwang", "thread1=" + Thread.currentThread()); 3 new Thread(new Runnable() { 4 @Override 5 public void run() { 6 Looper.prepare(); 7 new Handler().post(new Runnable() { 8 @Override 9 public void run() { 10 Log.i("songzheweiwang", "thread2=" + Thread.currentThread()); 11 } 12 }); 13 Looper.loop(); 14 } 15 }).start(); 16 }
在主執行緒中呼叫該方法,得到的log如下,可見回撥方法是在當前子執行緒中:
1 08-31 12:53:49.718 9655-9655/com.example.demos I/songzheweiwang: thread1=Thread[main,5,main] 2 08-31 12:53:49.719 9655-9750/com.example.demos I/songzheweiwang: thread2=Thread[Thread-7,5,main]
上述示例採用的是post方式,sendMessage方式也是一樣的結果,這裡就不舉例了。我們平始使用Handler多數情況下是在主執行緒中new Handler的,預設情況下使用的是main Looper,然後在子執行緒中用該Handler例項來post或者sendMessage,所以預設情況下回調方法就是執行在主執行緒中,我們在該方法中訪問UI就沒有報錯。
三、訊息迴圈Looper
上一節示例中,第6行和13行紅色部分展示了在子執行緒中,Handler使用子執行緒Looper的使用範例。如果去掉這兩行程式碼,系統會報如下異常:
Looper.loop(),就是用來開啟執行緒的訊息迴圈,沒有該行程式碼就無法收到訊息,其作用在上一篇文章中說過,這裡不贅述了。Looper.prepare(),是獲取當前執行緒的Looper,如果沒有Looper會報上述異常,原始碼如下所示:
1 public Handler(Callback callback, boolean async) { 2 ...... 3 mLooper = Looper.myLooper(); 4 if (mLooper == null) { 5 throw new RuntimeException( 6 "Can't create handler inside thread that has not called Looper.prepare()"); 7 } 8 ...... 9 }
Looper.myLooper()方法用於獲取當前執行緒中的Looper,如果當前執行緒下沒有Looper,就會報異常。每一個執行緒下都有自己專屬的Looper,由TheadLocal來進行管理,至於ThreadLocal,有興趣的可以自行研究。
我們是否會有疑問,平時在new Handler的時候,也沒有去呼叫Looper.prepare()方法,為什麼沒有報錯呢?
這是因為咱們new Handler的時候經常就是在主執行緒中完成的,會預設使用主執行緒的Looper。我們平時經常提到主執行緒,其實就是AcitivityThread,是在Zygote建立應用程式程序時呼叫其main方法啟動的。主執行緒啟動的時候會建立自己的Looper,原始碼如下:
1 public static void main(String[] args) { 2 ...... 3 Looper.prepareMainLooper(); 4 5 ActivityThread thread = new ActivityThread(); 6 thread.attach(false); 7 8 if (sMainThreadHandler == null) { 9 sMainThreadHandler = thread.getHandler(); 10 } 11 ...... 12 Looper.loop(); 13 ..... 14 }
Looper.prepareMainLooper()裡面也是呼叫的Looper.prepare()方法,所以在當前應用程序建立的時候,系統就為該程序的主執行緒建立好了Looper,後續在主執行緒中例項化Handler的時候,就預設為使用該Looper了。
四、一些Tip
1、當有Handler傳送Message時,會通過MessageQueue類的enqueueMessage方法將Message加入到Handler內部的MessageQueue中,我們一般稱之為訊息佇列。但實際上它的資料結構為單鏈表,而不是佇列,因為佇列是先進先出,中間不能插入和刪除元素,但是單鏈表可以。Message會根據post/sendMessage時指定處理的時間來在插入到連結串列中,或者通過quit方法將訊息從連結串列中移除。
2、取Message的方法MessageQueue.next()方法是個死迴圈,沒有訊息時會阻塞。Looper.loop()也是個死迴圈,而且呼叫了MessageQueue.next()方法,當MessageQueue沒有訊息時也會阻塞,而當有訊息加入時就會立即處理。讓這兩個死迴圈終止的唯一條件就是Looper執行quit/quitSafety方法讓自己退出,這樣會將訊息佇列標記為退出狀態,否者會一直死迴圈下去。
參考資料
本文主要參考了任玉剛的《Android開發藝術探