不想被面試官虐?Android知識彙總,你必須知道的Handler八大問題!

yilian發表於2020-04-08

前言

handler機制幾乎是 Android面試時必問的問題,雖然看過很多次 handler原始碼,但是有些面試官問的問題卻不一定能夠回答出來,趁著機會總結一下面試中所覆蓋的 handler知識點。

1、講講 Handler 的底層實現原理?

下面的這幅圖很完整的表現了整個 handler機制。

要理解handler的實現原理,其實最重要的是理解 Looper的實現原理, Looper才是實現 handler機制的核心。任何一個 handler在使用 sendMessage或者 post時候,都是先構造一個 Message,並把自己放到 message中,然後把 Message放到對應的 LooperMessageQueueLooper透過控制 MessageQueue來獲取 message執行其中的 handler或者 runnable。 要在當前執行緒中執行 handler指定操作,必須要先看當前執行緒中有沒有 looper,如果有 looperhandler就會透過 sendMessage,或者 post先構造一個 message,然後把 message放到當前執行緒的 looper中, looper會在當前執行緒中迴圈取出 message執行,如果沒有 looper,就要透過 looper.prepare()方法在當前執行緒中構建一個 looper,然後主動執行 looper.loop()來實現迴圈。

梳理一下其實最簡單的就下面四條:

1、每一個執行緒中最多隻有一個 Looper,透過 ThreadLocal來儲存, Looper中有 Message佇列,儲存 handler並且執行 handler傳送的 message

2、線上程中透過 Looper.prepare()來建立 Looper,並且透過 ThreadLocal來儲存 Looper,每一個執行緒中只能呼叫一次 Looper.prepare(),也就是說一個執行緒中最多隻有一個 Looper,這樣可以保證執行緒中 Looper的唯一性。

3、 handler中執行 sendMessage或者 post操作,這些操作執行的執行緒是 handlerLooper所在的執行緒,和 handler在哪裡建立沒關係,和 Handler中的 Looper在那建立有關係。

4、一個執行緒中只能有一個 Looper,但是一個 Looper可以對應多個 handler,在同一個 Looper中的訊息都在同一條執行緒中執行。

2、Handler機制,sendMessage和post(Runnable)的區別?

要看 sendMessagepost區別,需要從原始碼來看,下面是幾種使用 handler的方式,先看下這些方式,然後再從原始碼分析有什麼區別。  例1、 主執行緒中使用 handler

  //主執行緒
          Handler mHandler = new Handler(new Handler.Callback() {
              @Override
              public boolean handleMessage(@NonNull Message msg) {
                  if (msg.what == 1) {
                      //doing something
                  }
                  return false;
              }
          });
          Message msg = Message.obtain();
          msg.what = 1;
          mHandler.sendMessage(msg);

上面是在主執行緒中使用 handler,因為在 Android中系統已經在主執行緒中生成了 Looper,所以不需要自己來進行 looper的生成。如果上面的程式碼在子執行緒中執行,就會報

  Can't create handler inside thread " + Thread.currentThread()
                          + " that has not called Looper.prepare()

如果想著子執行緒中處理 handler的操作,就要必須要自己生成 Looper了。

例2 、子執行緒中使用handler

          Thread thread=new Thread(new Runnable() {
              @Override
              public void run() {
                  Looper.prepare();
                  Handler handler=new Handler();
                  handler.post(new Runnable() {
                      @Override
                      public void run() {
                      }
                  });
                  Looper.loop();
              }
          });

上面在 Thread中使用 handler,先執行 Looper.prepare方法,來在當前執行緒中生成一個 Looper物件並儲存在當前執行緒的 ThreadLocal中。 看下 Looper.prepare()中的原始碼:

  //prepare
      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
      private Looper(boolean quitAllowed) {
          mQueue = new MessageQueue(quitAllowed);
          mThread = Thread.currentThread();
      }

可以看到 prepare方法中會先從 sThreadLocal中取如果之前已經生成過 Looper就會報錯,否則就會生成一個新的 Looper並且儲存線上程的 ThreadLocal中,這樣可以確保每一個執行緒中只能有一個唯一的 Looper

另外:由於 Looper中擁有當前執行緒的引用,所以有時候可以用 Looper的這種特點來判斷當前執行緒是不是主執行緒。

      @RequiresApi(api = Build.VERSION_CODES.KITKAT)
      boolean isMainThread() {
          return Objects.requireNonNull(Looper.myLooper()).getThread() == 
  Looper.getMainLooper().getThread();
      }

sendMessage vs post

先來看看 sendMessage的程式碼呼叫鏈:

enqueueMessage原始碼如下:

      private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
              long uptimeMillis) {
          msg.target = this;
          msg.workSourceUid = ThreadLocalWorkSource.getUid();
          return queue.enqueueMessage(msg, uptimeMillis);
      }

enqueueMessage的程式碼處理很簡單, msg.target = this;就是把當前的 handler物件給 message.target。然後再講 message進入到佇列中。

post程式碼呼叫鏈:

呼叫 post時候會先呼叫 getPostMessage生成一個 Message,後面和 sendMessage的流程一樣。下面看下 getPostMessage方法的原始碼:

      private static Message getPostMessage(Runnable r) {
          Message m = Message.obtain();
          m.callback = r;
          return m;
      }

可以看到 getPostMessage中會先生成一個 Messgae,並且把 runnable賦值給 messagecallback.訊息都放到 MessageQueue中後,看下 Looper是如何處理的。

      for (;;) {
          Message msg = queue.next(); // might block
          if (msg == null) {
              return;
          }
          msg.target.dispatchMessage(msg);
      }

Looper中會遍歷 message列表,當 message不為 null時呼叫 msg.target.dispatchMessage(msg)方法。看下 message結構:

也就是說 msg.target.dispatchMessage方法其實就是呼叫的 Handler中的dispatchMessage方法,下面看下 dispatchMessage方法的原始碼:

      public void dispatchMessage(@NonNull Message msg) {
          if (msg.callback != null) {
              handleCallback(msg);
          } else {
              if (mCallback != null) {
                  if (mCallback.handleMessage(msg)) {
                      return;
                  }
              }
              handleMessage(msg);
          }
      }
  //
   private static void handleCallback(Message message) {
          message.callback.run();
      }

因為呼叫 post方法時生成的 message.callback=runnable,所以 dispatchMessage方法中會直接呼叫  message.callback.run();也就是說直接執行 post中的 runnable方法。 而 sendMessage中如果 mCallback不為 null就會呼叫 mCallback.handleMessage(msg)方法,否則會直接呼叫 handleMessage方法。

總結  post方法和 handleMessage方法的不同在於, postrunnable會直接在 callback中呼叫 run方法執行,而 sendMessage方法要使用者主動重寫 mCallback或者 handleMessage方法來處理。

3、Looper會一直消耗系統資源嗎?

首先給出結論, Looper不會一直消耗系統資源,當 LooperMessageQueue中沒有訊息時,或者定時訊息沒到執行時間時,當前持有 Looper的執行緒就會進入阻塞狀態。

下面看下 looper所在的執行緒是如何進入阻塞狀態的。 looper阻塞肯定跟訊息出隊有關,因此看下訊息出隊的程式碼。

訊息出隊

     Message next() {
          // Return here if the message loop has already quit and been disposed.
          // This can happen if the application tries to restart a looper after quit
          // which is not supported.
          final long ptr = mPtr;
          if (ptr == 0) {
              return null;
          }
          int nextPollTimeoutMillis = 0;
          for (;;) {
              if (nextPollTimeoutMillis != 0) {
                  Binder.flushPendingCommands();
              }
              nativePollOnce(ptr, nextPollTimeoutMillis);
              // While calling an idle handler, a new message could have been delivered
              // so go back and look again for a pending message without waiting.
           	  if(hasNoMessage)
           	  {
           	  nextPollTimeoutMillis =-1;
           	  }
          }
      }

上面的訊息出隊方法被簡寫了,主要看下面這段,沒有訊息的時候 nextPollTimeoutMillis=-1

 	if(hasNoMessage)
           	{
           	nextPollTimeoutMillis =-1;
           	}

看for迴圈裡面這個欄位所其的作用:

   if (nextPollTimeoutMillis != 0) {
                  Binder.flushPendingCommands();
              }
    nativePollOnce(ptr, nextPollTimeoutMillis);

Binder.flushPendingCommands();這個方法的作用可以看原始碼裡面給出的解釋:

      /**
       * Flush any Binder commands pending in the current thread to the kernel
       * driver.  This can be
       * useful to call before performing an operation that may block for a long
       * time, to ensure that any pending object references have been released
       * in order to prevent the process from holding on to objects longer than
       * it needs to.
       */

也就是說在使用者執行緒要進入阻塞之前跟核心執行緒傳送訊息,防止使用者執行緒長時間的持有某個物件。再看看下面這個方法: nativePollOnce(ptr, nextPollTimeoutMillis);nextPollingTimeOutMillis=-1時,這個 native方法會阻塞當前執行緒,執行緒阻塞後,等下次有訊息入隊才會重新進入可執行狀態,所以 Looper並不會一直死迴圈消耗執行記憶體,對佇列中的顏色訊息還沒到時間時也會阻塞當前執行緒,但是會有一個阻塞時間也就是 nextPollingTimeOutMillis>0的時間。

當訊息佇列中沒有訊息的時候looper肯定是被訊息入隊喚醒的。

訊息入隊

  boolean enqueueMessage(Message msg, long when) {
          if (msg.target == null) {
              throw new IllegalArgumentException("Message must have a target.");
          }
          if (msg.isInUse()) {
              throw new IllegalStateException(msg + " This message is already in use.");
          }
          synchronized (this) {
              if (mQuitting) {
                  IllegalStateException e = new IllegalStateException(
                          msg.target + " sending message to a Handler on a dead thread");
                  Log.w(TAG, e.getMessage(), e);
                  msg.recycle();
                  return false;
              }
              msg.markInUse();
              msg.when = when;
              Message p = mMessages;
              boolean needWake;
              if (p == null || when == 0 || when < p.when) {
                  // New head, wake up the event queue if blocked.
                  msg.next = p;
                  mMessages = msg;
                  needWake = mBlocked;
              } else {
                  // Inserted within the middle of the queue.  Usually we don't have to wake
                  // up the event queue unless there is a barrier at the head of the queue
                  // and the message is the earliest asynchronous message in the queue.
                  needWake = mBlocked && p.target == null && msg.isAsynchronous();
                  Message prev;
                  for (;;) {
                      prev = p;
                      p = p.next;
                      if (p == null || when < p.when) {
                          break;
                      }
                      if (needWake && p.isAsynchronous()) {
                          needWake = false;
                      }
                  }
                  msg.next = p; // invariant: p == prev.next
                  prev.next = msg;
              }
              // We can assume mPtr != 0 because mQuitting is false.
              if (needWake) {
                  nativeWake(mPtr);
              }
          }
          return true;
      }

上面可以看到訊息入隊之後會有一個

  if (needWake) {
                nativeWake(mPtr);
            }

方法,呼叫這個方法就可以喚醒執行緒了。另外訊息入隊的時候是根據訊息的 delay時間來在連結串列中排序的, delay時間長的排在後面,時間短的排在前面。如果時間相同那麼按插入時間先後來排,插入時間早的在前面,插入時間晚的在後面。

4、android的Handle機制,Looper關係,主執行緒的Handler是怎麼判斷收到的訊息是哪個Handler傳來的?

Looper是如何判斷 Message是從哪個 handler傳來的呢?其實很簡單,在 1中分析過, handlersendMessage的時候會構建一個 Message物件,並且把自己放在 Messagetarget裡面,這樣的話 Looper就可以根據 Message中的 target來判斷當前的訊息是哪個 handler傳來的。

5、Handler機制流程、Looper中延遲訊息誰來喚醒Looper?

從3中知道在訊息出隊的 for迴圈佇列中會呼叫到下面的方法。

  nativePollOnce(ptr, nextPollTimeoutMillis);

如果是延時訊息,會在被阻塞 nextPollTimeoutMillis時間後被叫醒, nextPollTimeoutMillis就是訊息要執行的時間和當前的時間差。

6、Handler是如何引起記憶體洩漏的?如何解決?

在子執行緒中,如果手動為其建立 Looper,那麼在所有的事情完成以後應該呼叫 quit方法來終止訊息迴圈,否則這個子執行緒就會一直處於等待的狀態,而如果退出 Looper以後,這個執行緒就會立刻終止,因此建議不需要的時候終止 Looper

  Looper.myLooper().quit()

那麼,如果在 HandlerhandleMessage方法中(或者是run方法)處理訊息,如果這個是一個延時訊息,會一直儲存在主執行緒的訊息佇列裡,並且會影響系統對 Activity的回收,造成記憶體洩露。

具體可以參考 Handler記憶體洩漏分析及解決

總結一下,解決 Handler記憶體洩露主要2點

1 、有延時訊息,要在 Activity銷燬的時候移除 Messages

2、 匿名內部類導致的洩露改為匿名靜態內部類,並且對上下文或者 Activity使用弱引用。

7、handler機制中如何確保Looper的唯一性?

Looper是儲存線上程的 ThreadLocal裡面的,使用 Handler的時候要呼叫 Looper.prepare()來建立一個 Looper並放在當前的執行緒的 ThreadLocal裡面。

      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));
      }

可以看到,如果多次呼叫 prepare的時候就會報 Only one Looper may be created per thread,所以這樣就可以保證一個執行緒中只有唯一的一個 Looper

8、Handler 是如何能夠執行緒切換,傳送Message的?

handler的執行跟建立 handler的執行緒無關,跟建立 looper的執行緒相關,加入在子執行緒中建立一個 Handler,但是 Handler相關的 Looper是主執行緒的,這樣,如果 handler執行 post一個 runnable,或者 sendMessage,最終的 handle Message都是在主執行緒中執行的。

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler handler=new Handler(getMainLooper());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,"hello,world",Toast.LENGTH_LONG).show();
                    }
                });
                Looper.loop();
            }
        });
        thread.start();

心裡話

不論是什麼樣的大小面試,要想不被面試官虐的不要不要的,只有刷爆面試題題做好全面的準備,除了這個還需要在平時把自己的基礎打紮實,這樣不論面試官怎麼樣一個知識點裡往死裡鑿,你也能應付如流啊~

如果文字版的 handle彙總還有些不懂得話,我給大家準備了三星架構師講解的 2小時影片, Handler面試需要的所有知識都在這,可以好好學一學!

當然,面試的時候肯定不會只問 handle,還有其他內容,附上大廠面試題整理的合集,這是我的學習筆記,進行了分類,循序漸進,由基礎到深入,由易到簡。將內容整理成了五個章節

計算機基礎面試題、資料結構和演算法面試題、 Java面試題、 Android面試題、其他擴充套件面試題、非技術面試題總共五個章節354頁。

還有一份 Android學習 PDF大全,這份 Android學習 PDF大全真的包含了方方面面了

內含 Java基礎知識點、 Android基礎、 Android進階延伸、演算法合集等等


位元組跳動真題解析、  Android 知識大全 PDF、簡歷模板可以關注我看個人簡介或者 私信我免費獲取

面試時 HR也是不可以忽略的環節,我們經常也會遇到很多關於簡歷製作,職業困惑、 HR經典面試問題回答等有關面試的問題。

有全套簡歷製作、春招困惑、 HR面試等問題解析參考建議。


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

相關文章