IdleHandler 的原理分析和妙用

作者:傷心的豬大腸
原文轉載:https://juejin.cn/post/6918941568359481352

我們都知道 Android 是基於消息處理機制的,比如應用啓動過程,Activity 啓動以及用戶點擊行爲都與 Handler 息息相關,Handler 負責 Android 中的消息處理,對於 Android 中的消息處理機制來說,MessageQueue 和 Looper,Message 也是非常重要的對象,而 IdleHandler 是 MessageQueue 的靜態內部接口。

IdleHandler,這是一種在只有當消息隊列沒有消息時或者是隊列中的消息還沒有到執行時間時纔會執行的 IdleHandler,它存放在mPendingIdleHandlers隊列中。

    /**
     * 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();
    }

從源碼的英文定義可以看出,IdleHandler 是一個回調接口,當線程中的消息隊列將要阻塞等待消息的時候,就會回調該接口,也就是說消息隊列中的消息都處理完畢了,沒有新的消息了,處於空閒狀態時就會回調該接口。

然後我們看看平時是如何使用的:

        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                //空閒時處理邏輯
              
                return false;
            }
        });

從之前的類定義可以看出,返回 false 表示執行後就將該回調移除掉,返回 true 表示該 IdleHandler 一直處於活躍狀態,只要空閒就會被回調。從代碼的使用來看,這裏相當於就是一個監聽回調,那麼觸發是在哪裏呢?

IdleHandler 的觸發

要了解觸發點在哪裏,首先我們簡要介紹一下 Android 中的消息處理機制。Android 是基於消息處理機制進行事件的處理的,用戶的點擊行爲都是通過消息來傳遞的。如果要保證一個程序能快速響應用戶的點擊事件,首先這個程序一定是存活的(在運行的),Android 的設計是程序中有一個死循環,在這個死循環中,如果沒有消息要處理了,那麼就進入休眠狀態,一旦有消息就立刻喚醒。下面我們來看看 Android 中的這個死循環在哪裏,我們先進入 Android 中的 ActivityThread 的 main() 方法:

//ActivityThread.java
public static void main(String[] args) {
    ···
    Looper.prepareMainLooper(); //創建Looper
        ···
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

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

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop(); //死循環所在的位置

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

再來看看 Looper.loop()中的代碼

    public static void loop() {
        final Looper me = myLooper();
                ···
        final MessageQueue queue = me.mQueue;

                ···

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
           ···
            msg.recycleUnchecked();
        }
    }

可以看到,loop 方法中存在一個for(;;)死循環,如果該方法中 queue.next()方法返回 null ,那麼直接 return 退出整個死循環,整個ActivityThread.main()方法也就結束了,整個程序也就退出了。但是我們的程序肯定是一直在運行的,也就是說 queue.next()方法中一直有消息,但是如果一段時間沒有操作了,整個程序也就沒有執行的消息了,那爲什麼程序還能一直運行呢,所以問題肯定就在 queue.next()這個方法中。

該方法中也有一個 for(;;)死循環,裏面有一個關鍵方法 nativePollOnce

    @UnsupportedAppUsage
    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.
                ···
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
                        ···
            nativePollOnce(ptr, nextPollTimeoutMillis); //沒有消息,阻塞等待
            ···
        }
    }

該方法在 nextPollTimeoutMillis = -1的時候就阻塞等待,直到下一條消息可用爲止。否則就繼續向下執行。那我們再看看是在哪裏喚醒的呢?是在消息入隊最終執行的方法 enqueueMessage 中:

boolean enqueueMessage(Message msg, long when) {
        ···
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

在 nativeWake 方法中進行喚醒,就是喚醒上面的那個地方,沒有消息的時候,這裏就處於阻塞狀態。

這樣我們把消息處理機制的整個邏輯大概梳理了一下,爲什麼需要理清呢,因爲 IdleHandler 是在消息隊列沒有消息或者是在有暫時不需要處理的消息(延遲消息),就是說這個時候是空閒的,進行 IdleHandler 進行處理的。所以我們可以猜測 IdleHandler 應該也在 next 方法中進行觸發它的方法。事實也確實如此:

Message next() {
        ......
        for (;;) {
            ......
            synchronized (this) {
        // 此處爲正常消息隊列的處理
                ......
                if (mQuitting) {
                    dispose();
                    return null;
                }
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                //mIdleHandlers 數組,賦值給 mPendingIdleHandlers
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                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;
        }
    }

看 MessageQueue 的源碼可以發現有兩處關於 IdleHandler 的聲明,分別是:

  • 存放 IdleHandler 的 ArrayList(mIdleHandlers),
  • 還有一個 IdleHandler 數組 (mPendingIdleHandlers)。

後面的數組,它裏面放的 IdleHandler 實例都是臨時的,也就是每次使用完(調用了queueIdle 方法)之後,都會置空(mPendingIdleHandlers[i] = null),在 MessageQueue 的 next() 方法中
大致的流程是這樣的:

  1. 如果本次循環拿到的 Message 爲空,或者這個 Message 是一個延時的消息而且還沒到指定的觸發時間,那麼,就認定當前的隊列爲空閒狀態。
  2. 接着就會遍歷 mPendingIdleHandlers 數組(這個數組裏面的元素每次都會到 mIdleHandlers 中去拿)來調用每一個IdleHandler 實例的 queueIdle 方法。
  3. 如果這個方法返回false的話,那麼這個實例就會從 mIdleHandlers 中移除,也就是當下次隊列空閒的時候,不會繼續回調它的 queueIdle 方法了。

處理完 IdleHandler 後會將 nextPollTimeoutMillis 設置爲0,也就是不阻塞消息隊列,當然要注意這裏執行的代碼同樣不能太耗時,因爲它是同步執行的,如果太耗時肯定會影響後面的 message 執行。

系統源碼中的使用

知道了這個 IdleHandler 是如何被觸發的,我們再來看看系統源碼時如何使用它的:比如 ActivityThread.Idle 在 ActivityThread.handleResumeActivity()中調用。

 @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;
                        ···
                    //該方法最終會執行 onResume方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        if (r == null) {
            // We didn't actually resume the activity, so skipping any follow-up actions.
            return;
        }
                        ··· 
                    ···

        r.nextIdle = mNewActivities;
        mNewActivities = r;
        if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
        Looper.myQueue().addIdleHandler(new Idler());
    }

可以看到在 handleResumeActivity() 方法中末尾會執行 Looper.myQueue().addIdleHandler(new Idler()),也就是說在 onResume 等方法都執行完,界面已經顯示出來了,那麼這個 Idler 是用來幹嘛的呢?先來看看這個內部類的定義:

    private class Idler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            ActivityClientRecord a = mNewActivities;
                                ···
            if (a != null) {
                mNewActivities = null;
                IActivityManager am = ActivityManager.getService();
               ActivityClientRecord prev;
                do {
                                            //打印一些日誌
                    if (localLOGV) Slog.v(
                        TAG, "Reporting idle of " + a +
                        " finished=" +
                        (a.activity != null && a.activity.mFinished));
                    if (a.activity != null && !a.activity.mFinished) {
                        try {
                                                            //AMS 進行一些資源的回收
                            am.activityIdle(a.token, a.createdConfig, stopProfiling);
                            a.createdConfig = null;
                        } catch (RemoteException ex) {
                            throw ex.rethrowFromSystemServer();
                        }
                    }
                    prev = a;
                    a = a.nextIdle;
                    prev.nextIdle = null;
                } while (a != null);
            }
            if (stopProfiling) {
                mProfiler.stopProfiling();
            }
                                //確認Jit 可以使用,否則拋出異常
            ensureJitEnabled();
            return false;
        }
    }

可以看到在 queueIdle 方法中會進行回收等操作,下面會詳細講解,但這一些都是等 onResume 方法執行完,界面已經顯示這些更重要的事情已經處理完了,空閒的時候開始處理這些事情。也就是說系統的設計邏輯是保障最重要的邏輯先執行完,再去處理其他次要的事情。

但是如果 MessageQueue 隊列中一直有消息,那麼 IdleHandler 就一直沒有機會被執行,那麼原本該銷燬的界面的 onStop,onDestory 就得不到執行嗎?不是這樣的,在 resumeTopActivityInnerLocked() -> completeResumeLocked() -> scheduleIdleTimeoutLocked() 方法中會發送一個會發送一個延遲消息(10s),如果界面很久沒有關閉(如果界面需要關閉),那麼 10s 後該消息被觸發就會關閉界面,執行 onStop 等方法。

常見使用方式

  1. 在應用啓動時我們可能希望把一些優先級沒那麼高的操作延遲一點處理,一般會使用 Handler.postDelayed(Runnable r, long delayMillis)來實現,但是又不知道該延遲多少時間比較合適,因爲手機性能不同,有的性能較差可能需要延遲較多,有的性能較好可以允許較少的延遲時間。所以在做項目性能優化的時候可以使用 IdleHandler,它在主線程空閒時執行任務,而不影響其他任務的執行。
  2. 想要在一個 View 繪製完成之後添加其他依賴於這個 View 的 View,當然這個用View.post()也能實現,區別就是前者會在消息隊列空閒時執行
  3. 發送一個返回 true 的 IdleHandler,在裏面讓某個 View 不停閃爍,這樣當用戶發呆時就可以誘導用戶點擊這個View,這也是種很酷的操作

第三方庫的使用

LeakCanary(1.5源碼)

我們來看看 LeakCanary 的使用,是在AndroidWatchExecutor 這個類中

public final class AndroidWatchExecutor implements WatchExecutor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private final Handler mainHandler;
  private final Handler backgroundHandler;
  private final long initialDelayMillis;
  private final long maxBackoffFactor;

  public AndroidWatchExecutor(long initialDelayMillis) {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
    this.initialDelayMillis = initialDelayMillis;
    maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis;
  }
    //初始調用
  @Override public void execute(Retryable retryable) {
    // 最終都會切換到主線程,調用waitForIdle()方法
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else {
      postWaitForIdle(retryable, 0);
    }
  }

  private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        waitForIdle(retryable, failedAttempts);
      }
    });
  }
    //IdleHandler 使用
  void waitForIdle(final Retryable retryable, final int failedAttempts) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        postToBackgroundWithDelay(retryable, failedAttempts);
        return false;
      }
    });
  }
  //最終的調用方法
    private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
    long delayMillis = initialDelayMillis * exponentialBackoffFactor;
    backgroundHandler.postDelayed(new Runnable() {
      @Override public void run() {
        Retryable.Result result = retryable.run();
        if (result == RETRY) {
          postWaitForIdle(retryable, failedAttempts + 1);
        }
      }
    }, delayMillis);
  }
}

再來看看 execute() 這個方法在何處被調用,我們知道 LeakCancary 是在界面銷燬 onDestroy 方法中進行 refWatch.watch() 的,而watch() -> ensureGoneAsync() -> execute()

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

而 ensureGone() 中會進行 GC 回收和一些分析等操作,所以通過這些分析後,我們可以知道 LeakCanary 進行內存泄漏檢測並不是 onDestry 方法執行完成後就進行垃圾回收和一些分析的,而是利用 IdleHandler 在空閒的時候進行這些操作的,儘量不去影響主線程的操作。

注意事項
關於IdleHandler的使用還有一些注意事項,我們也需要注意:

  1. MessageQueue 提供了add/remove IdleHandler方法,但是我們不一定需要成對使用它們,因爲IdleHandler.queueIdle() 的返回值返回 false 的時候可以移除 IdleHanlder。
  2. 不要將一些不重要的啓動服務放到 IdleHandler 中去管理,因爲它的處理時機不可控,如果 MessageQueue 一直有待處理的消息,那麼它的執行時機會很靠後。
  3. 當 mIdleHanders 一直不爲空時,爲什麼不會進入死循環?
  • 只有在 pendingIdleHandlerCount 爲 -1 時,纔會嘗試執行 mIdleHander;
  • pendingIdlehanderCount 在 next() 中初始時爲 -1,執行一遍後被置爲 0,所以不會重複執行;

總結

通過上面的分析,我們已經知道 IdleHandler 是 MessageQueue 的靜態內部接口,是在隊列空閒時會執行的,也瞭解它是如何被觸發的,這和消息機制息息相關。同時我們也瞭解到源碼中是怎麼使用的,通常應用開發是如何使用的,第三方框架是怎麼使用的,最後還有一些注意事項。

如果還想了解更多Android 相關的更多知識點,可以點進我的GitHub項目中:https://github.com/733gh/GH-Android-Review-master自行查看,裏面記錄了許多的Android 知識點。

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