阅读 177

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 知识点。

作者:Android开发架构师

原文链接:https://www.jianshu.com/p/df88769c4559

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐