问题记录 - 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. 参考

相关推荐
没有了遇见2 小时前
Android ButterKnife Android 35情况下 适配 Gradle 8.+
android
方白羽2 小时前
Android多层嵌套RecyclerView滚动
android·java·kotlin
菜就多学3 小时前
SurfaceControlViewHost 实现跨进程UI渲染
android·设计
2501_915106323 小时前
iOS App 测试工具全景分析,构建从开发调试到线上监控的多阶段工具链体系
android·测试工具·ios·小程序·uni-app·iphone·webview
小羊在奋斗4 小时前
MySQL表的约束:从基础到核心(附场景+案例)
android·数据库·mysql
e***19355 小时前
MySQL-mysql zip安装包配置教程
android·mysql·adb
方白羽5 小时前
Kotlin遇上Java 静态方法
android·java·kotlin
q***06476 小时前
SpringSecurity相关jar包的介绍
android·前端·后端
7***31887 小时前
若依微服务中配置 MySQL + DM 多数据源
android·mysql·微服务