1. 程式人生 > >【朝花夕拾】Handler篇(二)

【朝花夕拾】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開發藝術探