面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

南方吳彥祖_藍斯發表於2020-11-11

一、序

大家好,冷門知識又來了!

runWithScissors() 是 Handler 的一個方法,被標記為 @hide,不允許普通開發者呼叫。

這個方法算是比較冷門,如果面試中被問及,面試者不知道時,通常面試官會換個問法:"如何在子執行緒通過 Handler 向主執行緒傳送一個任務,並等主執行緒處理此任務後,再繼續執行?"。

這個場景,就可以藉助  runWithScissors() 來實現。雖然該方法被標記為 @hide,但是在 Framework 中,也有不少場景使用到它。不過它也有一些隱患,正是因為這些隱患,讓 Android 工程師將其標為 @hide,不允許普通開發者使用。

今天我們就來聊聊 Handler 的這個冷門的方法  runWithScissors(),以及它可能出現的一些問題。

二、Handler.runWithScissors()

2.1 runWithScissors()

先撇開  runWithScissors() 方法,既然這裡存在 2 個執行緒的通訊,那肯定需要考慮多執行緒同步。

首先想到的就是 Synchronized 鎖和它的等待/通知機制,而通過 Handler 跨執行緒通訊時,想要傳送一個「任務」,Runnable 肯定比 Message 更適合。

接下來,我們看看  runWithScissors() 的實現是不是如我們預想一樣。

public final boolean runWithScissors(final Runnable r, long timeout) {
  if (r == null) {
    throw new IllegalArgumentException("runnable must not be null");
  }
  if (timeout < 0) {
    throw new IllegalArgumentException("timeout must be non-negative");
  }
  if (Looper.myLooper() == mLooper) {
    r.run();
    return true;
  }
  BlockingRunnable br = new BlockingRunnable(r);
  return br.postAndWait(this, timeout);}

可以看到, runWithScissors() 接受一個 Runnable,並且可以設定超時時間。

流程也非常簡單:

  1. 先簡單的對入參進行校驗;
  2. 如果當前執行緒和 Handler 的處理執行緒一致,則直接執行  run() 方法;
  3. 執行緒不一致,則通過 BlockingRunnable 包裝一下,並執行其  postAndWait() 方法;

那再繼續看看 BlockingRunnable 的原始碼。

private static final class BlockingRunnable implements Runnable {
  private final Runnable mTask;
  private boolean mDone;
  public BlockingRunnable(Runnable task) {
    mTask = task;
  }
  @Override
  public void run() {
    try {
      // 執行在 Handler 執行緒
      mTask.run();
    } finally {
      synchronized (this) {
        mDone = true;
        notifyAll();
      }
    }
  }
  public boolean postAndWait(Handler handler, long timeout) {
    if (!handler.post(this)) {
      return false;
    }
    synchronized (this) {
      if (timeout > 0) {
        final long expirationTime = SystemClock.uptimeMillis() + timeout;
        while (!mDone) {
          long delay = expirationTime - SystemClock.uptimeMillis();
          if (delay <= 0) {
            return false; // timeout
          }
          try {
            wait(delay);
          } catch (InterruptedException ex) {
          }
        }
      } else {
        while (!mDone) {
          try {
            wait();
          } catch (InterruptedException ex) {
          }
        }
      }
    }
    return true;
  }}

待執行的任務,會記入 BlockingRunnable 的 mTask,等待後續被呼叫執行。

postAndWait() 的邏輯也很簡單,先通過 handler 嘗試將 BlockingRunnable 發出去,之後進入 Synchronized 臨界區,嘗試  wait() 阻塞。

如果設定了 timeout,則使用  wait(timeout) 進入阻塞,若被超時喚醒,則直接返回 false,表示任務執行失敗。

那麼現在可以看到  postAndWait() 返回 false 有 2 個場景:

  1. Handler  post() 失敗,表示 Looper 出問題了;
  2. 等待超時,任務還沒有執行結束;

除了超時喚醒外,我們還需要在任務執行完後,喚醒當前執行緒。

回看 BlockingRunnable 的  run() 方法, run() 被 Handler 排程並在其執行緒執行。在其中呼叫  mTask.run(),mTask 即我們需要執行的 Runnable 任務。執行結束後,標記 mDone 並通過  notifyAll() 喚醒等待。

任務發起執行緒,被喚醒後,會判斷 mDone,若為 true 則任務執行完成,直接返回 true 退出。

2.2 Framework 中的使用

runWithScissors() 被標記為 @hide,應用開發一般是用不上的,但是在 Framework 中,卻有不少使用場景。

面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

例如比較熟悉的 WMS 啟動流程中,分別在  main() 和  initPolicy()中,通過  runWithScissors() 切換到 "android.display" 和 "android.ui" 執行緒去做一些初始工作。

private void initPolicy() {
  UiThread.getHandler().runWithScissors(new Runnable() {
    public void run() {
      // 執行在"android.ui"執行緒
      WindowManagerPolicyThread.set(Thread.currentThread(), Looper.myLooper());
      mPolicy.init(mContext, WindowManagerService.this, WindowManagerService.this);
    }
  }, 0);}

例如上面程式碼,就是從 "android.display" 執行緒,通過切換到 "android.ui" 執行緒去執行任務。

三、runWithScissors() 的問題

看似  runWithScissors() 通過 Synchronized 的等待通知機制,配合 Handler 傳送 Runnable 執行阻塞任務,看似沒有問題,但依然被 Android 工程師設為 @hide。

我們繼續看看它的問題。

3.1 如果超時了,沒有取消的邏輯

通過  runWithScissors() 傳送 Runnable 時,可以指定超時時間。當超時喚醒時,是直接 false 退出。

當超時退出時,這個 Runnable 依然還在目標執行緒的 MessageQueue 中,沒有被移除掉,它最終還是會被 Handler 執行緒排程並執行。

此時的執行,顯然並不符合我們的業務預期。

3.2 可能造成死鎖

而更嚴重的是,使用  runWithScissors() 可能造成呼叫執行緒進入阻塞,而得不到喚醒,如果當前持有別的鎖,還會造成死鎖。

我們通過 Handler 傳送的 MessageQueue 的訊息,一般都會得到執行,而當執行緒 Looper 通過  quit() 退出時,會清理掉還未執行的任務,此時傳送執行緒,則永遠得不到喚醒。

那麼在使用  runWithScissors() 時,就要求 Handler 所在的執行緒 Looper,不允許退出,或者使用  quitSafely() 方式退出。

quit() 和  quitSafely() 都表示退出,會去清理對應的 MessageQueue,區別在於, qiut() 會清理 MessageQueue 中所有的訊息,而  quitSafely() 只會清理掉當前時間點之後(when > now)的訊息,當前時間之前的訊息,依然會得到執行。

那麼只要使用  quitSafely() 退出,通過  runWithScissors() 傳送的任務,依然會被執行。

也就是說,安全使用  runWithScissors() 要滿足 2 個條件:

  1. Handler 的 Looper 不允許退出,例如 Android 主執行緒 Looper 就不允許退出;
  2. Looper 退出時,使用安全退出  quitSafely() 方式退出;

四、總結時刻

今天我們介紹了一個冷門的方法  runWithScissors() 以及其原理,可以通過阻塞的方式,向目標執行緒傳送任務,並等待任務執行結束。

雖然被它標記為 @hide,無法直接使用,但這都是純軟體實現,我們其實可以自己實現一個 BlockingRunnable 去使用。當然原本存在的問題,在使用時也需要注意。

我知道就算這個方法不被標記為 @hide,使用的場景也非常的少,但是它依然可以幫助我們思考一些臨界問題,執行緒的同步、死鎖,以及 Handler 的退出方式對訊息的影響。


只要是程式設計師,不管是Java還是Android,如果不去閱讀原始碼,只看API文件,那就只是浮於表象,這對我們的知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀原始碼,不僅限於閱讀Android系統原始碼,還包括各種優秀的開源庫。

最後為了幫助大家深刻理解Handler相關知識點的原理以及面試相關知識,這裡還為大家整理了 Android開發相關原始碼精編解析

深入解析 Handler 原始碼解析

  • 傳送訊息
  • 訊息入隊
  • 訊息迴圈
  • 訊息遍歷
  • 訊息的處理
  • 同步屏障機制
  • 阻塞喚醒機制
面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

還有Handler相關面試題解析幫助熟練掌握Handler知識:

面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

最後為了幫助大家深刻理解 Android相關知識點的原理以及面試相關知識,這裡放上我搜集整理的 2019-2020BAT 面試真題解析,我把大廠面試中 常被問到的技術點整理成了PDF,包知識脈絡 + 諸多細節。

節省大家在網上搜尋資料的時間來學習,也可以分享給身邊好友一起學習。

《960全網最全Android開發筆記》

面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

《379頁Android開發面試寶典》

歷時半年,我們整理了這份市面上最全面的安卓面試題解析大全
包含了騰訊、百度、小米、阿里、樂視、美團、58、360、新浪、搜狐等一線網際網路公司面試被問到的題目。熟悉本文中列出的知識點會大大增加通過前兩輪技術面試的機率。

如何使用它?

1.可以通過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

《507頁Android開發相關原始碼解析》

只要是程式設計師,不管是Java還是Android,如果不去閱讀原始碼,只看API文件,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀原始碼,不僅限於閱讀各大系統原始碼,還包括各種優秀的開源庫。

面試官:"Handler的runWithScissors()瞭解嗎?為什麼Google不讓開發者用?"

資料太多,全部展示會影響篇幅,暫時就先列舉這些部分截圖,以上資源均免費分享,以上內容均放在了開源專案: github  中已收錄,大家可以自行獲取。

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

相關文章