关于IdleHandler的疑惑

3,562 阅读5分钟

奇怪的疑惑

在日常工作或者学习时,总会生出各种奇怪的疑惑,寻找答案的过程(一把梭,搜)非常有意思,得到的答案(一把梭,猜)也非常有价值。那么这份意思和价值就应该分享出来~

疑问来源

看到项目老代码做了一件事,自定义了一个页面的RootView,在首次onDraw的时候进行一些初始化工作,伪代码如下:

RootView {
  onDraw() {
    if (firstDraw) {
      mActivity.handler.postDoSomething();
    }
  }
}

乍一看,哟,这小伙儿想在页面首次绘制完成后(偷摸摸)干一些事儿,操作骚的很嘛

不过骚操作我也会,这种蜜汁需求我好像还有其他方法完成,诶,记得上次看了个IdleHandler可以用来做类似的事,待我翻翻以前看到的文章IdleHandler,页面启动优化神器

该文章分析到,我们可以通过IdleHandler实现在界面绘制完成后进行一个回调,从而(偷摸摸)干上一些事儿

问题来了,IdleHandler真这么骚?

验他一验

好嘛,这么骚的操作,我来验证一下:Open Demo Project

Activity :

@Override
protected void onResume() {
    super.onResume();

    Log.i("TEST_TAG", "onResume: ");
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            Log.i("TEST_TAG", "queueIdle: ");
            return false;
        }
    });
}

RootView :

RootView {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i("TEST_TAG", "onDraw: ");
        mAcrivity.handler.post(new Runnable() {
            @Override
            public void run() {
                Log.i("TEST_TAG", "run: post");
            }
        });
    }
}

Run It!

Run It!

分析一下

情况符合预期,由于在ActivityThread内先调用Activity.onResume(),后调用window.addView(),所以输出onResume日志之后输出onDraw日志,而后因为在onDraw内post了一个任务,输出了run日志,在这之后由于主线程idle了(没活儿了)所以调用了IdleHandler的回调输出queueIdle日志

再生疑惑

之前测试在IdleHandler回调中返回了false,表示执行完成后从MessageQueue中移除掉自己,如果哪次手抽返回了true了?岂不是因为Looper死循环一直获取MessageQueue.next(),导致一直判断没有下一个message,从而一直疯狂的调用idleHandler?就像下面Message.next()的源码所说:

 Message next() {
        ...一坨代码
        for (;;) {
            ...一坨代码
            if (msg != null) {
                if (now < msg.when) {
                    ...省略
                } else {
                    ...省略
                    return msg;
                }
            }
            ...一坨代码
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            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);
                    }
                }
            }
            ... 省略
        }
    }

源码说,如果当前时刻没有message需要处理,那我就应该翻牌IdleHandler。

但是,如果长时间没有message需要处理,处在for (;;)死循环内的咱们,唯一离开循环的条件message != null长时间不成立,岂不是一直在循环,一直在调用IdleHandler?

恍然大悟

哦不!我这该死的脑子🧠总是记不住东西!精华全忘,代码都少抠了!

Message next() {
        ...一坨代码
        for (;;) {
            ...一坨代码
            
            // 居然忘了你!
            nativePollOnce(ptr, nextPollTimeoutMillis);
            
            if (msg != null) {
                if (now < msg.when) {
                    ...省略
                } else {
                    ...省略
                    return msg;
                }
            }
        }
}

居然忘了没有消息的时候线程通过nativePollOnce(ptr, nextPollTimeoutMillis);进入了休眠状态

也就是说,IdleHandler的调用时机相关描述应该为:

每次消费掉一个有效message,在获取下一个message时,如果当前时刻没有需要消费的有效(需要立刻执行)的message,那么会执行IdleHandler一次,执行完成之后线程进入休眠状态,直到被唤醒

那么消息机制里的线程休眠是怎么回事,唤醒又是怎么回事,可以祭出收藏已久的博文Android应用程序消息处理机制(Looper、Handler)分析,出自大佬罗升阳

老有问题

那么问题又来了,学挖掘....(啪,老实点!)

哦不,问题是IdleHandler到底能用来解决什么问题?实现什么鬼需求?

不要怀疑Google的Coder

Google的Coder既然把IdleHandler放在了这,那么...那么我们就看看他用来干什么了。

打开MessageQueue.java找到interface IdleHandler,按住command点他一点

IdleHandler都用在了哪

粗略看了看(挑简单不费事能看明白的看),比如ActivityThread内GcIdler用于某些情况下强制进行BinderInternal.GC(虽然我不知道这是在干啥),比如在MonitoringInstrumentation内用作数据检测用,比如在TextToSpeechService内结合HandlerThread用于任务完成后进行广播通知用。

我还需要更多答案

看Google大佬用IdleHandler还真发现一些用法,那他还有更多的骚操作嘛?

这个问题比较严肃,就需要交给Google了

给他吧

经过检索提炼,发现每日一问 听说过Handler中的IdleHandler吗?链接内对这个问题有讨论,也分析了系统怎么用它,看起来有些货

来自链接内,建议进入阅读

@bigdevil

...省略部分...,目前可以想到的场景

  1. Activity启动优化:onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间
  2. 想要在一个View绘制完成之后添加其他依赖于这个View的View,当然这个用View#post()也能实现,区别就是前者会在消息队列空闲时执行
  3. 发送一个返回true的IdleHandler,在里面让某个View不停闪烁,这样当用户发呆时就可以诱导用户点击这个View,这也是种很酷的操作
  4. 一些第三方库中有使用,比如LeakCanary,Glide中有使用到,具体可以自行去查看

结语

如果我的文章确实有帮助到你,请不要忘了点一下👍

难免有很多地方理解不到位甚至解释错误,还请请直接指出