06.Android之訊息機制問題

楊充發表於2019-01-11

目錄介紹

  • 6.0.0.1 談談訊息機制Hander作用?有哪些要素?流程是怎樣的?
  • 6.0.0.2 為什麼一個執行緒只有一個Looper、只有一個MessageQueue,可以有多個Handler?
  • 6.0.0.3 可以在子執行緒直接new一個Handler嗎?會出現什麼問題,那該怎麼做?
  • 6.0.0.4 Looper.prepare()能否呼叫兩次或者多次,會出現什麼情況?
  • 6.0.0.5 為什麼系統不建議在子執行緒訪問UI,不對UI控制元件的訪問加上鎖機制的原因?
  • 6.0.0.6 如何獲取當前執行緒的Looper?是怎麼實現的?(理解ThreadLocal)
  • 6.0.0.7 Looper.loop是一個死迴圈,拿不到需要處理的Message就會阻塞,那在UI執行緒中為什麼不會導致ANR?
  • 6.0.0.8 Handler.sendMessageDelayed()怎麼實現延遲的?結合Looper.loop()迴圈中,Message=messageQueue.next()和MessageQueue.enqueueMessage()分析。
  • 6.0.0.9 Message可以如何建立?哪種效果更好,為什麼?
  • 6.0.1.3 使用Hanlder的postDealy()後訊息佇列會發生什麼變化?
  • 6.0.1.4 ThreadLocal有什麼作用?

好訊息

  • 部落格筆記大彙總【15年10月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計500篇[近100萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!所有的筆記將會更新到GitHub上,同時保持更新,歡迎同行提出或者push不同的看法或者筆記!

6.0.0.1 談談訊息機制Hander作用?有哪些要素?流程是怎樣的?

  • 作用:
    • 跨執行緒通訊。當子執行緒中進行耗時操作後需要更新UI時,通過Handler將有關UI的操作切換到主執行緒中執行。
  • 四要素:
    • Message(訊息):需要被傳遞的訊息,其中包含了訊息ID,訊息處理物件以及處理的資料等,由MessageQueue統一列隊,最終由Handler處理。技術部落格大總結
    • MessageQueue(訊息佇列):用來存放Handler傳送過來的訊息,內部通過單連結串列的資料結構來維護訊息列表,等待Looper的抽取。
    • Handler(處理者):負責Message的傳送及處理。通過 Handler.sendMessage() 向訊息池傳送各種訊息事件;通過 Handler.handleMessage() 處理相應的訊息事件。
    • Looper(訊息泵):通過Looper.loop()不斷地從MessageQueue中抽取Message,按分發機制將訊息分發給目標處理者。
  • 具體流程
    • Handler.sendMessage()傳送訊息時,會通過MessageQueue.enqueueMessage()向MessageQueue中新增一條訊息;
    • 通過Looper.loop()開啟迴圈後,不斷輪詢呼叫MessageQueue.next();
    • 呼叫目標Handler.dispatchMessage()去傳遞訊息,目標Handler收到訊息後呼叫Handler.handlerMessage()處理訊息。
    • image

6.0.0.2 為什麼一個執行緒只有一個Looper、只有一個MessageQueue,可以有多個Handler?

  • 注意:一個Thread只能有一個Looper,可以有多個Handler
    • Looper有一個MessageQueue,可以處理來自多個Handler的Message;MessageQueue有一組待處理的Message,這些Message可來自不同的Handler;Message中記錄了負責傳送和處理訊息的Handler;Handler中有Looper和MessageQueue。
  • 為什麼一個執行緒只有一個Looper?技術部落格大總結
    • 需使用Looper的prepare方法,Looper.prepare()。可以看下原始碼,Android中一個執行緒最多僅僅能有一個Looper,若在已有Looper的執行緒中呼叫Looper.prepare()會丟擲RuntimeException(“Only one Looper may be created per thread”)。
    • 所以一個執行緒只有一個Looper,不知道這樣解釋是否合理!更多可以檢視我的部落格彙總:github.com/yangchong21…
    public static void prepare() {
        prepare(true);
    }
    
    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));
    }
    複製程式碼

6.0.0.3 可以在子執行緒直接new一個Handler嗎?會出現什麼問題,那該怎麼做?

  • 不同於主執行緒直接new一個Handler,由於子執行緒的Looper需要手動去建立,在建立Handler時需要多一些方法:
    • Handler的工作是依賴於Looper的,而Looper(與訊息佇列)又是屬於某一個執行緒(ThreadLocal是執行緒內部的資料儲存類,通過它可以在指定執行緒中儲存資料,其他執行緒則無法獲取到),其他執行緒不能訪問。因此Handler就是間接跟執行緒是繫結在一起了。因此要使用Handler必須要保證Handler所建立的執行緒中有Looper物件並且啟動迴圈。因為子執行緒中預設是沒有Looper的,所以會報錯。
    • 正確的使用方法是:技術部落格大總結
    handler = null;
    new Thread(new Runnable() {
       private Looper mLooper;
       @Override
       public void run() {
           //必須呼叫Looper的prepare方法為當前執行緒建立一個Looper物件,然後啟動迴圈
           //prepare方法中實質是給ThreadLocal物件建立了一個Looper物件
           //如果當前執行緒已經建立過Looper物件了,那麼會報錯
           Looper.prepare();
           handler = new Handler();
           //獲取Looper物件
           mLooper = Looper.myLooper();
           //啟動訊息迴圈
           Looper.loop();
           //在適當的時候退出Looper的訊息迴圈,防止記憶體洩漏
           mLooper.quit();
       }
    }).start();
    複製程式碼
  • 主執行緒中預設是建立了Looper並且啟動了訊息的迴圈的,因此不會報錯:應用程式的入口是ActivityThread的main方法,在這個方法裡面會建立Looper,並且執行Looper的loop方法來啟動訊息的迴圈,使得應用程式一直執行。

6.0.0.4 Looper.prepare()能否呼叫兩次或者多次,會出現什麼情況?

  • Looper.prepare()方法原始碼分析
    • 可以看到Looper中有一個ThreadLocal成員變數,熟悉JDK的同學應該知道,當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
    public static void prepare() {
        prepare(true);
    }
    
    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));
    }
    複製程式碼
  • 思考:Looper.prepare()能否呼叫兩次或者多次
    • 如果執行,則會報錯,並提示prepare中的Excetion資訊。由此可以得出在每個執行緒中Looper.prepare()能且只能呼叫一次
    • 技術部落格大總結
    //這裡Looper.prepare()方法呼叫了兩次
    Looper.prepare();
    Looper.prepare();
    Handler mHandler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           if (msg.what == 1) {
              Log.i(TAG, "在子執行緒中定義Handler,並接收到訊息。。。");
           }
       }
    };
    Looper.loop();
    複製程式碼

6.0.0.5 為什麼系統不建議在子執行緒訪問UI,不對UI控制元件的訪問加上鎖機制的原因?

  • 為什麼系統不建議在子執行緒訪問UI
    • 系統不建議在子執行緒訪問UI的原因是,UI控制元件非執行緒安全,在多執行緒中併發訪問可能會導致UI控制元件處於不可預期的狀態。
  • 不對UI控制元件的訪問加上鎖機制的原因

6.0.0.7 Looper.loop是一個死迴圈,拿不到需要處理的Message就會阻塞,那在UI執行緒中為什麼不會導致ANR?

  • 問題描述
    • 在處理訊息的時候使用了Looper.loop()方法,並且在該方法中進入了一個死迴圈,同時Looper.loop()方法是在主執行緒中呼叫的,那麼為什麼沒有造成阻塞呢?
  • ActivityThread中main方法
    • ActivityThread類的註釋上可以知道這個類管理著我們平常所說的主執行緒(UI執行緒)
      • 首先 ActivityThread 並不是一個 Thread,就只是一個 final 類而已。我們常說的主執行緒就是從這個類的 main 方法開始,main 方法很簡短
      public static final void main(String[] args) {
          ...
          //建立Looper和MessageQueue
          Looper.prepareMainLooper();
          ...
          //輪詢器開始輪詢
          Looper.loop();
          ...
      }
      複製程式碼
  • Looper.loop()方法無限迴圈
    • 看看Looper.loop()方法無限迴圈部分的程式碼
      while (true) {
         //取出訊息佇列的訊息,可能會阻塞
         Message msg = queue.next(); // might block
         ...
         //解析訊息,分發訊息
         msg.target.dispatchMessage(msg);
         ...
      }
      複製程式碼
  • 為什麼這個死迴圈不會造成ANR異常呢?
    • 因為Android 的是由事件驅動的,looper.loop() 不斷地接收事件、處理事件,每一個點選觸控或者說Activity的生命週期都是執行在 Looper.loop() 的控制之下,如果它停止了,應用也就停止了。只能是某一個訊息或者說對訊息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。技術部落格大總結
  • 處理訊息handleMessage方法
    • 如下所示
      • 可以看見Activity的生命週期都是依靠主執行緒的Looper.loop,當收到不同Message時則採用相應措施。
      • 如果某個訊息處理時間過長,比如你在onCreate(),onResume()裡面處理耗時操作,那麼下一次的訊息比如使用者的點選事件不能處理了,整個迴圈就會產生卡頓,時間一長就成了ANR。
      public void handleMessage(Message msg) {
          if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
          switch (msg.what) {
              case LAUNCH_ACTIVITY: {
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                  final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                  r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo);
                  handleLaunchActivity(r, null);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
              }
              break;
              case RELAUNCH_ACTIVITY: {
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
                  ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                  handleRelaunchActivity(r);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
              }
              break;
              case PAUSE_ACTIVITY:
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
                  handlePauseActivity((IBinder) msg.obj, false, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 2) != 0);
                  maybeSnapshot();
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                  break;
              case PAUSE_ACTIVITY_FINISHING:
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
                  handlePauseActivity((IBinder) msg.obj, true, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 1) != 0);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                  break;
              ...........
          }
      }
      複製程式碼
  • loop的迴圈消耗效能嗎?
    • 主執行緒Looper從訊息佇列讀取訊息,當讀完所有訊息時,主執行緒阻塞。子執行緒往訊息佇列傳送訊息,並且往管道檔案寫資料,主執行緒即被喚醒,從管道檔案讀取資料,主執行緒被喚醒只是為了讀取訊息,當訊息讀取完畢,再次睡眠。因此loop的迴圈並不會對CPU效能有過多的消耗。
    • 簡單的來說:ActivityThread的main方法主要就是做訊息迴圈,一旦退出訊息迴圈,那麼你的程式也就可以退出了。

6.0.0.9 Message可以如何建立?哪種效果更好,為什麼?runOnUiThread如何實現子執行緒更新UI?

  • 建立Message物件的幾種方式:技術部落格大總結
    • Message msg = new Message();
    • Message msg = Message.obtain();
    • Message msg = handler1.obtainMessage();
  • 後兩種方法都是從整個Messge池中返回一個新的Message例項,能有效避免重複Message建立物件,因此更鼓勵這種方式建立Message
  • runOnUiThread如何實現子執行緒更新UI
    • 看看原始碼,如下所示
    • 如果msg.callback為空的話,會直接呼叫我們的mCallback.handleMessage(msg),即handler的handlerMessage方法。由於Handler物件是在主執行緒中建立的,所以handler的handlerMessage方法的執行也會在主執行緒中。
    • 在runOnUiThread程式首先會判斷當前執行緒是否是UI執行緒,如果是就直接執行,如果不是則post,這時其實質還是使用的Handler機制來處理執行緒與UI通訊。
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    
    @Override
    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }
    複製程式碼

6.0.1.3 使用Hanlder的postDealy()後訊息佇列會發生什麼變化?

  • post delay的Message並不是先等待一定時間再放入到MessageQueue中,而是直接進入並阻塞當前執行緒,然後將其delay的時間和隊頭的進行比較,按照觸發時間進行排序,如果觸發時間更近則放入隊頭,保證隊頭的時間最小、隊尾的時間最大。此時,如果隊頭的Message正是被delay的,則將當前執行緒堵塞一段時間,直到等待足夠時間再喚醒執行該Message,否則喚醒後直接執行。

6.0.1.4 ThreadLocal有什麼作用?

  • 執行緒本地儲存的功能
    • ThreadLocal類可實現執行緒本地儲存的功能,把共享資料的可見範圍限制在同一個執行緒之內,無須同步就能保證執行緒之間不出現資料爭用的問題,這裡可理解為ThreadLocal幫助Handler找到本執行緒的Looper。
    • 技術部落格大總結
  • 怎麼儲存呢?底層資料結構是啥?
    • 每個執行緒的Thread物件中都有一個ThreadLocalMap物件,它儲存了一組以ThreadLocal.threadLocalHashCode為key、以本地執行緒變數為value的鍵值對,而ThreadLocal物件就是當前執行緒的ThreadLocalMap的訪問入口,也就包含了一個獨一無二的threadLocalHashCode值,通過這個值就可以線上程鍵值值對中找回對應的本地執行緒變數。

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章