06.Android之消息機制問題

目錄介紹

  • 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萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 連接地址:https://github.com/yangchong211/YCBlogs
  • 若是以爲好,能夠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,不知道這樣解釋是否合理!更多能夠查看個人博客彙總:https://github.com/yangchong211/YCBlogs
    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.關於個人博客

相關文章
相關標籤/搜索