溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android中的消息機(jī)制Handler是什么?

發(fā)布時(shí)間:2020-05-22 16:39:38 來源:億速云 閱讀:310 作者:鴿子 欄目:移動開發(fā)

前言
做 Android 開發(fā)肯定離不開跟 Handler 打交道,它通常被我們用來做主線程與子線程之間的通信工具,而 Handler 作為 Android 中消息機(jī)制的重要一員也確實(shí)給我們的開發(fā)帶來了極大的便利。


可以說只要有異步線程與主線程通信的地方就一定會有 Handler。
那么;

  • Handler 的通信機(jī)制的背后的原理是什么?
  • Handler、Thread 和 HandlerThread 的差別?
  • 消息機(jī)制 Handler 作用 ?有哪些要素 ?流程是怎樣的 ?
  • Handler 引起的內(nèi)存泄露原因以及最佳解決方案
  • 使用 Handler 的 postDealy 后消息隊(duì)列會有什么變化?
  • 可以在子線程直接 new 一個 Handler 嗎?怎么做?
  • Handler 中有 Loop 死循環(huán),為什么沒有阻塞 主線程,原理是什么

本文將持續(xù)為你揭曉


一丶Handler原理

handler 整個流程中,主要有四個對象,handler,Message,MessageQueue,Looper。 當(dāng)應(yīng)用創(chuàng)建的時(shí)候,就會在主線程中創(chuàng)建 handler 對象,

Android 中主線程是不能進(jìn)行耗時(shí)操作的,子線程是不能進(jìn)行更新 UI 的。所以就有了 handler, 它的作用就是實(shí)現(xiàn)線程之間的通信。

當(dāng)應(yīng)用創(chuàng) 建的時(shí)候,就會在主線程中創(chuàng)建 handler對象, 我們通過要傳送的消息保存到 Message 中,handler 通過調(diào)用 sendMessage 方法 將 Message 發(fā)送到 MessageQueue 中,Looper 對象就會不斷的調(diào)用 loop()方法

不斷的從 MessageQueue 中取出 Message 交給 handler 進(jìn)行處理。從而實(shí)現(xiàn)線程 之間的通信。


二丶Handler、Thread 和 HandlerThread 的差別:

1) Handler 線程的消息通訊的橋梁,主要用來發(fā)送消息及處理消息。

2) Thread 普通線程,如果需要有自己的消息隊(duì)列,需要調(diào)用 Looper.prepare()創(chuàng)建 Looper 實(shí)例,調(diào)用 loop()去循環(huán)消息。

3) HandlerThread 是一個帶有 Looper 的線程,在 HandleThread 的 run()方法中調(diào)用了 Looper.prepare()創(chuàng)建了 Looper 實(shí)例,并調(diào)用 Looper.loop()開啟了 Loop 循環(huán),循環(huán)從消息隊(duì) 列中獲取消息并交由 Handler 處理。利用該線程的 Looper 創(chuàng)建 Handler 實(shí)例,此 Handler 的 handleMessage()方法是運(yùn)行在子線程中的。即 Handler 利用哪個線程的 Looper 創(chuàng)建的實(shí)例, 它就和相應(yīng)的線程綁定到一起,處理該線程上的消息,它的 handleMessage()方法就是在那 個線程中運(yùn)行的,無參構(gòu)造默認(rèn)是主線程。

HandlerThread 提供了 quit()/quitSafely()方法退出 HandlerThread 的消息循環(huán),它們分別調(diào)用 LooperquitquitSafely 方法,quit 會將消息 隊(duì)列中的所有消息移除,而 quitSafely 會將消息隊(duì)列所有延遲消息移除,非延遲消息派發(fā)出 去讓 Handler 去處理。

HandlerThread 適合處理本地 IO 讀寫操作(讀寫數(shù)據(jù)庫或文件),因?yàn)楸镜?IO 操作耗 時(shí)不長,對于單線程+異步隊(duì)列不會產(chǎn)生較大阻塞,而網(wǎng)絡(luò)操作相對比較耗時(shí),容易阻塞后 面的請求,因此HandlerThread 不適合加入網(wǎng)絡(luò)操作


三丶消息機(jī)制 Handler 作用 ?有哪些要素 ?流程是怎樣的 ?

負(fù)責(zé)跨線程通信,這是因?yàn)樵谥骶€程不能做耗時(shí)操作,而子線程不能更新 UI,所以當(dāng)子線程中進(jìn)行耗時(shí)操作后需要更新 UI時(shí),通過 Handler 將有關(guān) UI 的操作切換到主線程中執(zhí)行。Handler 作用 ?有哪些要素 ?流程是怎樣的 ?
具體分為四大要素
①M(fèi)essage(消息): 需要被傳遞的消息,消息分為硬件產(chǎn)生的消息(如按鈕、觸摸)和軟件生成的消息。
②MessageQueue(消息隊(duì)列): 負(fù)責(zé)消息的存儲與管理,負(fù)責(zé)管理由 Handler 發(fā)送過來的 Message。讀取會自動刪除消息,單鏈表維護(hù),插入和刪除上有優(yōu)勢。在其 next()方法中會無限循環(huán),不斷判斷是否有消息,有就返回這條消息并移除
③Handler(消息處理器): 負(fù)責(zé) Message 的發(fā)送及處理。主要向消息池發(fā)送各種消息事件(Handler.sendMessage())和處理相應(yīng)消息事件(Handler.handleMessage()),按照先進(jìn)先出執(zhí)行,內(nèi)部使用的是單鏈表的結(jié)構(gòu)。

④Looper(消息池): 負(fù)責(zé)關(guān)聯(lián)線程以及消息的分發(fā),在該線程下從 MessageQueue 獲取 Message,分發(fā)給Handler,Looper 創(chuàng)建的時(shí)候會創(chuàng)建一個MessageQueue,調(diào)用 loop()方法的時(shí)候消息循環(huán)開始,其中會不斷調(diào)用 messageQueue 的 next()方法,當(dāng)有消息就處理,否則阻塞在 messageQueue 的next()方法中。當(dāng) Looper 的 quit()被調(diào)用的時(shí)候會調(diào)用messageQueue 的 quit(),此時(shí) next()會返回 null,然后 loop()方法也就跟著退出。


四丶Handler 引起的內(nèi)存泄露原因以及最佳解決方案

泄露原因:
Handler 允許我們發(fā)送延時(shí)消息,如果在延時(shí)期間用戶關(guān)閉了 Activity,那么該 Activity會泄露。 這個泄露是因?yàn)?Message會持有 Handler,而又因?yàn)?Java 的特性,內(nèi)部類會持有外部類,使得 Activity 會被Handler 持有,這樣最終就導(dǎo)致 Activity 泄露。

解決方案:
將 Handler 定義成靜態(tài)的內(nèi)部類,在內(nèi)部持有
Activity 的弱引用,并在 Acitivity 的 onDestroy()中調(diào)用 handler.removeCallbacksAndMessages(null)及時(shí)移除所有消息


五丶使用 Handler 的 postDealy 后消息隊(duì)列會有什么變化?

如果隊(duì)列中只有這個消息,那么消息不會被發(fā)送,而是計(jì)算到時(shí)喚醒的時(shí)間,先將 Looper 阻塞,到時(shí)間就喚醒它。但如果此時(shí)要加入新消息,該消息隊(duì)列的對頭跟 delay 時(shí)間相比更長,則插入到頭部,按照觸發(fā)時(shí)間進(jìn)行排序,隊(duì)頭的時(shí)間最小、隊(duì)尾的時(shí)間最大


六丶可以在子線程直接 new 一個 Handler 嗎?怎么做?

不可以,因?yàn)樵谥骶€程中,Activity 內(nèi)部包含一個 Looper 對象,它會自動管理 Looper,處理子線程中發(fā)送過來的消息。而對于子線程而言,沒有任何對象幫助我們維護(hù) Looper 對象,所以需要我們自己手動維護(hù)。所以要在子線程開啟 Handler 要先創(chuàng)建 Looper,并開啟 Looper 循環(huán)

   //示例代碼
   new Thread(new Runnable(){
            @Override
             public void run() {
                 looper.prepare();
                 new Handler() {
                         @Override 
                         piblic void handlerMessage(Message msg) {
                             super,handleMessage(msg);
                         }
                 }
                 looper.loop();
            }
       }).start();
七丶Handler 中有 Loop 死循環(huán),為什么沒有阻塞 主線程,原理是什么

該問題很難被考到,但是如果一旦問到,100%會回答 不上來。開發(fā)者很難注意到一個主線程的四循環(huán)居然沒有阻塞住主 線程。

應(yīng)該從 主線程的消息循環(huán)機(jī)制 與 Linux 的循環(huán)異步等 待作用講起。最后將 handle 引起的內(nèi)存泄漏,內(nèi)存泄漏一定是一 個加分項(xiàng)
這里有簡單的幾個問題拋出來:

1.Looper 死循環(huán)為什么不會導(dǎo)致應(yīng)用卡死,會消耗大量資源嗎?
2.主線程的消息循環(huán)機(jī)制是什么(死循環(huán)如何處理其它事務(wù))?
3.ActivityThread 的動力是什么?(ActivityThread 執(zhí)行 Looper 的線程是什么)
4.Handler 是如何能夠線程切換,發(fā)送 Message 的?(線程間通訊)
5.子線程有哪些更新 UI 的方法。
6.子線程中 Toast,showDialog,的方法。(和子線程不能更新 UI 有關(guān)嗎)
7.如何處理 Handler 使用不當(dāng)導(dǎo)致的內(nèi)存泄露?

1.Looper 死循環(huán)為什么不會導(dǎo)致應(yīng)用卡死?

線程默認(rèn)沒有 Looper 的,如果需要使用 Handler 就必須為 線程創(chuàng)建 Looper。我們經(jīng)常提到的主線程,也叫 UI 線程, 它就是 ActivityThread,ActivityThread 被創(chuàng)建時(shí)就會初 始化 Looper,這也是在主線程中默認(rèn)可以使用 Handler 的 原因

我們先來看一段代碼:

 new Thread(new Runnable() {
        @Override
        public void run() {
            Log.e("qdx", "step 0 ");
            Looper.prepare();

            Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();

            Log.e("qdx", "step 1 ");
            Looper.loop();

            Log.e("qdx", "step 2 ");

        }
    }).start();

我們知道Looper.loop();里面維護(hù)了一個死循環(huán)方法,所以按照理論,上述代碼執(zhí)行的應(yīng)該是 step 0 –>step 1 也就是說循環(huán)在Looper.prepare();與Looper.loop();之間

在子線程中,如果手動為其創(chuàng)建了Looper,那么在所有的事情完成以后應(yīng)該調(diào)用quit方法來終止消息循環(huán),否則這個子線程就會一直處于等待(阻塞)狀態(tài),而如果退出Looper以后,這個線程就會立刻(執(zhí)行所有方法并)終止,因此建議不需要的時(shí)候終止Looper

執(zhí)行結(jié)果也正如我們所說,這時(shí)候如果了解ActivityThread,并且在main方法中我們會看到主線程也是通過Looper方式來維持一個消息循環(huán)。

public static void main(String[] args) {
    Looper.prepareMainLooper();//創(chuàng)建Looper和MessageQueue對象,用于處理主線程的消息

    ActivityThread thread = new ActivityThread();
    thread.attach(false);//建立Binder通道 (創(chuàng)建新線程)

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();

    //如果能執(zhí)行下面方法,說明應(yīng)用崩潰或者是退出了...
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

那么回到我們的問題上,這個死循環(huán)會不會導(dǎo)致應(yīng)用卡死,即使不會的話,它會慢慢的消耗事實(shí)上,會在進(jìn)入死循環(huán)之前便創(chuàng)建了新binder線程,在代碼ActivityThread.main()中:越來越多的資源嗎?

對于線程即是一段可執(zhí)行的代碼,當(dāng)可執(zhí)行代碼執(zhí)行完成后,線程生命周期便該終止了,線程退出。而對于主線程,我們是絕不希望會被運(yùn)行一段時(shí)間,自己就退出,那么如何保證能一直存活呢?簡單做法就是可執(zhí)行代碼是能一直執(zhí)行下去的,死循環(huán)便能保證不會被退出,例如,binder線程也是采用死循環(huán)的方法,通過循環(huán)方式不同與Binder驅(qū)動進(jìn)行讀寫操作,當(dāng)然并非簡單地死循環(huán),無消息時(shí)會休眠。但這里可能又引發(fā)了另一個問題,既然是死循環(huán)又如何去處理其他事務(wù)呢?通過創(chuàng)建新線程的方式。真正會卡死主線程的操作是在回調(diào)方法onCreate/onStart/onResume等操作時(shí)間過長,會導(dǎo)致掉幀,甚至發(fā)生ANR,looper.loop本身不會導(dǎo)致應(yīng)用卡死。
主線程的死循環(huán)一直運(yùn)行是不是特別消耗CPU資源呢? 其實(shí)不然,這里就涉及到Linux pipe/epoll機(jī)制,簡單說就是在主線程的MessageQueue沒有消息時(shí),便阻塞在loop的queue.next()中的nativePollOnce()方法里,此時(shí)主線程會釋放CPU資源進(jìn)入休眠狀態(tài),直到下個消息到達(dá)或者有事務(wù)發(fā)生,通過往pipe管道寫端寫入數(shù)據(jù)來喚醒主線程工作。這里采用的epoll機(jī)制,是一種IO多路復(fù)用機(jī)制,可以同時(shí)監(jiān)控多個描述符,當(dāng)某個描述符就緒(讀或?qū)懢途w),則立刻通知相應(yīng)程序進(jìn)行讀或?qū)懖僮?,本質(zhì)同步I/O,即讀寫是阻塞的。 所以說,主線程大多數(shù)時(shí)候都是處于休眠狀態(tài),并不會消耗大量CPU資源


2.主線程的消息循環(huán)機(jī)制是什么?

事實(shí)上,會在進(jìn)入死循環(huán)之前便創(chuàng)建了新binder線程,在代碼ActivityThread.main()中:

  public static void main(String[] args) {
  //創(chuàng)建Looper和MessageQueue對象,用于處理主線程的消息
  Looper.prepareMainLooper();

   //創(chuàng)建ActivityThread對象
   ActivityThread thread = new ActivityThread(); 

   //建立Binder通道 (創(chuàng)建新線程)
   thread.attach(false);

   Looper.loop(); //消息循環(huán)運(yùn)行
   throw new RuntimeException("Main thread loop unexpectedly exited");
   }

Activity的生命周期都是依靠主線程的 Looper.loop,當(dāng)收到不同Message時(shí)則采用相應(yīng)措施:一旦退出消息循環(huán),那么你的程序也就可以退出了。 從消息隊(duì)列中取消息可能會阻塞,取到消息會做出相應(yīng)的處理。如果某個消息處理時(shí)間過長,就可能會影響UI線程的刷新速率,造成卡頓的現(xiàn)象。

thread.attach(false)方法函數(shù)中便會創(chuàng)建一個Binder線程(具體是指ApplicationThread,Binder的服務(wù)端,用于接收系統(tǒng)服務(wù)AMS發(fā)送來的事件),該Binder線程通過Handler將Message發(fā)送給主線程。「Activity 啟動過程」

比如收到msg=H.LAUNCH_ACTIVITY,則調(diào)用ActivityThread.handleLaunchActivity()方法,最終會通過反射機(jī)制,創(chuàng)建Activity實(shí)例,然后再執(zhí)行Activity.onCreate()等方法;

再比如收到msg=H.PAUSE_ACTIVITY,則調(diào)用ActivityThread.handlePauseActivity()方法,最終會執(zhí)行Activity.onPause()等方法。

主線程的消息又是哪來的呢?當(dāng)然是App進(jìn)程中的其他線程通過Handler發(fā)送給主線程進(jìn)程


3.ActivityThread 的動力是什么?
進(jìn)程 每個app運(yùn)行時(shí)前首先創(chuàng)建一個進(jìn)程,該進(jìn)程是由Zygote fork出來的,用于承載App上運(yùn)行的各種Activity/Service等組件。進(jìn)程對于上層應(yīng)用來說是完全透明的,這也是google有意為之,讓App程序都是運(yùn)行在Android Runtime。大多數(shù)情況一個App就運(yùn)行在一個進(jìn)程中,除非在AndroidManifest.xml中配置Android:process屬性,或通過native代碼fork進(jìn)程

線程 線程對應(yīng)用來說非常常見,比如每次new Thread().start都會創(chuàng)建一個新的線程。該線程與App所在進(jìn)程之間資源共享,從Linux角度來說進(jìn)程與線程除了是否共享資源外,并沒有本質(zhì)的區(qū)別,都是一個task_struct結(jié)構(gòu)體,在CPU看來進(jìn)程或線程無非就是一段可執(zhí)行的代碼,CPU采用CFS調(diào)度算法,保證每個task都盡可能公平的享有CPU時(shí)間片。

其實(shí)承載ActivityThread的主線程就是由Zygote fork而創(chuàng)建的進(jìn)程。


4.Handler 是如何能夠線程切換

其實(shí)看完上面我們大致也清楚線程間是共享資源的。所以Handler處理不同線程問題就只要注意異步情況即可。

這里再引申出Handler的一些小知識點(diǎn)。 Handler創(chuàng)建的時(shí)候會采用當(dāng)前線程的Looper來構(gòu)造消息循環(huán)系統(tǒng),Looper在哪個線程創(chuàng)建,就跟哪個線程綁定,并且Handler是在他關(guān)聯(lián)的Looper對應(yīng)的線程中處理消息的。(敲黑板)

那么Handler內(nèi)部如何獲取到當(dāng)前線程的Looper呢—–ThreadLocal。ThreadLocal可以在不同的線程中互不干擾的存儲并提供數(shù)據(jù),通過ThreadLocal可以輕松獲取每個線程的Looper。

當(dāng)然需要注意的是:

①線程是默認(rèn)沒有Looper的,如果需要使用Handler,就必須為線程創(chuàng)建Looper。我們經(jīng)常提到的主線程,也叫UI線程,它就是ActivityThread,
②ActivityThread被創(chuàng)建時(shí)就會初始化Looper,這也是在主線程中默認(rèn)可以使用Handler的原因。

系統(tǒng)為什么不允許在子線程中訪問UI?(摘自《Android開發(fā)藝術(shù)探索》)
這是因?yàn)锳ndroid的UI控件不是線程安全的,如果在多線程中并發(fā)訪問可能會導(dǎo)致UI控件處于不可預(yù)期的狀態(tài),那么為什么系統(tǒng)不對UI控件的訪問加上鎖機(jī)制呢?
缺點(diǎn)有兩個:

①首先加上鎖機(jī)制會讓UI訪問的邏輯變得復(fù)雜
②鎖機(jī)制會降低UI訪問的效率,因?yàn)殒i機(jī)制會阻塞某些線程的執(zhí)行。 所以最簡單且高效的方法就是采用單線程模型來處理UI操作


5.子線程有哪些更新UI的方法

主線程中定義Handler,子線程通過mHandler發(fā)送消息,主線程Handler的handleMessage更新UI。 用Activity對象的runOnUiThread方法。 創(chuàng)建Handler,傳入getMainLooper。 View.post(Runnabler) 。

runOnUiThread 第一種咱們就不分析了,我們來看看第二種比較常用的寫法。

先重新溫習(xí)一下上面說的

Looper在哪個線程創(chuàng)建,就跟哪個線程綁定,并且Handler是在他關(guān)聯(lián)的Looper對應(yīng)的線程中處理消息的。(敲黑板)

  new Thread(new Runnable() {
        @Override
        public void run() {

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //DO UI method

                }
            });

        }
  }).start();

  final Handler mHandler = new Handler();

  public final void runOnUiThread(Runnable action) {
     if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);//子線程(非UI線程)
     } else {
        action.run();
     }
  }

進(jìn)入Activity類里面,可以看到如果是在子線程中,通過mHandler發(fā)送的更新UI消息。 而這個Handler是在Activity中創(chuàng)建的,也就是說在主線程中創(chuàng)建,所以便和我們在主線程中使用Handler更新UI沒有差別。 因?yàn)檫@個Looper,就是ActivityThread中創(chuàng)建的Looper(Looper.prepareMainLooper())。

創(chuàng)建Handler,傳入getMainLooper 那么同理,我們在子線程中,是否也可以創(chuàng)建一個Handler,并獲取MainLooper,從而在子線程中更新UI呢? 首先我們看到,在Looper類中有靜態(tài)對象sMainLooper,并且這個sMainLooper就是在ActivityThread中創(chuàng)建的MainLooper

  private static Looper sMainLooper;  // guarded by Looper.class

  public static void prepareMainLooper() {
     prepare(false);
     synchronized (Looper.class) {
         if (sMainLooper != null) {
             throw new IllegalStateException("The main Looper has already been prepared.");
         }
         sMainLooper = myLooper();
     }
  }

所以不用多說,我們就可以通過這個sMainLooper來進(jìn)行更新UI操作

      new Thread(new Runnable() {
        @Override
        public void run() {

            Log.e("qdx", "step 1 "+Thread.currentThread().getName());

            Handler handler=new Handler(getMainLooper());
            handler.post(new Runnable() {
                @Override
                public void run() {

                    //Do Ui method
                    Log.e("qdx", "step 2 "+Thread.currentThread().getName());
                }
            });

        }
    }).start();

View.post(Runnabler)老樣子,我們點(diǎn)入源碼
//View

/**
 * <p>Causes the Runnable to be added to the message queue.
 * The runnable will be run on the user interface thread.</p>
 *
 * @param action The Runnable that will be executed.
 *
 * @return Returns true if the Runnable was successfully placed in to the
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.
 *
 */
  public boolean post(Runnable action) {
     final AttachInfo attachInfo = mAttachInfo;
     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.
     getRunQueue().post(action);
     return true;
  }

     /**
      * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
      * handler can be used to pump events in the UI events queue.
      */
     final Handler mHandler;

居然也是Handler從中作祟,根據(jù)Handler的注釋,也可以清楚該Handler可以處理UI事件,也就是說它的Looper也是主線程的sMainLooper。這就是說我們常用的更新UI都是通過Handler實(shí)現(xiàn)的。

另外更新UI 也可以通過AsyncTask來實(shí)現(xiàn),難道這個AsyncTask的線程切換也是通過 Handler 嗎? 沒錯,也是通過Handler……


6.子線程中Toast,showDialog,的方法
可能有些人看到這個問題,就會想: 子線程本來就不可以更新UI的啊 而且上面也說了更新UI的方法.兄臺且慢,且聽我把話寫完

    new Thread(new Runnable() {
        @Override
        public void run() {

            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩潰無疑

        }
    }).start();

看到這個崩潰日志,是否有些疑惑,因?yàn)橐话闳绻泳€程不能更新UI控件是會報(bào)如下錯誤的(子線程不能更新UI)

所以子線程不能更新Toast的原因就和Handler有關(guān)了,據(jù)我們了解,每一個Handler都要有對應(yīng)的Looper對象,那么。 滿足你

  new Thread(new Runnable() {
        @Override
        public void run() {

            Looper.prepare();
            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
            Looper.loop();

        }
  }).start();

這樣便能在子線程中Toast,不是說子線程…? 老樣子,我們追根到底看一下Toast內(nèi)部執(zhí)行方式
//Toast

/**
 * Show the view for the specified duration.
 */
  public void show() {
   ......

     INotificationManager service = getService();//從SMgr中獲取名為notification的服務(wù)
     String pkg = mContext.getOpPackageName();
     TN tn = mTN;
     tn.mNextView = mNextView;

     try {
         service.enqueueToast(pkg, tn, mDuration);//enqueue? 難不成和Handler的隊(duì)列有關(guān)?
     } catch (RemoteException e) {
         // Empty
     }

在show方法中,我們看到Toast的show方法和普通UI 控件不太一樣,并且也是通過Binder進(jìn)程間通訊方法執(zhí)行Toast繪制。這其中的過程就不在多討論了,有興趣的可以在NotificationManagerService類中分析。

現(xiàn)在把目光放在TN 這個類上(難道越重要的類命名就越簡潔,如H類),通過TN 類,可以了解到它是Binder的本地類。在Toast的show方法中,將這個TN對象傳給NotificationManagerService就是為了通訊!并且我們也在TN中發(fā)現(xiàn)了它的show方法。

  private static class TN extends ITransientNotification.Stub {//Binder服務(wù)端的具體實(shí)現(xiàn)類

     /**
      * schedule handleShow into the right thread
      */
      @Override
      public void show(IBinder windowToken) {
         mHandler.obtainMessage(0, windowToken).sendToTarget();
      }

      final Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             IBinder token = (IBinder) msg.obj;
             handleShow(token);
         }
      };

  }

看完上面代碼,就知道子線程中Toast報(bào)錯的原因,因?yàn)樵赥N中使用Handler,所以需要創(chuàng)建Looper對象。 那么既然用Handler來發(fā)送消息,就可以在handleMessage中找到更新Toast的方法。 在handleMessage看到由handleShow處理。
//Toast的TN類

    public void handleShow(IBinder windowToken) {

            ......
            mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

            mParams.x = mX;
            mParams.y = mY;
            mParams.verticalMargin = mVerticalMargin;
            mParams.horizontalMargin = mHorizontalMargin;
            mParams.packageName = packageName;
            mParams.hideTimeoutMilliseconds = mDuration ==
                Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
            mParams.token = windowToken;
            if (mView.getParent() != null) {
                mWM.removeView(mView);
            }
            mWM.addView(mView, mParams);//使用WindowManager的addView方法
            trySendAccessibilityEvent();
        }
    }

看到這里就可以總結(jié)一下:

Toast本質(zhì)是通過window顯示和繪制的(操作的是window),而主線程不能更新UI 是因?yàn)閂iewRootImpl的checkThread方法在Activity維護(hù)的View樹的行為。 Toast中TN類使用Handler是為了用隊(duì)列和時(shí)間控制排隊(duì)顯示Toast,所以為了防止在創(chuàng)建TN時(shí)拋出異常,需要在子線程中使用Looper.prepare();和Looper.loop();(但是不建議這么做,因?yàn)樗鼤咕€程無法執(zhí)行結(jié)束,導(dǎo)致內(nèi)存泄露)

Dialog亦是如此。同時(shí)我們又多了一個知識點(diǎn)要去研究:Android 中Window是什么,它內(nèi)部有什么機(jī)制?


7.如何處理Handler 使用不當(dāng)導(dǎo)致的內(nèi)存泄露? 首先上文在子線程中為了節(jié)目效果,使用如下方式創(chuàng)建Looper

    Looper.prepare();
        .......
    Looper.loop();

實(shí)際上這是非常危險(xiǎn)的一種做法

在子線程中,如果手動為其創(chuàng)建Looper,那么在所有的事情完成以后應(yīng)該調(diào)用quit方法來終止消息循環(huán),否則這個子線程就會一直處于等待的狀態(tài),而如果退出Looper以后,這個線程就會立刻終止,因此建議不需要的時(shí)候終止Looper。(【 Looper.myLooper().quit();】)

那么,如果在Handler的handleMessage方法中(或者是run方法)處理消息,如果這個是一個延時(shí)消息,會一直保存在主線程的消息隊(duì)列里,并且會影響系統(tǒng)對Activity的回收,造成內(nèi)存泄露。

具體可以參考Handler內(nèi)存泄漏分析及解決

總結(jié)一下,解決Handler內(nèi)存泄露主要2點(diǎn)

1 有延時(shí)消息,要在Activity銷毀的時(shí)候移除Messages
2 匿名內(nèi)部類導(dǎo)致的泄露改為匿名靜態(tài)內(nèi)部類,并且對上下文或者Activity使用弱引用。

總結(jié)
想不到Handler居然可以騰出這么多浪花,與此同時(shí)感謝前輩的摸索。

另外Handler還有許多不為人知的秘密,等待大家探索,下面我再簡單的介紹兩分鐘

HandlerThread
IdleHandler

HandlerThread

HandlerThread繼承Thread,它是一種可以使用Handler的Thread,它的實(shí)現(xiàn)也很簡單,在run方法中也是通過Looper.prepare()來創(chuàng)建消息隊(duì)列,并通過Looper.loop()來開啟消息循環(huán)(與我們手動創(chuàng)建方法基本一致),這樣在實(shí)際的使用中就允許在HandlerThread中創(chuàng)建Handler了

由于HandlerThread的run方法是一個無限循環(huán),因此當(dāng)不需要使用的時(shí)候通過quit或者quitSafely方法來終止線程的執(zhí)行

HandlerThread的本質(zhì)也是線程,所以切記關(guān)聯(lián)的Handler中處理消息的handleMessage為子線程。

IdleHandler

/**
 * Callback interface for discovering when a thread is going to block
 * waiting for more messages.
 */
  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();
 }

根據(jù)注釋可以了解到,這個接口方法是在消息隊(duì)列全部處理完成后或者是在阻塞的過程中等待更多的消息的時(shí)候調(diào)用的,返回值false表示只回調(diào)一次,true表示可以接收多次回調(diào)。
具體使用如下代碼

    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {

            return false;
        }
    });

另外提供一個小技巧:在HandlerThread中獲取Looper的MessageQueue方法之反射。

因?yàn)長ooper.myQueue()如果在主線程調(diào)用就會使用主線程looper 使用handlerThread.getLooper().getQueue()最低版本需要23 //HandlerThread中獲取MessageQueue

  Field field = Looper.class.getDeclaredField("mQueue");
        field.setAccessible(true);
        MessageQueue queue = (MessageQueue) field.get(handlerThread.getLooper());

那么Android的消息循環(huán)機(jī)制是通過Handler,是否可以通過IdleHandler來判斷Activity的加載和繪制情況(measure,layout,draw等)呢?并且IdleHandler是否也隱藏著不為人知的特殊功能?


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI