问题记录 - Android IdleHandler 没有执行

1. 问题背景

最近测试同学反馈了一个问题,有个自定义 View 的内容区域在某个特定场景下展示空白。

首先抓包看了后端下发的数据是正常的,同时数据也正确传给了 View,可以排除是数据问题。

接下来怀疑是 View 的初始化有问题,断点发现,View 内部的初始化方法的确没有执行到。由于这个 View 不是页面首屏必须展示的,为了提升首屏打开速度,它的初始化方法是通过 addIdleHandler 的方式去触发。

按理说,主线程处理完首屏的 UI 渲染以及其他计算工作之后,总会有空闲的时机执行 IdleHandler 任务。但这个 IdleHandler 一直得不到执行,那只能说明主线程一直处于繁忙的状态。

这里先直接说原因:该场景下存在多个自定义 TextViewonLayout() 方法中调用 update 方法,update 方法又会使得 onLayout() 方法被调用,以此无限循环,向 MessageQueue 一直发送大量 Message,导致主线程根本没有空闲的时间执行 IdleHandler 任务。主线程虽然繁忙但没有卡顿,因为做的都是 UI 渲染工作。

知道是主线程繁忙引起的不难,但是要定位到具体是哪段代码出的问题还是花了不少时间。过程中绕了很多弯路,最终才得知有快速定位的方法,在此分享给大家:

通过 Looper.setMessageLogging() 方法监听主线程 MessageQueue 上的 Message 日志,观察日志便能很容易找出问题所在。

ruby 复制代码
2025-11-27 16:27:26.173 28376-28494 xxx   D  [:0, ]:28376 28376 >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@82a6bb1: 0
2025-11-27 16:27:26.174 28376-28494 xxx   D  [:0, ]:28376 28376 <<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@82a6bb1
2025-11-27 16:27:26.189 28376-28494 xxx   D  [:0, ]:28376 28376 >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@9854804: 0
2025-11-27 16:27:26.189 28376-28494 xxx   D  [:0, ]:28376 28376 <<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@9854804
2025-11-27 16:27:26.206 28376-28494 xxx   D  [:0, ]:28376 28376 >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@d1702b3: 0
2025-11-27 16:27:26.207 28376-28494 xxx   D  [:0, ]:28376 28376 <<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@d1702b3
2025-11-27 16:27:26.223 28376-28494 xxx   D  [:0, ]:28376 28376 >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@fab846e: 0
2025-11-27 16:27:26.224 28376-28494 xxx   D  [:0, ]:28376 28376 <<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@fab846e
2025-11-27 16:27:26.241 28376-28494 xxx   D  [:0, ]:28376 28376 >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@a3f91a5: 0
2025-11-27 16:27:26.242 28376-28494 xxx   D  [:0, ]:28376 28376 <<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@a3f91a5
2025-11-27 16:27:26.256 28376-28494 xxx   D  [:0, ]:28376 28376 >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {2da134c} com.xxx.yyy.zzz.customTextView$$ExternalSyntheticLambda1@27c4d88: 0ƒ

上面的日志中 com.xxx.yyy.zzz.customTextView 不断地大量出现,必然有问题,点进这个 TextView 里面寻找肯定能找到问题根源,过程这里就不赘述了。

Looper.setMessageLogging() 的具体用法在下面的篇幅会有说明。

2. IdleHandler

IdleHandler 简介

应用中有些任务很重要必须被执行,但时机上又没有那么迫切。于是便有了 IdleHandler,它最适合处理这种场景,既能充分利用 CPU 但又不跟紧急任务争夺 CPU。当线程没有 message 需要处理而阻塞时,简单来说就是线程空闲,IdleHandler.queueIdle() 被回调。queueIdle() 返回 false 时将自动从 MessageQueue 中清理当前 IdleHandler。

java 复制代码
/**
 * 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 用法

kotlin 复制代码
Looper.myQueue().addIdleHandler(object : IdleHandler {
    override fun queueIdle(): Boolean {
        Log.d("TAG", "addIdleHandler example")
        return false
    }
})

上面这段代码输出 1 次 addIdleHandler example。不过要是将 return false 改成 return true,则会在空闲时间多次输出 addIdleHandler example,直到将这个 IdleHandler 移除。

MessageQueue.addIdleHandler()

java 复制代码
/**
 * Add a new {@link IdleHandler} to this message queue.  This may be
 * removed automatically for you by returning false from
 * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
 * invoked, or explicitly removing it with {@link #removeIdleHandler}.
 *
 * <p>This method is safe to call from any thread.
 *
 * @param handler The IdleHandler to be added.
 */
public void addIdleHandler(@NonNull IdleHandler handler) {
    if (handler == null) {
        throw new NullPointerException("Can't add a null IdleHandler");
    }
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}

Android 系统中也有用到 IdleHandler,详见 ActivityThread.scheduleGcIdler()

java 复制代码
final class GcIdler implements MessageQueue.IdleHandler {
    @Override
    public final boolean queueIdle() {
        doGcIfNeeded();
        purgePendingResources();
        return false;
    }
}

@UnsupportedAppUsage
void scheduleGcIdler() {
    if (!mGcIdlerScheduled) {
        mGcIdlerScheduled = true;
        Looper.myQueue().addIdleHandler(mGcIdler);
    }
    mH.removeMessages(H.GC_WHEN_IDLE);
}

void unscheduleGcIdler() {
    if (mGcIdlerScheduled) {
        mGcIdlerScheduled = false;
        Looper.myQueue().removeIdleHandler(mGcIdler);
    }
    mH.removeMessages(H.GC_WHEN_IDLE);
}

源码分析

看看 MessageQueue.next() 方法是如何处理 IdleHandler 的。

java 复制代码
@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.
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                        if (prevMsg.next == null) {
                            mLast = prevMsg;
                        }
                    } else {
                        mMessages = msg.next;
                        if (msg.next == null) {
                            mLast = null;
                        }
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    if (msg.isAsynchronous()) {
                        mAsyncMessageCount--;
                    }
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }

            // If first time idle, then get the number of idlers to run.
            // Idle handles only run if the queue is empty or if the first message
            // in the queue (possibly a barrier) is due to be handled in the future.
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                // 没有 IdleHandler,继续循环
                mBlocked = true;
                continue;
            }

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

        // Run the idle handlers.
        // We only ever reach this code block during the first iteration.
        // 运行 IdleHandler
        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);
            }

            // 删除不被保留的 IdleHandler
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

正如源码所述,IdleHandler 仅在线程空闲时才被执行。

  • 如果 MessageQueue 中有消息,不会执行 IdleHandler

  • 每次 next() 调用时 IdleHandler 最多只有一次被执行的机会

3. 解决方法

为什么 IdleHandler 不被执行?当然是因为 MessageQueue 中的消息太多,每次 next() 调用时都能找到一个待处理的 Message,所以 IdleHandler 根本没有处理的机会。

那么怎么查看 MessageQueue 中有哪些消息呢?

setMessageLogging()

注释文档中说,setMessageLogging() 用于当前 Looper 处理 Message 时打印日志。

  • 传 null 参数关闭日志功能,传非 null 的 printer 开启日志功能

  • 开启日志功能后,会在每个 Message 分发的开始以及结束时输出日志信息到 printer,具体的日志信息包括区分 Message 的目标 Hander 以及 Message 内容

对照 Looper.loop() 方法源码,跟上面描述一致。

java 复制代码
/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
@SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
        "ClearIdentityCallNotFollowedByTryFinally",
        "ResultOfClearIdentityCallNotStoredInVariable"})
public static void loop() {
    final Looper me = myLooper();
    ...
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

/**
 * Poll and deliver single message, return true if the outer loop should continue.
 */
@SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
        "ClearIdentityCallNotFollowedByTryFinally"})
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    ...
    // This must be in a local variable, in case a UI event sets the logger
    final Printer logging = me.mLogging;
    if (logging != null) {
        logging.println(">>>>> Dispatching to " + msg.target + " "
                + msg.callback + ": " + msg.what);
    }
    ...
        msg.target.dispatchMessage(msg);
    ...

    if (logging != null) {
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }
    ...
    return true;
}

使用示例

kotlin 复制代码
private fun printer() {
    Looper.getMainLooper().setMessageLogging { printer ->
        if (printer != null && !printer.contains("FrameDisplayEventReceiver")
            && !printer.contains("HardwareRendererObserver")
        ) {
            Log.d("xxx", printer)
        }
    }
}

为了便于 logcat 中观察,这里将日志中包含 FrameDisplayEventReceiverHardwareRendererObserver 都剔除了。

MessageHelper 工具类

kotlin 复制代码
class MessageHelper : Choreographer.FrameCallback {
    private var messagesField: Field? = null
    private var nextField: Field? = null

    init {
        try {
            messagesField = MessageQueue::class.java.getDeclaredField("mMessages")
            messagesField?.isAccessible = true
            nextField = Message::class.java.getDeclaredField("next")
            nextField?.isAccessible = true
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        }
    }

    private fun printMessages() {
        val queue = Looper.myQueue()
        try {
            var msg: Message? = messagesField?.get(queue) as Message?
            val sb = StringBuilder()
            while (msg != null) {
                sb.append(msg.toString())
                sb.append("\n")
                msg = nextField?.get(msg) as Message?
            }
            val finalString = sb.toString()
            if (!finalString.contains("FrameDisplayEventReceiver")
                && !finalString.contains("HardwareRendererObserver")
            ) {
                Log.i(TAG, finalString)
            }
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
    }

    override fun doFrame(frameTimeNanos: Long) {
        Choreographer.getInstance().postFrameCallback(this)

        printMessages()
    }

    companion object {
        private const val TAG = "MessageHelper"
    }
}

为了便于 logcat 中观察,这里将日志中包含 FrameDisplayEventReceiverHardwareRendererObserver 都剔除了。

使用示例

调用以下代码将输出每一帧 MessageQueue 中的消息。

kotlin 复制代码
Choreographer.getInstance().postFrameCallback(MessageHelper())

4. 总结

  • IdleHandler 可以用来处理一些不紧急的任务,比如 ActivityThread 使用它来执行 gc 任务

  • Looper.setMessageLogging()方法打印消息日志,观察是否有异常消息

  • 使用上述 MessageHelper 类打印当前 MessageQueue 中的所有 Message,观察是否有异常消息

5. 参考

相关推荐
dalancon3 小时前
VSYNC 信号完整流程2
android
dalancon3 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013844 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android5 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才5 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶6 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙6 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github
qq_283720057 小时前
MySQL技巧(四): EXPLAIN 关键参数详细解释
android·adb
没有了遇见8 小时前
Android 架构之网络框架多域名配置<三>
android
myloveasuka9 小时前
[Java]单列集合
android·java·开发语言