06.Android之消息機制問題

目錄介紹

  • 6.0.0.1 談談消息機制Hander作用弃衍?有哪些要素?流程是怎樣的坚俗?
  • 6.0.0.2 為什么一個線程只有一個Looper镜盯、只有一個MessageQueue,可以有多個Handler猖败?
  • 6.0.0.3 可以在子線程直接new一個Handler嗎速缆?會出現(xiàn)什么問題,那該怎么做恩闻?
  • 6.0.0.4 Looper.prepare()能否調用兩次或者多次艺糜,會出現(xiàn)什么情況?
  • 6.0.0.5 為什么系統(tǒng)不建議在子線程訪問UI幢尚,不對UI控件的訪問加上鎖機制的原因破停?
  • 6.0.0.6 如何獲取當前線程的Looper?是怎么實現(xiàn)的侠草?(理解ThreadLocal)
  • 6.0.0.7 Looper.loop是一個死循環(huán)辱挥,拿不到需要處理的Message就會阻塞,那在UI線程中為什么不會導致ANR边涕?
  • 6.0.0.8 Handler.sendMessageDelayed()怎么實現(xiàn)延遲的晤碘?結合Looper.loop()循環(huán)中,Message=messageQueue.next()和MessageQueue.enqueueMessage()分析功蜓。
  • 6.0.0.9 Message可以如何創(chuàng)建园爷?哪種效果更好,為什么式撼?
  • 6.0.1.3 使用Hanlder的postDealy()后消息隊列會發(fā)生什么變化童社?
  • 6.0.1.4 ThreadLocal有什么作用?

好消息

  • 博客筆記大匯總【15年10月到至今】著隆,包括Java基礎及深入知識點扰楼,Android技術博客呀癣,Python學習筆記等等,還包括平時開發(fā)中遇到的bug匯總弦赖,當然也在工作之余收集了大量的面試題项栏,長期更新維護并且修正,持續(xù)完善……開源的文件是markdown格式的蹬竖!同時也開源了生活博客沼沈,從12年起,積累共計500篇[近100萬字]币厕,將會陸續(xù)發(fā)表到網上列另,轉載請注明出處,謝謝旦装!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好页衙,可以star一下,謝謝阴绢!當然也歡迎提出建議拷姿,萬事起于忽微,量變引起質變旱函!所有的筆記將會更新到GitHub上响巢,同時保持更新,歡迎同行提出或者push不同的看法或者筆記棒妨!

6.0.0.1 談談消息機制Hander作用踪古?有哪些要素?流程是怎樣的券腔?

  • 作用:
    • 跨線程通信伏穆。當子線程中進行耗時操作后需要更新UI時,通過Handler將有關UI的操作切換到主線程中執(zhí)行纷纫。
  • 四要素:
    • Message(消息):需要被傳遞的消息枕扫,其中包含了消息ID,消息處理對象以及處理的數(shù)據(jù)等辱魁,由MessageQueue統(tǒng)一列隊烟瞧,最終由Handler處理。技術博客大總結
    • MessageQueue(消息隊列):用來存放Handler發(fā)送過來的消息染簇,內部通過單鏈表的數(shù)據(jù)結構來維護消息列表参滴,等待Looper的抽取。
    • Handler(處理者):負責Message的發(fā)送及處理锻弓。通過 Handler.sendMessage() 向消息池發(fā)送各種消息事件砾赔;通過 Handler.handleMessage() 處理相應的消息事件。
    • Looper(消息泵):通過Looper.loop()不斷地從MessageQueue中抽取Message,按分發(fā)機制將消息分發(fā)給目標處理者暴心。
  • 具體流程
    • Handler.sendMessage()發(fā)送消息時妓盲,會通過MessageQueue.enqueueMessage()向MessageQueue中添加一條消息;
    • 通過Looper.loop()開啟循環(huán)后专普,不斷輪詢調用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中記錄了負責發(fā)送和處理消息的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嗎尘奏?會出現(xiàn)什么問題滩褥,那該怎么做?

  • 不同于主線程直接new一個Handler炫加,由于子線程的Looper需要手動去創(chuàng)建瑰煎,在創(chuàng)建Handler時需要多一些方法:
    • Handler的工作是依賴于Looper的,而Looper(與消息隊列)又是屬于某一個線程(ThreadLocal是線程內部的數(shù)據(jù)存儲類俗孝,通過它可以在指定線程中存儲數(shù)據(jù)丢间,其他線程則無法獲取到),其他線程不能訪問驹针。因此Handler就是間接跟線程是綁定在一起了烘挫。因此要使用Handler必須要保證Handler所創(chuàng)建的線程中有Looper對象并且啟動循環(huán)。因為子線程中默認是沒有Looper的,所以會報錯饮六。
    • 正確的使用方法是:技術博客大總結
    handler = null;
    new Thread(new Runnable() {
       private Looper mLooper;
       @Override
       public void run() {
           //必須調用Looper的prepare方法為當前線程創(chuàng)建一個Looper對象其垄,然后啟動循環(huán)
           //prepare方法中實質是給ThreadLocal對象創(chuàng)建了一個Looper對象
           //如果當前線程已經創(chuàng)建過Looper對象了,那么會報錯
           Looper.prepare();
           handler = new Handler();
           //獲取Looper對象
           mLooper = Looper.myLooper();
           //啟動消息循環(huán)
           Looper.loop();
           //在適當?shù)臅r候退出Looper的消息循環(huán)卤橄,防止內存泄漏
           mLooper.quit();
       }
    }).start();
    
  • 主線程中默認是創(chuàng)建了Looper并且啟動了消息的循環(huán)的绿满,因此不會報錯:應用程序的入口是ActivityThread的main方法,在這個方法里面會創(chuàng)建Looper窟扑,并且執(zhí)行Looper的loop方法來啟動消息的循環(huán)喇颁,使得應用程序一直運行。

6.0.0.4 Looper.prepare()能否調用兩次或者多次嚎货,會出現(xiàn)什么情況橘霎?

  • 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 為什么系統(tǒng)不建議在子線程訪問UI巧勤,不對UI控件的訪問加上鎖機制的原因嵌灰?

  • 為什么系統(tǒng)不建議在子線程訪問UI
    • 系統(tǒng)不建議在子線程訪問UI的原因是,UI控件非線程安全颅悉,在多線程中并發(fā)訪問可能會導致UI控件處于不可預期的狀態(tài)沽瞭。
  • 不對UI控件的訪問加上鎖機制的原因
    • 上鎖會讓UI控件變得復雜和低效
    • 上鎖后會阻塞某些進程的執(zhí)行技術博客大總結

6.0.0.7 Looper.loop是一個死循環(huán),拿不到需要處理的Message就會阻塞剩瓶,那在UI線程中為什么不會導致ANR驹溃?

  • 問題描述
    • 在處理消息的時候使用了Looper.loop()方法,并且在該方法中進入了一個死循環(huán)延曙,同時Looper.loop()方法是在主線程中調用的豌鹤,那么為什么沒有造成阻塞呢?
  • ActivityThread中main方法
    • ActivityThread類的注釋上可以知道這個類管理著我們平常所說的主線程(UI線程)
      • 首先 ActivityThread 并不是一個 Thread枝缔,就只是一個 final 類而已布疙。我們常說的主線程就是從這個類的 main 方法開始蚊惯,main 方法很簡短
      public static final void main(String[] args) {
          ...
          //創(chuàng)建Looper和MessageQueue
          Looper.prepareMainLooper();
          ...
          //輪詢器開始輪詢
          Looper.loop();
          ...
      }
      
  • Looper.loop()方法無限循環(huán)
    • 看看Looper.loop()方法無限循環(huán)部分的代碼
      while (true) {
         //取出消息隊列的消息,可能會阻塞
         Message msg = queue.next(); // might block
         ...
         //解析消息灵临,分發(fā)消息
         msg.target.dispatchMessage(msg);
         ...
      }
      
  • 為什么這個死循環(huán)不會造成ANR異常呢截型?
    • 因為Android 的是由事件驅動的,looper.loop() 不斷地接收事件儒溉、處理事件宦焦,每一個點擊觸摸或者說Activity的生命周期都是運行在 Looper.loop() 的控制之下,如果它停止了顿涣,應用也就停止了波闹。只能是某一個消息或者說對消息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它涛碑。技術博客大總結
  • 處理消息handleMessage方法
    • 如下所示
      • 可以看見Activity的生命周期都是依靠主線程的Looper.loop精堕,當收到不同Message時則采用相應措施。
      • 如果某個消息處理時間過長锌唾,比如你在onCreate(),onResume()里面處理耗時操作,那么下一次的消息比如用戶的點擊事件不能處理了夺英,整個循環(huán)就會產生卡頓晌涕,時間一長就成了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的循環(huán)消耗性能嗎痛悯?
    • 主線程Looper從消息隊列讀取消息余黎,當讀完所有消息時,主線程阻塞载萌。子線程往消息隊列發(fā)送消息惧财,并且往管道文件寫數(shù)據(jù),主線程即被喚醒扭仁,從管道文件讀取數(shù)據(jù)垮衷,主線程被喚醒只是為了讀取消息,當消息讀取完畢乖坠,再次睡眠搀突。因此loop的循環(huán)并不會對CPU性能有過多的消耗。
    • 簡單的來說:ActivityThread的main方法主要就是做消息循環(huán)熊泵,一旦退出消息循環(huán)仰迁,那么你的程序也就可以退出了。

6.0.0.9 Message可以如何創(chuàng)建?哪種效果更好,為什么垢袱?runOnUiThread如何實現(xiàn)子線程更新UI辆脸?

  • 創(chuàng)建Message對象的幾種方式:技術博客大總結
    • Message msg = new Message();
    • Message msg = Message.obtain();
    • Message msg = handler1.obtainMessage();
  • 后兩種方法都是從整個Messge池中返回一個新的Message實例,能有效避免重復Message創(chuàng)建對象喧锦,因此更鼓勵這種方式創(chuàng)建Message
  • runOnUiThread如何實現(xiàn)子線程更新UI
    • 看看源碼辆童,如下所示
    • 如果msg.callback為空的話营密,會直接調用我們的mCallback.handleMessage(msg)澄步,即handler的handlerMessage方法冰蘑。由于Handler對象是在主線程中創(chuàng)建的,所以handler的handlerMessage方法的執(zhí)行也會在主線程中村缸。
    • 在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()后消息隊列會發(fā)生什么變化?

  • post delay的Message并不是先等待一定時間再放入到MessageQueue中东羹,而是直接進入并阻塞當前線程剂桥,然后將其delay的時間和隊頭的進行比較,按照觸發(fā)時間進行排序属提,如果觸發(fā)時間更近則放入隊頭权逗,保證隊頭的時間最小、隊尾的時間最大冤议。此時斟薇,如果隊頭的Message正是被delay的,則將當前線程堵塞一段時間恕酸,直到等待足夠時間再喚醒執(zhí)行該Message堪滨,否則喚醒后直接執(zhí)行。

6.0.1.4 ThreadLocal有什么作用蕊温?

  • 線程本地存儲的功能
    • ThreadLocal類可實現(xiàn)線程本地存儲的功能袱箱,把共享數(shù)據(jù)的可見范圍限制在同一個線程之內,無須同步就能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題义矛,這里可理解為ThreadLocal幫助Handler找到本線程的Looper发笔。
    • 技術博客大總結
  • 怎么存儲呢?底層數(shù)據(jù)結構是啥凉翻?
    • 每個線程的Thread對象中都有一個ThreadLocalMap對象筐咧,它存儲了一組以ThreadLocal.threadLocalHashCode為key、以本地線程變量為value的鍵值對噪矛,而ThreadLocal對象就是當前線程的ThreadLocalMap的訪問入口量蕊,也就包含了一個獨一無二的threadLocalHashCode值,通過這個值就可以在線程鍵值值對中找回對應的本地線程變量艇挨。

關于其他內容介紹

01.關于博客匯總鏈接

02.關于我的博客

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缩滨,隨后出現(xiàn)的幾起案子势就,更是在濱河造成了極大的恐慌泉瞻,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苞冯,死亡現(xiàn)場離奇詭異袖牙,居然都是意外死亡,警方通過查閱死者的電腦和手機舅锄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門鞭达,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人皇忿,你說我怎么就攤上這事畴蹭。” “怎么了鳍烁?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵叨襟,是天一觀的道長。 經常有香客問我幔荒,道長糊闽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任爹梁,我火速辦了婚禮右犹,結果婚禮上,老公的妹妹穿的比我還像新娘卫键。我一直安慰自己傀履,他們只是感情好虱朵,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布莉炉。 她就那樣靜靜地躺著,像睡著了一般碴犬。 火紅的嫁衣襯著肌膚如雪絮宁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天服协,我揣著相機與錄音绍昂,去河邊找鬼。 笑死偿荷,一個胖子當著我的面吹牛窘游,可吹牛的內容都是我干的。 我是一名探鬼主播跳纳,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼忍饰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寺庄?” 一聲冷哼從身側響起艾蓝,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤力崇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赢织,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亮靴,經...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年于置,在試婚紗的時候發(fā)現(xiàn)自己被綠了茧吊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡俱两,死狀恐怖饱狂,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情宪彩,我是刑警寧澤休讳,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站尿孔,受9級特大地震影響俊柔,放射性物質發(fā)生泄漏。R本人自食惡果不足惜活合,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一雏婶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧白指,春花似錦留晚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至橄唬,卻和暖如春赋焕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仰楚。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工隆判, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人僧界。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓侨嘀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親捂襟。 傳聞我的和親對象是個殘疾皇子咬腕,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內容