1. 问题背景
最近测试同学反馈了一个问题,有个自定义 View 的内容区域在某个特定场景下展示空白。
首先抓包看了后端下发的数据是正常的,同时数据也正确传给了 View,可以排除是数据问题。
接下来怀疑是 View 的初始化有问题,断点发现,View 内部的初始化方法的确没有执行到。由于这个 View 不是页面首屏必须展示的,为了提升首屏打开速度,它的初始化方法是通过 addIdleHandler 的方式去触发。
按理说,主线程处理完首屏的 UI 渲染以及其他计算工作之后,总会有空闲的时机执行 IdleHandler 任务。但这个 IdleHandler 一直得不到执行,那只能说明主线程一直处于繁忙的状态。
这里先直接说原因:该场景下存在多个自定义
TextView在onLayout()方法中调用 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 中观察,这里将日志中包含 FrameDisplayEventReceiver 和 HardwareRendererObserver 都剔除了。
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 中观察,这里将日志中包含 FrameDisplayEventReceiver 和 HardwareRendererObserver 都剔除了。
使用示例
调用以下代码将输出每一帧 MessageQueue 中的消息。
kotlin
Choreographer.getInstance().postFrameCallback(MessageHelper())
4. 总结
-
IdleHandler 可以用来处理一些不紧急的任务,比如 ActivityThread 使用它来执行 gc 任务
-
Looper.setMessageLogging()方法打印消息日志,观察是否有异常消息 -
使用上述 MessageHelper 类打印当前 MessageQueue 中的所有 Message,观察是否有异常消息