1. 程式人生 > >面試官:“看你簡歷上寫熟悉 Handler 機制,那聊聊 IdleHandler 吧?”

面試官:“看你簡歷上寫熟悉 Handler 機制,那聊聊 IdleHandler 吧?”

一. 序

Handler 機制算是 Android 基本功,面試常客。但現在面試,多數已經不會直接讓你講講 Handler 的機制,Looper 是如何迴圈的,MessageQueue 是如何管理 Message 等,而是基於場景去提問,看看你對 Handler 機制的掌握是否紮實。

本文就來聊聊 Handler 中的 IdleHandler,這個我們比較少用的功能。它能幹什麼?怎麼使用?有什麼合適的使用場景?哪些不是合適的使用場景?在 Android Framework 中有哪些地方用到了它?

二. IdleHandler

2.1 簡單說說 Handler 機制

在說 IdleHandler 之前,先簡單瞭解一下 Handler 機制。

Handler 是標準的事件驅動模型,存在一個訊息佇列 MessageQueue,它是一個基於訊息觸發時間的優先順序佇列,還有一個基於此訊息佇列的事件迴圈 Looper,Looper 通過迴圈,不斷的從 MessageQueue 中取出待處理的 Message,再交由對應的事件處理器 Handler/callback 來處理。

其中 MessageQueue 被 Looper 管理,Looper 在構造時同步會建立 MessageQueue,並利用 ThreadLocal 這種 TLS,將其與當前執行緒繫結。而 App 的主執行緒在啟動時,已經構造並準備好主執行緒的 Looper 物件,開發者只需要直接使用即可。

Handler 類中封裝了大部分「Handler 機制」對外的操作介面,可以通過它的 send/post 相關的方法,向訊息佇列 MessageQueue 中插入一條 Message。在 Looper 迴圈中,又會不斷的從 MessageQueue 取出下一條待處理的 Message 進行處理。

IdleHandler 使用相關的邏輯,就在 MessageQueue 取訊息的 next() 方法中。

2.2 IdleHandler 是什麼?怎麼用?

IdleHandler 說白了,就是 Handler 機制提供的一種,可以在 Looper 事件迴圈的過程中,當出現空閒的時候,允許我們執行任務的一種機制。

IdleHandler 被定義在 MessageQueue 中,它是一個介面。

// MessageQueue.java
public static interface IdleHandler {
  boolean queueIdle();
}

可以看到,定義時需要實現其 queueIdle() 方法。同時返回值為 true 表示是一個持久的 IdleHandler 會重複使用,返回 false 表示是一個一次性的 IdleHandler。

既然 IdleHandler 被定義在 MessageQueue 中,使用它也需要藉助 MessageQueue。在 MessageQueue 中定義了對應的 add 和 remove 方法。

// MessageQueue.java
public void addIdleHandler(@NonNull IdleHandler handler) {
    // ...
  synchronized (this) {
    mIdleHandlers.add(handler);
  }
}
public void removeIdleHandler(@NonNull IdleHandler handler) {
  synchronized (this) {
    mIdleHandlers.remove(handler);
  }
}

可以看到 add 或 remove 其實操作的都是 mIdleHandlers,它的型別是一個 ArrayList。

既然 IdleHandler 主要是在 MessageQueue 出現空閒的時候被執行,那麼何時出現空閒?

MessageQueue 是一個基於訊息觸發時間的優先順序佇列,所以隊列出現空閒存在兩種場景。

  1. MessageQueue 為空,沒有訊息;
  2. MessageQueue 中最近需要處理的訊息,是一個延遲訊息(when>currentTime),需要滯後執行;

這兩個場景,都會嘗試執行 IdleHandler。

處理 IdleHandler 的場景,就在 Message.next() 這個獲取訊息佇列下一個待執行訊息的方法中,我們跟一下具體的邏輯。

Message next() {
    // ...
  int pendingIdleHandlerCount = -1; 
  int nextPollTimeoutMillis = 0;
  for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
      // ...
      if (msg != null) {
        if (now < msg.when) {
          // 計算休眠的時間
          nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
        } else {
          // Other code
          // 找到訊息處理後返回
          return msg;
        }
      } else {
        // 沒有更多的訊息
        nextPollTimeoutMillis = -1;
      }
      
      if (pendingIdleHandlerCount < 0
          && (mMessages == null || now < mMessages.when)) {
        pendingIdleHandlerCount = mIdleHandlers.size();
      }
      if (pendingIdleHandlerCount <= 0) {
        mBlocked = true;
        continue;
      }

      if (mPendingIdleHandlers == null) {
        mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
      }
      mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    for (int i = 0; i < pendingIdleHandlerCount; i++) {
      final IdleHandler idler = mPendingIdleHandlers[i];
      mPendingIdleHandlers[i] = null; 

      boolean keep = false;
      try {
        keep = idler.queueIdle();
      } catch (Throwable t) {
        Log.wtf(TAG, "IdleHandler threw exception", t);
      }

      if (!keep) {
        synchronized (this) {
          mIdleHandlers.remove(idler);
        }
      }
    }

    pendingIdleHandlerCount = 0;
    nextPollTimeoutMillis = 0;
  }
}

我們先解釋一下 next() 中關於 IdleHandler 執行的主邏輯:

  1. 準備執行 IdleHandler 時,說明當前待執行的訊息為 null,或者這條訊息的執行時間未到;
  2. pendingIdleHandlerCount < 0 時,根據 mIdleHandlers.size() 賦值給 pendingIdleHandlerCount,它是後期迴圈的基礎;
  3. mIdleHandlers 中的 IdleHandler 拷貝到 mPendingIdleHandlers 陣列中,這個陣列是臨時的,之後進入 for 迴圈;
  4. 迴圈中從陣列中取出 IdleHandler,並呼叫其 queueIdle() 記錄返回值存到 keep 中;
  5. keep 為 false 時,從 mIdleHandler 中移除當前迴圈的 IdleHandler,反之則保留;

可以看到 IdleHandler 機制中,最核心的就是在 next() 中,當佇列空閒的時候,迴圈 mIdleHandler 中記錄的 IdleHandler 物件,如果其 queueIdle() 返回值為 false 時,將其從 mIdleHander 中移除。

需要注意的是,對 mIdleHandler 這個 List 的所有操作,都通過 synchronized 來保證執行緒安全,這一點無需擔心。

2.3 IdleHander 是如何保證不進入死迴圈的?

當佇列空閒時,會迴圈執行一遍 mIdleHandlers 陣列並執行 IdleHandler.queueIdle() 方法。而如果陣列中有一些 IdleHander 的 queueIdle() 返回了 true,則會保留在 mIdleHanders 陣列中,下次依然會再執行一遍。

注意現在程式碼邏輯還在 MessageQueue.next() 的迴圈中,在這個場景下 IdleHandler 機制是如何保證不會進入死迴圈的?

有些文章會說 IdleHandler 不會死迴圈,是因為下次迴圈呼叫了 nativePollOnce() 藉助 epoll 機制進入休眠狀態,下次有新訊息入隊的時候會重新喚醒,但這是不對的。

注意看前面 next() 中的程式碼,在方法的末尾會重置 pendingIdleHandlerCount 和 nextPollTimeoutMillis。

Message next() {
    // ...
  int pendingIdleHandlerCount = -1; 
  int nextPollTimeoutMillis = 0;
  for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);
    // ...
    // 迴圈執行 mIdleHandlers
    // ...
    pendingIdleHandlerCount = 0;
    nextPollTimeoutMillis = 0;
  }
}

nextPollTimeoutMillis 決定了下次進入 nativePollOnce() 超時的時間,它傳遞 0 的時候等於不會進入休眠,所以說 natievPollOnce() 進入休眠所以不會死迴圈是不對的。

這很好理解,畢竟 IdleHandler.queueIdle() 執行在主執行緒,它執行的時間是不可控的,那麼 MessageQueue 中的訊息情況可能會變化,所以需要再處理一遍。

實際不會死迴圈的關鍵是在於 pendingIdleHandlerCount,我們看看下面的程式碼。

Message next() {
    // ...
  // Step 1
  int pendingIdleHandlerCount = -1; 
  int nextPollTimeoutMillis = 0;
  for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
      // ...
      // Step 2
      if (pendingIdleHandlerCount < 0
          && (mMessages == null || now < mMessages.when)) {
        pendingIdleHandlerCount = mIdleHandlers.size();
      }
        // Step 3
      if (pendingIdleHandlerCount <= 0) {
        mBlocked = true;
        continue;
      }
      // ...
    }
        // Step 4
    pendingIdleHandlerCount = 0;
    nextPollTimeoutMillis = 0;
  }
}

我們梳理一下:

  • Step 1,迴圈開始前,pendingIdleHandlerCount 的初始值為 -1;
  • Step 2,在 pendingIdleHandlerCount<0 時,才會通過 mIdleHandlers.size() 賦值。也就是說只有第一次迴圈才會改變 pendingIdleHandlerCount 的值;
  • Step 3,如果 pendingIdleHandlerCount<=0 時,則迴圈 continus;
  • Step 4,重置 pendingIdleHandlerCount 為 0;

在第二次迴圈時,pendingIdleHandlerCount 等於 0,在 Step 2 不會改變它的值,那麼在 Step 3 中會直接 continus 繼續下一次迴圈,此時沒有機會修改 nextPollTimeoutMillis

那麼 nextPollTimeoutMillis 有兩種可能:-1 或者下次喚醒的等待間隔時間,在執行到 nativePollOnce() 時就會進入休眠,等待再次被喚醒。

下次喚醒時,mMessage 必然會有一個待執行的 Message,則 MessageQueue.next() 返回到 Looper.loop() 的迴圈中,分發處理這個 Message,之後又是一輪新的 next() 中去迴圈。

2.4 framework 中如何使用 IdleHander?

到這裡基本上就講清楚 IdleHandler 如何使用以及一些細節,接下來我們來看看,在系統中,有哪些地方會用到 IdleHandler 機制。

在 AS 中搜索一下 IdleHandler。

簡單解釋一下:

  1. ActivityThread.Idler 在 ActivityThread.handleResumeActivity() 中呼叫。
  2. ActivityThread.GcIdler 是在記憶體不足時,強行 GC;
  3. Instrumentation.ActivityGoing 在 Activity onCreate() 執行前新增;
  4. Instrumentation.Idler 呼叫的時機就比較多了,是鍵盤相關的呼叫;
  5. TextToSpeechService.SynthThread 是在 TTS 合成完成之後傳送廣播;

有興趣可以自己追一下原始碼,這些都是使用的場景,具體用 IdleHander 幹什麼,還是要看業務。

三.一些面試問題

到這裡我們就講清楚 IdleHandler 幹什麼?怎麼用?有什麼問題?以及使用中一些原理的講解。

下面準備一些基本的問題,供大家理解。

Q:IdleHandler 有什麼用?

  1. IdleHandler 是 Handler 提供的一種在訊息佇列空閒時,執行任務的時機;
  2. 當 MessageQueue 當前沒有立即需要處理的訊息時,會執行 IdleHandler;

Q:MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成對使用?

  1. 不是必須;
  2. IdleHandler.queueIdle() 的返回值,可以移除加入 MessageQueue 的 IdleHandler;

Q:當 mIdleHanders 一直不為空時,為什麼不會進入死迴圈?

  1. 只有在 pendingIdleHandlerCount 為 -1 時,才會嘗試執行 mIdleHander;
  2. pendingIdlehanderCount 在 next() 中初始時為 -1,執行一遍後被置為 0,所以不會重複執行;

Q:是否可以將一些不重要的啟動服務,搬移到 IdleHandler 中去處理?

  1. 不建議;
  2. IdleHandler 的處理時機不可控,如果 MessageQueue 一直有待處理的訊息,那麼 IdleHander 的執行時機會很靠後;

Q:IdleHandler 的 queueIdle() 執行在那個執行緒?

  1. 陷進問題,queueIdle() 執行的執行緒,只和當前 MessageQueue 的 Looper 所在的執行緒有關;
  2. 子執行緒一樣可以構造 Looper,並新增 IdleHandler;

三. 小結時刻

到這裡就把 IdleHandler 的使用和原理說清除了。

IdleHandler 是 Handler 提供的一種在訊息佇列空閒時,執行任務的時機。但它執行的時機依賴訊息佇列的情況,那麼如果 MessageQueue 一直有待執行的訊息時,IdleHandler 就一直得不到執行,也就是它的執行時機是不可控的,不適合執行一些對時機要求比較高的任務。

本文就到這裡,對你有幫助嗎?有任何問題歡迎留言。覺得有幫助別忘了轉發、點好看,謝謝!


推薦閱讀:

  • TCP 三次握手四次揮手意外情況處理
  • 作為位元組跳動面試官,有些話我不得不說
  • 被開發者拋棄的 Executors,錯在哪兒?

公眾號後臺回覆成長『成長』,將會得到我準備的學習資料。

相關推薦

面試簡歷熟悉 Handler 機制聊聊 IdleHandler ?”

一. 序 Handler 機制算是 Android 基本功,面試常客。但現在面試,多數已經不會直接讓你講講 Handler 的機制,Looper 是如何迴圈的,MessageQueue 是如何管理 Message 等,而是基於場景去提問,看看你對 Handler 機制的掌握是否紮實。 本文就來聊聊 H

面試說說對css效率的理解

大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。 內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。 大家的支援是我創作的動力。 選擇器的優先順序 眾所周知,選擇器是有權重的,優先順序從低到高,如下所示: 型別選擇器(例如,h1)和偽元素(例如,::before) 類選擇器

不會 Explain執行計劃簡歷熟悉 SQL優化

昨天中午在食堂,和部門的技術大牛們坐在一桌吃飯,作為一個卑微技術渣仔默默的吃著飯,聽大佬們高談闊論,研究各種高階技術,我TM也想說話可實在插不上嘴。 聊著聊著突然說到他上午面試了一個工作6年的程式設計師,表情挺複雜,他說:我看他簡歷寫著熟悉`SQL`語句調優,就問了下 `Explain` 執行計劃怎麼看?結

【兩萬字】面試聽說很懂集合原始碼接我二十道問題!

問題一:看到這個圖,你會想到什麼? (PS:截圖自《程式設計思想》) 答: 這個圖由Map指向Collection的Produces並不是說Map是Collection的一個子類(子介面),這裡的意思是指Map的KeySet獲取到的一個檢視是Collection的子介面。 我們可以看到集合有兩個基本介面:

面試"準備用HashMap存1w條資料構造時傳10000還會觸發擴容嗎?"

// 預計存入 1w 條資料,初始化賦值 10000,避免 resize。 HashMap<String,String> map = new HashMap<>(10000) // for (int i = 0; i < 10000; i++) Java 集合的擴容

Java 記憶體模型都不會就敢在簡歷熟悉併發程式設計嗎

# 從 PC 記憶體架構到 Java 記憶體模型 > 你知道 Java 記憶體模型 JMM 嗎?那你知道它的三大特性嗎? > > Java 是如何解決指令重排問題的? > > 既然CPU有快取一致性協議(MESI),為什麼 JMM 還需要volatile關鍵字? > 帶

墨菲定律覺得一個地方可能有bug麽這個地方就會有bug----順帶了解下Tomcat少有人註意的localhost.log

col 紅色 exceptio str host .cn trac 線程同步 html 一、問題概述 題目有點長,但應該值得後端java們了解下有點小坑的localhost.log,讓我長話短說。 博主是搞java後端的。後臺是很簡單的spring mvc + spri

面試過Redis資料結構底層實現嗎?

面試中,redis也是很受面試官親睞的一部分。我向在這裡講的是redis的底層資料結構,而不是你理解的五大資料結構。你有沒有想過redis底層是怎樣的資料結構呢,他們和我們java中的HashMap、List、等使用的資料結構有什麼區別呢。 1. 字串處理(string) 我們都知道redis是用C語言寫

面試簡歷就是包裝過了!

在網際網路極速膨脹的社會背景下,各行各業湧入網際網路的IT民工日益增大。   早在2016年,我司釋出了Java、Ios工程師的招聘資訊,就Java工程師單個崗位而言,日收簡歷近200份,Ios日收簡歷近一千份。   沒錯,這就是當年培訓機構對Ios工程師這個崗位發起的市場討伐。而隨著近幾

面試小夥子聽說過ThreadLocal原始碼?(萬字圖文深度解析ThreadLocal)

前言 (高清無損原圖.pdf關注公眾號後回覆 ThreadLocal 獲取,文末有公眾號連結) 前幾天寫了一篇AQS相關的文章:我畫了35張圖就是為了讓你深入 AQS,反響不錯,還上了部落格園首頁編輯推薦,有生之年系列呀,哈哈。 這次趁熱打鐵再寫一篇ThreadLocal的文章,同樣是深入原理,圖文並

面試一個認為比較“完美”的單例

單例模式是保證一個類的例項有且只有一個,在需要控制資源(如資料庫連線池),或資源共享(如有狀態的工具類)的場景中比較適用。如果讓我們寫一個單例實現,估計絕大部分人都覺得自己沒問題,但如果需要實現一個比較完美的單例,可能並沒有你想象中簡單。本文以主人公小雨的一次面試為背景,循序漸進地討論如何實現一個較為“完美”

面試談談類載入器有沒有過類載入器的原始碼

一、類載入 1.1、在java程式碼中,型別的載入,連線,初始化過程都是在程式執行期間完成的。 圖示: 1.2、型別的載入——這裡的型別是指的什麼? 答:型別就是指的我們Java原始碼通過編譯後的class檔案。 1.3、型別的來源有哪些? (1)本地磁碟 (2)網路下載,class檔案 (3)war,ja

面試說說互斥鎖、自旋鎖、讀鎖、悲觀鎖、樂觀鎖的應用場景

前言 生活中用到的鎖,用途都比較簡單粗暴,上鎖基本是為了防止外人進來、電動車被偷等等。 但生活中也不是沒有 BUG 的,比如加鎖的電動車在「廣西 - 竊·格瓦拉」面前,鎖就是形同虛設,只要他願意,他就可以輕輕鬆鬆地把你電動車給「順走」,不然打工怎麼會是他這輩子不可能的事情呢?牛逼之人,必有牛

面試如果讓個分散式配置中心就問慌不慌

## 前言 一位讀者朋友跟我反饋,能不能寫一篇比較全的配置中心的文章。自己最近在面試過程中有被面試官問:**如何設計一個配置中心?** 這個話題,由於自己在工作中也沒實際使用過配置中心,所以對於如何去設計是完全沒有概念的。 今天就給大家寫一篇去配置中心需要考慮的點,我也不是什麼配置中心開源專案的參與者,所

面試快排會嗎?

快排可以說是一道必知的常見面試題,同時也有多種實現方式。在這篇文章中,我使用的是隨機三路快排。 之所以使用隨機快速排序而不是普通的快排。是因為前者可以使得數列有序的概率降低,從而使隨機快速排序平均速度是比快速排序要快的。具體的兩者的效能差別可以看下這篇文章: blog.csdn.net/haelang/a

面試"我為什麼要聘用"

關於面試,面試官也是人,人的想法可能千奇百怪,雖然其中有一定的規律可以循,但是不乏意料之外的問題。老師的工作是為學生開啟一扇門,讓學生自己走進去,不能使勁把分們拉進來,因為走進來必須是學生自己的事情。講得再多,沒有體悟也是按圖索驥。面試的套路可以說上“兵無常勢,水無常形”能因

大專程式設計師面試簡歷當場被撕面試大專生我們不收

每個人都有過求職的經歷,這個過程也是很痛苦的,因為求職不可能會讓你一帆風順的,經常會使你碰壁。即便是被拒絕了之後,想想自己的哪些方面的不足,在下一次面試的時候做好準備,也沒什麼抱怨的。但是總有一些面試官的做法卻讓人感到憤怒。 就有一名程式設計師發帖講述了自己最近的一次面試被面試官撕掉簡歷的

3年後想在自己的簡歷什麼?

個人感悟             這裡的簡歷,可以理解為更改的層面上,比如個人介紹上。無論你是學生、打工者、boss還是自由職業者,始終都會有場合需要個人介紹。        

怎麼回答面試對Spring的理解

spring呢,是pivotal公司維護的一系列開源工具的總稱,最為人所知的是spring mvc,事實上,他們都是基於spring framework,並且再其上繼續增強,為某一方面服務的java元件。最近spring framework 剛升級到5,非常不錯。比較常見的有

面試為什麼要離開之前的公司?

離職、跳槽是一件很正常的事。但是難免在面試時會被問到:“你為什麼要離開之前的公司?”首先,你要知道面試官這麼問的目的是什麼?面試官問這個問題,是想知道你離開之前公司的原因是否合理,是想知道你是否對公司忠誠、有熱情、感興趣,想考察你動機是否單純、是否只是把公司當成一個“避難所”