原始碼深度解析 Handler 機制及應用

vivo網際網路技術發表於2020-12-02

本文以原始碼分析+實際應用的形式,詳細講解了 Handler 機制的原理,以及在開發中的使用場景和要注意的地方。

一、基本原理回顧

在 Android 開發中,Handler及相關衍生類的應用經常用到,Android的執行也是建立在這套機制上的,所以瞭解其中的原理細節,以及其中的坑對於每位開發者來說都是非常有必要的。Handler機制的五個組成部分:Handler、Thread(ThreadLocal)、Looper、MessageQueue、Message。

原始碼深度解析 Handler 機制及應用

 

1、Thread(ThreadLocal)

Handler機制用到的跟Thread相關的,而根本原因是Handler必須和對應的Looper繫結,而Looper的建立和儲存是跟Thread一一對應的,也就是說每個執行緒都可以建立唯一一個且互不相關的Looper,這是通過ThreadLocal來實現的,也就是說是用ThreadLocal物件來儲存Looper物件的,從而達到執行緒隔離的目的。

1
2
3
4
5
6
7
8
static  final  ThreadLocal<Looper> sThreadLocal =  new  ThreadLocal<Looper>();
  
private  static  void  prepare( boolean  quitAllowed) {
     if  (sThreadLocal.get() !=  null ) {
         throw  new  RuntimeException( "Only one Looper may be created per thread" );
     }
     sThreadLocal.set( new  Looper(quitAllowed));
}

2、Handler

1
2
3
4
5
6
7
Handler()
Handler(Callback callback)
Handler(Looper looper)
Handler(Looper looper, Callback callback)
Handler( boolean  async)
Handler(Callback callback,  boolean  async)
Handler(Looper looper, Callback callback,  boolean  async)

2.1 建立Handler大體上有兩種方式:

一種是不傳Looper

這種就需要在建立Handler前,預先呼叫Looper.prepare來建立當前執行緒的預設Looper,否則會報錯。

一種是傳入指定的Looper

這種就是Handler和指定的Looper進行繫結,也就是說Handler其實是可以跟任意執行緒進行繫結的,不侷限於在建立Handler所在的執行緒裡。

2.2 async引數

這裡Handler有個async引數,通過這個參數列明通過這個Handler傳送的訊息全都是非同步訊息,因為在把訊息壓入佇列的時候,會把這個標誌設定到message裡.這個標誌是全域性的,也就是說通過構造Handler函式傳入的async引數,就確定了通過這個Handler傳送的訊息都是非同步訊息,預設是false,即都是同步訊息。至於這個非同步訊息有什麼特殊的用途,我們在後面講了屏障訊息後,再聯絡起來講。

1
2
3
4
5
6
7
private  boolean  enqueueMessage(MessageQueue queue, Message msg,  long  uptimeMillis) {
     msg.target =  this ;
     if  (mAsynchronous) {
         msg.setAsynchronous( true );
     }
     return  queue.enqueueMessage(msg, uptimeMillis);
}

2.3 callback引數

這個回撥引數是訊息被分發之後的一種回撥,最終是在msg呼叫Handler的dispatchMessage時,根據實際情況進行回撥:

1
2
3
4
5
6
7
8
9
10
11
12
public  void  dispatchMessage(Message msg) {
     if  (msg.callback !=  null ) {
         handleCallback(msg);
     else  {
         if  (mCallback !=  null ) {
             if  (mCallback.handleMessage(msg)) {
                 return ;
             }
         }
         handleMessage(msg);
     }
}

3、 Looper

用於為執行緒執行訊息迴圈的類。預設執行緒沒有與它們相關聯的Looper;所以要在執行迴圈的執行緒中呼叫prepare(),然後呼叫loop()讓它迴圈處理訊息,直到迴圈停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private  static  void  prepare( boolean  quitAllowed) {
         if  (sThreadLocal.get() !=  null ) {
             throw  new  RuntimeException( "Only one Looper may be created per thread" );
         }
         sThreadLocal.set( new  Looper(quitAllowed));
     }
      
     public  static  void  loop() {
         ...
      
         for  (;;) {
         ...
         }
          
         ...
     }
  
class  LooperThread  extends  Thread {
     public  Handler mHandler;
  
     public  void  run() {
          
         Looper.prepare(); 
          
         mHandler =  new  Handler() {
             public  void  handleMessage(Message msg) {
                  
                 Message msg=Message.obtain();
             }
         };
         
         Looper.loop();
     }
}

既然在使用Looper前,必須呼叫prepare建立Looper,為什麼我們平常在主執行緒裡沒有看到呼叫prepare呢?這是因為Android主執行緒建立的時候,在ActivityThread的入口main方法裡就已經預設建立了Looper。

1
2
3
4
5
6
7
public  static  void  main(String[] args) {
     ...
     Looper.prepareMainLooper();
     ...
     Looper.loop();
     ...
}

我們再來回顧一下Looper相關類的之間的聯絡:

原始碼深度解析 Handler 機制及應用 原始碼深度解析 Handler 機制及應用

 

4、MessageQueue 和 Message

MessageQueue是一個訊息佇列,Handler將Message傳送到訊息佇列中,訊息佇列會按照一定的規則取出要執行的Message。Message並不是直接加到MessageQueue的,而是通過Handler物件和Looper關聯到一起。

MessageQueue裡的message是按時間排序的,越早加入佇列的訊息放在佇列頭部,優先執行,這個時間就是sendMessage的時候傳過來的,預設是用的當前系統從啟動到現在的非休眠的時間SystemClock.uptimeMillis()。

sendMessageAtFrontOfQueue 這個方法傳入的時間是0,也就是說呼叫這個方法的message肯定會放到對訊息佇列頭部,但是這個方法不要輕易用,容易引發問題。

存到MessageQueue裡的訊息可能有三種:同步訊息,非同步訊息,屏障訊息。

原始碼深度解析 Handler 機制及應用 原始碼深度解析 Handler 機制及應用

4.1 同步訊息

我們預設用的都是同步訊息,即前面講Handler裡的建構函式引數的async引數預設是false,同步訊息在MessageQueue裡的存和取完全就是按照時間排的,也就是通過msg.when來排的。

4.2 非同步訊息

非同步訊息就是在建立Handler如果傳入的async是true或者傳送來的Message通過msg.setAsynchronous(true);後的訊息就是非同步訊息,非同步訊息的功能要配合下面要講的屏障訊息才有效,否則和同步訊息是一樣的處理。

1
2
3
4
5
6
7
8
private  boolean  enqueueMessage(MessageQueue queue, Message msg,  long  uptimeMillis) {
     msg.target =  this ;
     // 這個mAsynchronous就是在建立Handler的時候傳入async引數
     if  (mAsynchronous) {
         msg.setAsynchronous( true );
     }
     return  queue.enqueueMessage(msg, uptimeMillis);
}

4.3 Barrier(屏障)訊息

屏障(Barrier) 是一種特殊的Message,它最大的特徵就是target為null(只有屏障的target可以為null,如果我們自己設定Message的target為null的話會報異常),並且arg1屬性被用作屏障的識別符號來區別不同的屏障。屏障的作用是用於攔截佇列中同步訊息,放行非同步訊息。

那麼屏障訊息是怎麼被新增和刪除的呢?我們可以看到在MessageQueue裡有新增和刪除屏障訊息的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
private  int  postSyncBarrier( long  when) {
     // Enqueue a new sync barrier token.
     // We don't need to wake the queue because the purpose of a barrier is to stall it.
     synchronized  ( this ) {
         final  int  token = mNextBarrierToken++;
         final  Message msg = Message.obtain();
         msg.markInUse();
         msg.when = when;
         msg.arg1 = token;
  
         Message prev =  null ;
         Message p = mMessages;
         if  (when !=  0 ) {
             // 這裡是說如果p指向的訊息時間戳比屏障訊息小,說明這個訊息比屏障訊息先進入佇列,
             // 那麼這個訊息不應該受到屏障訊息的影響(屏障訊息隻影響比它後加入訊息佇列的訊息),找到第一個比屏障訊息晚進入的訊息指標
             while  (p !=  null  && p.when <= when) {
                 prev = p;
                 p = p.next;
             }
         }
         // 上面找到第一個比屏障訊息晚進入的訊息指標之後,把屏障訊息插入到訊息佇列中,也就是屏障訊息指向第一個比它晚進入的訊息p,
         // 上一個比它早進入訊息佇列的prev指向屏障訊息,這樣就完成了插入。
         if  (prev !=  null ) {  // invariant: p == prev.next
             msg.next = p;
             prev.next = msg;
         else  {
         // 如果prev是null,說明上面沒有經過移動,也就是屏障訊息就是在訊息佇列的頭部了。
             msg.next = p;
             mMessages = msg;
         }
         return  token;
     }
}
  
public  void  removeSyncBarrier( int  token) {
     // Remove a sync barrier token from the queue.
     // If the queue is no longer stalled by a barrier then wake it.
     synchronized  ( this ) {
         Message prev =  null ;
         Message p = mMessages;
         // 前面在插入屏障訊息後會生成一個token,這個token就是用來刪除該屏障訊息用的。
         // 所以這裡通過判斷target和token來找到該屏障訊息,從而進行刪除操作
         // 找到屏障訊息的指標p
         while  (p !=  null  && (p.target !=  null  || p.arg1 != token)) {
             prev = p;
             p = p.next;
         }
         if  (p ==  null ) {
             throw  new  IllegalStateException( "The specified message queue synchronization "
                     " barrier token has not been posted or has already been removed." );
         }
         final  boolean  needWake;
         // 上面找到屏障訊息的指標p後,把前一個訊息指向屏障訊息的後一個訊息,這樣就把屏障訊息移除了
         if  (prev !=  null ) {
             prev.next = p.next;
             needWake =  false ;
         else  {
             mMessages = p.next;
             needWake = mMessages ==  null  || mMessages.target !=  null ;
         }
         p.recycleUnchecked();
  
         // If the loop is quitting then it is already awake.
         // We can assume mPtr != 0 when mQuitting is false.
         if  (needWake && !mQuitting) {
             nativeWake(mPtr);
         }
     }
}

原始碼深度解析 Handler 機制及應用


4.4 屏障訊息的作用

說完了屏障訊息的插入和刪除,那麼屏障訊息在哪裡起作用的?它跟前面提到的非同步訊息又有什麼關聯呢?我們可以看到MessageQueue的next方法裡有這麼一段:

1
2
3
4
5
6
7
8
9
10
11
// 這裡就是判斷當前訊息是否是屏障訊息,判斷依據就是msg.target==null, 如果存在屏障訊息,那麼在它之後進來的訊息中,
// 只把非同步訊息放行繼續執行,同步訊息阻塞,直到屏障訊息被remove掉。
if  (msg !=  null  && msg.target ==  null ) {
     // Stalled by a barrier.  Find the next asynchronous message in the queue.
     do  {
         prevMsg = msg;
         msg = msg.next;
         // 這裡的isAsynchronous方法就是前面設定進msg的async引數,通過它判斷如果是非同步訊息,則跳出迴圈,把該非同步訊息返回
         // 否則是同步訊息,把同步訊息阻塞。
     while  (msg !=  null  && !msg.isAsynchronous());
}

4.5 屏障訊息的實際應用

屏障訊息的作用是把在它之後入隊的同步訊息阻塞,但是非同步訊息還是正常按順序取出執行,那麼它的實際用途是什麼呢?我們看到ViewRootImpl.scheduleTraversals()用到了屏障訊息和非同步訊息。

TraversalRunnable的run(),在這個run()中會執行doTraversal(),最終會觸發View的繪製流程:measure(),layout(),draw()。為了讓繪製流程儘快被執行,用到了同步屏障技術。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void  scheduleTraversals() {
     if  (!mTraversalScheduled) {
         mTraversalScheduled =  true ;
         // 這裡先將主執行緒的MessageQueue設定了個訊息屏障
         mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
         // 這裡傳送了個非同步訊息mTraversalRunnable,這個mTraversalRunnable最終會執行doTraversal(),也就是會觸發View的繪製流程
         // 也就是說通過設定屏障訊息,會把主執行緒的同步訊息先阻塞,優先執行View繪製這個非同步訊息進行介面繪製。
         // 這很好理解,介面繪製的任務肯定要優先,否則就會出現介面卡頓。
         mChoreographer.postCallback(
                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable,  null );
         if  (!mUnbufferedInputDispatch) {
             scheduleConsumeBatchedInput();
         }
         notifyRendererOfFramePending();
         pokeDrawLockIfNeeded();
     }
}
  
private  void  postCallbackDelayedInternal( int  callbackType,
         Object action, Object token,  long  delayMillis) {
     if  (DEBUG_FRAMES) {
         Log.d(TAG,  "PostCallback: type="  + callbackType
                 ", action="  + action +  ", token="  + token
                 ", delayMillis="  + delayMillis);
     }
  
     synchronized  (mLock) {
         final  long  now = SystemClock.uptimeMillis();
         final  long  dueTime = now + delayMillis;
         mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
  
         if  (dueTime <= now) {
             scheduleFrameLocked(now);
         else  {
             Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
             msg.arg1 = callbackType;
             // 設定該訊息是非同步訊息
             msg.setAsynchronous( true );
             mHandler.sendMessageAtTime(msg, dueTime);
         }
     }
}

4.6  我們能用屏障訊息做什麼?

那麼除了系統中使用到了屏障訊息,我們在開發中有什麼場景能派上用場嗎? 運用屏障訊息可以阻塞同步訊息的特性,我們可以用來實現UI介面初始化和資料載入同時進行。

我們一般在Activity建立的時候,為了減少空指標異常的發生,都會在onCreate先setContent,然後findView初始化控制元件,然後再執行網路資料載入的非同步請求,待網路資料載入完成後,再重新整理各個控制元件的介面。

試想一下,怎麼利用屏障訊息的特性來達到介面初始化和非同步網路資料的載入同時進行,而不影響介面渲染?先來看一個時序圖:

原始碼深度解析 Handler 機制及應用 原始碼深度解析 Handler 機制及應用

我們通過下面虛擬碼進一步加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 在上一個頁面裡非同步載入下一個頁面的資料
  // 網路請求返回的資料
     Data netWorkData;
     // 建立屏障訊息會生成一個token,這個token用來刪除屏障訊息,很重要。
     int  barrierToken;
  
     // 建立非同步執行緒載入網路資料
     HandlerThread thread =  new  HandlerThread( "preLoad" ){
         @Override
         protected  void  onLooperPrepared() {
             Handler mThreadHandler =  new  Handler(thread.getLooper());
             // 1、把請求網路耗時訊息推入訊息佇列
             mHandler.post( new  Runnable() {
                 @Override
                 public  void  run() {
                     // 非同步耗時操作:網路請求資料,賦值給netWorkData
                     netWorkData = xxx;
  
                 }
             });
  
             // 2、然後給非同步執行緒的佇列發一個屏障訊息推入訊息佇列
             barrierToken = thread.getLooper().getQueue().postSyncBarrier();
  
             // 3、然後給非同步執行緒的訊息佇列發一個重新整理UI介面的同步訊息
             // 這個訊息在屏障訊息被remove前得不到執行的。
             mHandler.post( new  Runnable() {
                 @Override
                 public  void  run() {
                     // 回撥主執行緒, 把netWorkData賦給監聽方法,重新整理介面
  
                 }
             });
         }
     };
     thread.start();
     
// 當前介面初始化介面
protected  void  onCreate(Bundle savedInstanceState) {
     setContentView(view);
  
     // 各種findview操作完成
     Button btn = findViewById(R.id.xxx);
     ...
     // 4、待控制元件初始化完成,把非同步執行緒設定的屏障訊息remove掉,這樣非同步執行緒請求資料完成後,3、處的重新整理UI介面的同步訊息就有機會執行,就可以安全得重新整理介面了。
     thread.getLooper().getQueue().removeSyncBarrier(barrierToken);
}

但是,MessageQueue原始碼裡我們我們看到,屏障訊息的建立和刪除都是隱藏方法(@hide),我們沒法直接呼叫,只能用反射來呼叫,所以在實際使用中還得綜合驗證。

4.7 IdleHandler及應用

IdleHandler,字面意思就是空閒的處理器(就是說我是在訊息佇列空閒的時候才會執行的,如果訊息佇列裡有其他非IdleHandler訊息在執行,則我先不執行),它其實就是一個介面,我們就認為它是空閒訊息吧,只不過它不是存在MessageQueue裡,而是以陣列的形式儲存的。

1
2
3
4
5
6
7
8
9
10
public  static  interface  IdleHandler {
     /**
      * Called when the message queue has run out of messages and will now
      * wait for more.  Return true to keep your idle handler active, false
      * to have it removed.  This may be called if there are still messages
      * pending in the queue, but they are all scheduled to be dispatched
      * after the current time.
      */
     boolean  queueIdle();
}

我們看到MessageQueue有新增和刪除IdleHandler的方法,IdleHandler被儲存在一個ArrayList裡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private  final  ArrayList<IdleHandler> mIdleHandlers =  new  ArrayList<IdleHandler>();
  
...
  
public  void  addIdleHandler( @NonNull  IdleHandler handler) {
     if  (handler ==  null ) {
         throw  new  NullPointerException( "Can't add a null IdleHandler" );
     }
     synchronized  ( this ) {
         mIdleHandlers.add(handler);
     }
}
  
public  void  removeIdleHandler( @NonNull  IdleHandler handler) {
     synchronized  ( this ) {
         mIdleHandlers.remove(handler);
     }
}

那麼,它是怎麼實現在訊息佇列空閒的間隙得到執行的呢?還是看next()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
// pendingIdleHandlerCount < 0是說for迴圈只執行第一次
// mMessages == null || now < mMessages.when) 是說當前訊息佇列沒有訊息或者要執行的訊息晚於當前時間
// 說明現在訊息佇列處於空閒。
if  (pendingIdleHandlerCount <  0
         && (mMessages ==  null  || now < mMessages.when)) {
     pendingIdleHandlerCount = mIdleHandlers.size();
}
if  (pendingIdleHandlerCount <=  0 ) {
     // No idle handlers to run.  Loop and wait some more.
     mBlocked =  true ;
     continue ;
}

在上面這段程式碼判定當前訊息佇列處於空閒後,就會拿到空閒訊息的大小,下面這段程式碼就是把把空閒訊息執行一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for  ( int  i =  0 ; i < pendingIdleHandlerCount; i++) {
     final  IdleHandler idler = mPendingIdleHandlers[i];
     mPendingIdleHandlers[i] =  null // release the reference to the handler
  
     boolean  keep =  false ;
     try  {
         // 如果queueIdle返回true,則該空閒訊息不會被自動刪除,在下次執行next的時候,如果還出現佇列空閒,會再次執行。
         // 如果返回false,則該空閒訊息會在執行完後,被自動刪除掉。
         keep = idler.queueIdle();
     catch  (Throwable t) {
         Log.wtf(TAG,  "IdleHandler threw exception" , t);
     }
  
     if  (!keep) {
         synchronized  ( this ) {
             mIdleHandlers.remove(idler);
         }
     }
}
  
// Reset the idle handler count to 0 so we do not run them again.
// 這裡把空閒訊息標誌置為0,而不置為-1,就是說本次已經處理完,防止for迴圈反覆執行,影響其他訊息的執行
pendingIdleHandlerCount =  0 ;
  
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis =  0 ;

總結一下:

  • 如果本次迴圈拿到的訊息為空,或者這個訊息是一個延時的訊息而且還沒到指定的觸發時間,那麼,就認定當前的佇列為空閒狀態。
  • 接著就會遍歷mPendingIdleHandlers陣列(這個陣列裡面的元素每次都會到mIdleHandlers中去拿)來呼叫每一個IdleHandler例項的queueIdle方法, 如果這個方法返回false的話,那麼這個例項就會從mIdleHandlers中移除,也就是當下次佇列空閒的時候,不會繼續回撥它的queueIdle方法了。
  • 處理完IdleHandler後會將nextPollTimeoutMillis設定為0,也就是不阻塞訊息佇列,當然要注意這裡執行的程式碼同樣不能太耗時,因為它是同步執行的,如果太耗時肯定會影響後面的message執行。

IdleHandler的原理大概就是上面講的那樣,那麼能力決定用處,從本質上講就是趁著訊息佇列空閒的時候乾點事情,具體做什麼,是在IdleHandler的queueIdle()方法裡。那麼IdleHandler在系統原始碼裡使用場景是怎樣的?我們可以看到它在主執行緒生命週期處理中使用比較多,比如在ActivityThread裡有個 就有一個名叫GcIdler的內部類,實現的就是IdleHandler介面,它的作用就是在主執行緒空閒的時候對記憶體進行強制GC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final  class  GcIdler  implements  MessageQueue.IdleHandler {
     @Override
     public  final  boolean  queueIdle() {
         doGcIfNeeded();
         return  false ;
     }
}
        
// 這裡的意思就是說判斷距離上次GC的時間是否超過5秒,超過則執行後臺強制GC
void  doGcIfNeeded() {
     mGcIdlerScheduled =  false ;
     final  long  now = SystemClock.uptimeMillis();
     //Slog.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime()
     //        + "m now=" + now);
     if  ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
         //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!");
         BinderInternal.forceGc( "bg" );
     }
}

我們看看它是在哪裡新增到訊息佇列的:

1
2
3
4
5
6
7
8
// 這個方法是在mH的handleMessage方法裡調的,也就是說也是通過AMS(ActivityManagerService)把訊息傳送到主執行緒訊息佇列
void  scheduleGcIdler() {
     if  (!mGcIdlerScheduled) {
         mGcIdlerScheduled =  true ;
         Looper.myQueue().addIdleHandler(mGcIdler);
     }
     mH.removeMessages(H.GC_WHEN_IDLE);
}

還有就是在ActivityThread的performLaunchActivity方法執行時,最終會執行到Instrumentation.callActivityOnCreate方法,在這個方法裡,也有用到IdleHandler做一些額外的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  void  callActivityOnCreate(Activity activity, Bundle icicle) {
     prePerformCreate(activity);
     activity.performCreate(icicle);
     postPerformCreate(activity);
}
  
private  void  prePerformCreate(Activity activity) {
     if  (mWaitingActivities !=  null ) {
         synchronized  (mSync) {
             final  int  N = mWaitingActivities.size();
             for  ( int  i= 0 ; i<N; i++) {
                 final  ActivityWaiter aw = mWaitingActivities.get(i);
                 final  Intent intent = aw.intent;
                 if  (intent.filterEquals(activity.getIntent())) {
                     aw.activity = activity;
                     mMessageQueue.addIdleHandler( new  ActivityGoing(aw));
                 }
             }
         }
     }
}

除此之外,在一些第三方庫中都有使用IdleHandler,比如LeakCanary,Glide中有使用到。

那麼對於我們來說,IdleHandler可以有些什麼使用場景呢?根據它最核心的原理,在訊息佇列空閒的時候做點事情,那麼對於主執行緒來講,我們有很多的一些程式碼不是必須要跟隨生命週期方法同步執行的,就可以用IdleHandler,減少主執行緒的耗時,也就減少應用或者Activity的啟動時間。例如:一些第三方庫的初始化,埋點尤其是延時埋點上報等,都可以用IdleHandler新增到訊息佇列裡。

==好了,提個問題:前面我們說了在主執行緒建立的main函式裡建立了Handler和Looper,回顧了上面的Handler機制的原理,我們都知道一般執行緒執行完就會退出,由系統回收資源,那Android UI執行緒也是基於Handler Looper機制的,那麼為什麼UI執行緒可以一直常駐?不會被阻塞呢?==

因為Looper在執行loop方法裡,是一個for迴圈,也就是說執行緒永遠不會執行完退出,所以開啟APP可以一直顯示,Activity的生命週期就是通過訊息佇列把訊息一個一個取出來執行的,然後因為MessageQueue的休眠喚醒機制,當訊息佇列裡沒有訊息時,訊息佇列會進入休眠,並釋放CPU資源,當又有新訊息進入佇列時,會喚醒佇列,把訊息取出來執行。

二、Handler應用之HandlerThread

HandlerThread本質上是一個Thread,所不同的是,它充分利用了Handler機制,通過在內部建立Looper迴圈,外部通過Handler把非同步任務推送給訊息佇列,從而達到不用重複建立多個Thread,即能將多個非同步任務排隊進行非同步執行,它的原理很簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public  void  run() {
     mTid = Process.myTid();
     Looper.prepare();
     synchronized  ( this ) {
         mLooper = Looper.myLooper();
         notifyAll();
     }
     Process.setThreadPriority(mPriority);
     onLooperPrepared();
     Looper.loop();
     mTid = - 1 ;
}

線上程的run方法裡建立了looper迴圈,這樣這個執行緒不主動quit的話,不會銷燬,有訊息則執行訊息,沒有訊息根據MessageQueue休眠機制,會釋放CPU資源,進入休眠。

使用HandlerThread時,我們注意到,在建立Handler時,是要傳入執行緒的Looper進行繫結的,所以必須先執行HandlerThread的start方法,因為執行start方法,才會執行HandlerThread的run方法,才會建立執行緒的Looper,建立Handler傳入的Looper才不會是null。

所以我們一般使用是這樣的:

  • 建立HandlerThread後,呼叫start,然後再建立Handler;
  • 從run方法裡我們看到有個onLooperPrepared()方法,可以實現這個方法,在這個方法裡建立Handler,這樣就不受start位置的限制了,原理就是以為run方法是在呼叫start方法後才會執行。

那麼怎麼回收一個HandlerThread呢?我們看到HandlerThread裡有個quit方法,這個方法最終會呼叫到MessageQueue的quit方法,從而結束訊息分發,最終終止一個HandlerThread執行緒。

1
2
3
4
5
6
7
8
public  boolean  quit() {
     Looper looper = getLooper();
     if  (looper !=  null ) {
         looper.quit();
         return  true ;
     }
     return  false ;
}

三、Handler應用之IntentService

IntentService其實是Service和HandlerThread的結合體,我們可以看到在onCreate裡建立了個HandlerThread並建立了個Handler和該HandlerThread繫結,然後在onStat方法裡以訊息的形式傳送給HandlerThread執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public  void  onCreate() {
     // TODO: It would be nice to have an option to hold a partial wakelock
     // during processing, and to have a static startService(Context, Intent)
     // method that would launch the service & hand off a wakelock.
  
     super .onCreate();
     // 建立HandlerThread
     HandlerThread thread =  new  HandlerThread( "IntentService["  + mName +  "]" );
     thread.start();
  
     // 建立Handler和HandlerThread繫結
     mServiceLooper = thread.getLooper();
     mServiceHandler =  new  ServiceHandler(mServiceLooper);
}
  
@Override
public  void  onStart( @Nullable  Intent intent,  int  startId) {
     Message msg = mServiceHandler.obtainMessage();
     msg.arg1 = startId;
     msg.obj = intent;
     // 想HandlerThread的訊息佇列傳送訊息
     mServiceHandler.sendMessage(msg);
}

最終在handleMessage裡執行

1
2
3
4
5
6
7
8
9
10
11
private  final  class  ServiceHandler  extends  Handler {
         public  ServiceHandler(Looper looper) {
             super (looper);
         }
  
         @Override
         public  void  handleMessage(Message msg) {
             onHandleIntent((Intent)msg.obj);
             stopSelf(msg.arg1);
         }
     }

所以我們使用IntentService都必須實現onHandleIntent這個抽象方法,在這個抽象方法裡做具體的業務操作。

我們都知道IntentService在執行完非同步任務後,會自動銷燬,這是怎麼實現的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public  ServiceHandler(Looper looper) {
         super (looper);
     }
  
     @Override
     public  void  handleMessage(Message msg) {
         onHandleIntent((Intent)msg.obj);
         // 答案在這裡:在這裡會停止Service
         stopSelf(msg.arg1);
     }
}
  
// 然後在onDestory裡會終止掉訊息迴圈,從而達到銷燬非同步執行緒的目的:
@Override
public  void  onDestroy() {
     mServiceLooper.quit();
}

四、Handler.post和View.post

我們先來看個大家平常經常使用的案例:獲取View的寬高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
protected  void  onCreate(Bundle savedInstanceState) {
  
     // 位置1
     Log.i( "view_w_&_h" "onCreate "  + mView.getWidth() +  " "  + mView.getHeight());
     mView.post( new  Runnable() {
         @Override
         public  void  run() {
         // 位置2
             Log.i( "view_w_&_h" "onCreate postRun "  + mView.getWidth() +  " "  + mView.getHeight());
         }
     });
  
     new  Handler(Looper.getMainLooper()).post( new  Runnable() {
         @Override
         public  void  run() {
         // 位置3
             Log.i( "view_w_&_h" "onCreate Handler "  + mView.getWidth() +  " "  + mView.getHeight());
         }
     });
}
  
@Override
protected  void  onResume() {
     super .onResume();
     // 位置4
     Log.i( "view_w_&_h" "onResume "  + mView.getWidth() +  " "  + mView.getHeight());
  
     new  Handler(Looper.getMainLooper()).post( new  Runnable() {
         @Override
         public  void  run() {
             // 位置5
             Log.i( "view_w_&_h" "onResume Handler "  + mView.getWidth() +  " "  + mView.getHeight());
         }
     });
}

這幾個位置,哪些能獲取到mView的寬高?

我們都知道在View被attach到window之前,是獲取不到View的寬高的,因為這個時候View還沒有被Measure、layout、draw,所以在onCreate或者onResume直接呼叫View的寬高方法,都是0,Handler.post在onCreate裡也是獲取不到,但是在onResume裡能獲取到,而View.post無論放在onCreate或者onResume裡,都能獲取到View的寬高,為什麼?

我們先看個簡版的View的繪製流程:

原始碼深度解析 Handler 機制及應用 原始碼深度解析 Handler 機制及應用

我們都知道View的最終繪製是在performTraversals()方法裡,包括measure、layout、draw,從上面的圖往上追溯,我們知道,View的繪製是在ActivityThread的handleResumeActivity方法裡,這個方法相信大家不會陌生,這個方法就是會回撥Activity的onResume方法的頂級方法。

1
2
3
4
5
6
7
8
9
10
11
12
final  void  handleResumeActivity(IBinder token,  boolean  clearHide,  boolean  isForward,  boolean  reallyResume,  int  seq, String reason) {
     ...
     // 這裡追溯進去,最終會呼叫Activity的onStart方法和onResume方法
     r = performResumeActivity(token, clearHide, reason);
      
     ...
      
     // 呼叫WindowManager的addView方法,這裡就是最終執行View繪製的地方
     wm.addView(decor, l);
      
     ...
}

從上面的程式碼片段執行順序來看,Activity的onStart和onResume被執行的時候,其實介面還沒有開始進行繪製(wm.addView(decor, l)還沒執行到),這裡就可以解釋為什麼用Handler.post在onCreate裡拿不到寬高。因為Handler機制,它是把訊息推送到主執行緒的訊息佇列裡去,在onCreate裡把訊息推到訊息佇列時,onResume的訊息都還沒入隊,也就沒有執行,所以拿不到。那為什麼onResume裡能拿到呢?因為訊息佇列的機制,Handler.post推送的訊息,必須得等上一個訊息執行完才能得到執行,所以它必須得等handleResumeActivity執行完,而handleResumeActivity執行完成後,View已經繪製完成了,當然就能拿到寬高了。

好了,現在解釋第二個疑問,為什麼View.post在onCreate裡能拿到View的寬高呢?我們先看下View.post方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public  boolean  post(Runnable action) {
     final  AttachInfo attachInfo = mAttachInfo;
     // attachInfo不為null,說明View已經被attach到window,也就是完成了繪製,所以直接把訊息推送到主執行緒的訊息佇列執行。
     if  (attachInfo !=  null ) {
         return  attachInfo.mHandler.post(action);
     }
  
     // Postpone the runnable until we know on which thread it needs to run.
     // Assume that the runnable will be successfully placed after attach.
     // 關鍵就在這裡,走到這裡,說明attachInfo為null,也就是現在View還沒attach到window,所以把訊息臨時儲存到RunQueue裡
     getRunQueue().post(action);
     return  true ;
}

上面我們可以看到,如果attachInfo為null,則Runnable會臨時儲存起來,儲存到RunQueue裡,並沒有立即執行,那麼儲存到RunQueue是什麼時候被執行的呢?

我們看到HandlerActionQueue有個executeActions方法,這個方法就是用來執行儲存其中的Runnable的:

1
2
3
4
5
6
7
8
9
10
11
12
public  void  executeActions(Handler handler) {
     synchronized  ( this ) {
         final  HandlerAction[] actions = mActions;
         for  ( int  i =  0 , count = mCount; i < count; i++) {
             final  HandlerAction handlerAction = actions[i];
             handler.postDelayed(handlerAction.action, handlerAction.delay);
         }
  
         mActions =  null ;
         mCount =  0 ;
     }
}

那麼這個方法是在什麼時機呼叫的呢?接著往下看:在View的dispatchAttachedToWindow方法裡,我們看到呼叫了RunQueue的executeActions,執行儲存在RunQueue裡的runnable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void  dispatchAttachedToWindow(AttachInfo info,  int  visibility) {
  
     ...
      
     // Transfer all pending runnables.
     if  (mRunQueue !=  null ) {
         mRunQueue.executeActions(info.mHandler);
         mRunQueue =  null ;
     }
      
     onAttachedToWindow();
      
     ...
}

那麼dispatchAttachedToWindow又是在什麼時候被呼叫呢?在ViewRootImpl的performTraversals方法裡,我們看到dispatchAttachedToWindow被執行。host就是DecorView。

1
2
3
4
5
6
7
8
9
10
private  void  performTraversals() {
     ...
     host.dispatchAttachedToWindow(mAttachInfo,  0 );
     ...
     performMeasure();
     ...
     performLayout();
     ...
     performDraw();
}

從前面的View繪製的UML時序圖,我們知道,performTraversals是在ActivityThread的handleResumeActivity被呼叫的。

總結一下:

系統在執行ActivityThread的handleResumeActivity的方法裡,最終會調到ViewRootImpl的performTraversals()方法,performTraversals()方法呼叫host的dispatchAttachedToWindow()方法,host就是DecorView也就是View,接著在View的dispatchAttachedToWindow()方法中呼叫mRunQueue.executeActions()方法,這個方法內部會遍歷HandlerAction陣列,利用Handler來post之前存放的Runnable。

這裡就可以解釋為什麼View.post在onCreate裡同樣可以得到View的寬高,是因為View.post發出的訊息,它被執行的時機是在View被繪製之後。

==可能有同學要問了:dispatchAttachedToWindow 方法是在 performMeasure 方法之前呼叫的,既然在呼叫的時候還沒有執行performMeasure來進行測量,那麼為什麼在執行完dispatchAttachedToWindow方法後就可以獲取到寬高呢?==

還是回到Handler機制最基本的原理,訊息是以佇列的形式存在訊息佇列裡,然後依次等待Loop執行的,而performTraversals的執行它本身就是在一個Runnable訊息裡,所以performTraversals在執行的時候,其他訊息得等performTraversals執行完了才能得到執行,也就是說mRunQueue.executeActions()的訊息必須得等performTraversals徹底執行完才能得到執行,所以View.post(runnable)中的runnable執行是要在performTraversals方法執行完之後的,並非一呼叫dispatchAttachedToWindow就會執行。

前面還遺留了一個問題:View.post方法裡的mAttachInfo是在什麼時候賦值的呢?

1
2
3
4
5
6
7
public  ViewRootImpl(Context context, Display display) {
     ...
      
     mAttachInfo =  new  View.AttachInfo(mWindowSession, mWindow, display,  this , mHandler,  this ,
         context);
     ...
}

我們看到它是在ViewRootImpl的建構函式裡被賦值的,那麼ViewRootImpl是什麼時候被建立的呢?順著往上找,我們看到,它是在WindowManagerGlobal的add方法裡被建立的。

1
2
3
4
5
6
7
public  void  addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
     ...
     ViewRootImpl root;
     ...
     root =  new  ViewRootImpl(view.getContext(), display);
     ...
}

前面也講了WindowManagerGlobal的addView方法是在ActivityThread的handleResumeActivity()方法裡被執行的,所以問題就解開了,為什麼View.post方法裡會先判斷mAttachInfo是否為空,不為空,說明View.post的呼叫時機是在onResume之後,也就是View繪製完成之後,這個時候直接推入主執行緒訊息佇列執行就可以。而如果mAttachInfo為空,說明View還沒繪製完,先暫存起來,待繪製完後再依次推入主執行緒執行。

要注意的是View.post方法是有坑的,android版本 < 24,也就是7.0以下的系統。

1
2
3
4
5
6
7
8
9
10
11
// 7.0以下系統
public  boolean  post(Runnable action) {
     final  AttachInfo attachInfo = mAttachInfo;
     if  (attachInfo !=  null ) {
         return  attachInfo.mHandler.post(action);
     }
     // Assume that post will succeed later
     // 注意此處,不同於我7.0及以上系統,
     ViewRootImpl.getRunQueue().post(action);
     return  true ;
}

而我們看一下 ViewRootImpl 的RunQueue是怎麼實現的:

1
2
3
4
5
6
7
8
9
10
static  final  ThreadLocal<RunQueue> sRunQueues =  new  ThreadLocal<RunQueue>();
static  RunQueue getRunQueue() {
     RunQueue rq = sRunQueues.get();
     if  (rq !=  null ) {
         return  rq;
     }
     rq =  new  RunQueue();
     sRunQueues.set(rq);
     return  rq;
}

結合前面講的ThreadLocal特性,它是跟執行緒相關的,也就是說儲存其中的變數只在本執行緒內可見,其他執行緒獲取不到。

好了,假設有這種場景,我們子執行緒裡用View.post一個訊息,從上面的程式碼看,它會儲存子執行緒的ThreadLocal裡,但是在執行RunQueue的時候,又是在主執行緒裡去找runnable呼叫,因為ThreadLocal執行緒隔離,主執行緒永遠也找不到這個訊息,這個訊息也就沒法得到執行了。

而7.0及以上沒有這個問題,是因為在post方法裡把runnable儲存在主執行緒裡:getRunQueue().post(action)。

總結一下:

上面這個問題的前提有兩個:View被繪製前,且在子執行緒裡呼叫View.post。如果View.post是在View被繪製之後,也就是mAttachInfo非空,那麼會立即推入主執行緒呼叫,也就不存在因執行緒隔離找不到runnable的問題。


作者:He Ying


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2738666/,如需轉載,請註明出處,否則將追究法律責任。

相關文章