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

前言

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,還有其他內容,附上大廠面試題整理的合集,這是我的學習筆記,進行了分類,循序漸進,由基礎到深入,由易到簡。將內容整理成了五個章節

學習PDF大全+字節跳動真題+簡歷模板

計算機基礎面試題、數據結構和算法面試題、Java面試題、Android面試題、其他擴展面試題、非技術面試題總共五個章節354頁。

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

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


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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章