Looper 消息监控
Looper.setMessageLogging() 常被当作“主线程卡顿检测 API”,但它实际只是给每次消息分发 发送两条字符串日志。Android 17 的 Looper.loopOnce() 还提供独立的慢 delivery、慢 dispatch 阈值,以及面向 framework 的 Looper.Observer token 回调。三者观察的时间点和异常能力不同, 不能用一个 Printer 字符串解析器替代全部监控。
本文面向已经读过 Looper主循环、 消息延迟边界、FrameHandler 帧调度 和 ActivityThread.H 主线程 的读者。 本文只追踪 Looper 的消息观测边界:日志何时开始/结束、delivery 与 dispatch 如何分别计算、 异常时哪条回调仍然收到通知,以及 ActivityThread 如何在真实源码中创建主 Looper。本文不 提供未经源码支持的“16ms 阈值”、线上采样比例或第三方监控库结论。
读完后,读者应能从一条日志判断它表示“等待太久”还是“执行太久”,解释异常为何没有 <<<<< Finished 的保证,选择 Printer、slow log 或 Observer 的适用场景,并写出不依赖 字符串内容的最小验证。
1. 配置入口
源码文件:frameworks/base/core/java/android/os/Looper.java
setMessageLogging() 只保存 Printer 引用;传入非空 Printer 时还调用 StrictMode.noteSlowCall("setMessageLogging")。日志默认关闭,传入 null 即停止。
public void setMessageLogging(@Nullable Printer printer) {
if (printer != null) {
StrictMode.noteSlowCall("setMessageLogging");
}
mLogging = printer;
}源码文件:frameworks/base/core/java/android/util/Printer.java
Printer 的接口只有一个 println(String x),所以 Looper 不会把结构化 Message、异常对象或 独立 token 传给 Printer。
public interface Printer {
void println(String x);
}这决定了 Printer 监控的第一条边界:它只能从字符串中观察 target、callback 和 what 的文本 表示,不能稳定获得内部字段或异常原因。
2. 派发切面
源码文件:frameworks/base/core/java/android/os/Looper.java
loopOnce() 从 MessageQueue 取出消息后,读取当前 mLogging 到局部变量,然后在调用 msg.target.dispatchMessage(msg) 前后分别输出开始和结束文本。
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
try {
msg.target.dispatchMessage(msg);
// Observer、slow log 和 finally 清理
} finally {
// 恢复 WorkSource、结束 trace、停止 LooperDoctor
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}两条 Printer 日志包围的是 Handler 的 dispatchMessage() 区间,而不是 MessageQueue 等待区间。 因此:
| 观测 | 起点 | 终点 | 是否包含排队等待 |
|---|---|---|---|
| Printer dispatch 文本 | 即将调用 dispatchMessage | dispatch 正常返回后 | 否 |
| slow delivery | msg.when | dispatch 开始 | 是 |
| slow dispatch | dispatch 开始 | dispatch 正常返回 | 否 |
3. 日志文本
源码文件:frameworks/base/core/java/android/os/Looper.java
开始日志包含 msg.target、msg.callback 和 msg.what;结束日志不包含 what。因此一个 Printer 解析器不能只用结束文本唯一定位所有消息,也不能假定 target 的 toString() 永远 稳定。
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);如果 target 的 toString() 或 callback 的 toString() 自身很重,日志拼接也会增加派发开销。 源码 Javadoc 明确说 Message logging 默认关闭并会带来性能代价;这不是对具体设备耗时的数值 保证,而是 API 层的成本边界。
4. 主循环入口
源码文件:frameworks/base/core/java/android/os/Looper.java
Looper.loop() 反复调用 loopOnce();当 MessageQueue 返回 null(通常表示退出)时停止循环。 所以 Printer 观察的是已经被 Looper 取出的消息,不会看到仍在队列中等待或被退出清理的消息。
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
me.mInLoop = true;
final long ident = Binder.clearCallingIdentity();
for (;;) {
if (!loopOnce(me, ident, 0)) {
return;
}
}
}这也解释了监控盲区:如果消息在 next() 前被 removeMessages()、quit() 或其它队列清理 删除,Printer 不会收到一对开始/结束日志。
5. 慢消息阈值
源码文件:frameworks/base/core/java/android/os/Looper.java
Android 17 提供隐藏的 setSlowLogThresholdMs(),分别配置 delivery 和 dispatch 阈值。两者 都大于 0(或 loop 的 threshold override 生效)时,loopOnce() 才读取相应时间戳。
public void setSlowLogThresholdMs(long slowDispatchThresholdMs,
long slowDeliveryThresholdMs) {
mSlowDispatchThresholdMs = slowDispatchThresholdMs;
mSlowDeliveryThresholdMs = slowDeliveryThresholdMs;
}loopOnce() 的计算分开决定是否需要开始/结束时间:
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0 || hasOverride)
&& (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0 || hasOverride);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;delivery 需要 msg.when,因为它测量从消息计划时间到真正开始 dispatch 的延迟;没有有效 when 的 front-of-queue 或特殊消息不能按同一规则计算。
6. 慢日志内容
源码文件:frameworks/base/core/java/android/os/Looper.java
showSlowLog() 用测量区间与阈值比较,并记录进程名、线程名、是否主 Looper、线程组、Handler 类名、callback 和 what。它只说明某段 Looper 时间超过阈值,不直接给出业务根因。
private static boolean showSlowLog(long threshold, long measureStart,
long measureEnd, String what, Message msg) {
final long actualTime = measureEnd - measureStart;
if (actualTime < threshold) {
return false;
}
Slog.w(TAG, "Slow " + what + " took " + actualTime + "ms "
+ Thread.currentThread().getName()
+ " app=" + Process.myProcessName()
+ " main=" + (myLooper() == getMainLooper())
+ " h=" + msg.target.getClass().getName()
+ " c=" + msg.callback + " m=" + msg.what);
return true;
}慢 delivery 表示消息到达执行机会之间等待较久;慢 dispatch 表示 Handler 代码本身执行较久。 如果 delivery 慢而 dispatch 正常,应该优先检查前序消息和队列拥塞,而不是直接修改当前 callback。
7. Delivery 抑制
源码文件:frameworks/base/core/java/android/os/Looper.java
Looper 使用 mSlowDeliveryDetected 抑制连续 slow delivery 日志;当后续消息恢复到接近及时 状态时记录 Drained 并清除抑制。它是日志降噪状态,不是队列状态机,也不会改变消息调度。
if (me.mSlowDeliveryDetected) {
if (!slow && (dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
me.mSlowDeliveryDetected = false;
}
} else if (slow) {
me.mSlowDeliveryDetected = true;
}因此一次日志缺失不一定表示 delivery 没有变慢,可能是抑制状态仍然有效;调试时要结合 阈值配置、Drained 和消息序列判断。
8. 正常结束
源码文件:frameworks/base/core/java/android/os/Looper.java
正常 dispatch 返回后,Looper 记录 dispatch 结束时间、执行 slow log,发送 Printer 结束文本, 检查 Binder identity,最后回收 Message。Printer 结束日志发生在 Message 回收前。
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
if (logSlowDelivery) {
showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery", msg);
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
msg.recycleUnchecked();这段顺序说明 Printer 的结束回调已经接近 dispatch 完成,但不代表后续 Message 回收、Binder identity 检查或外部资源清理都完成。
9. 异常结束
源码文件:frameworks/base/core/java/android/os/Looper.java
如果 dispatchMessage() 抛出 Exception,Looper 会通知 Observer 的 dispatchingThrewException(),然后重新抛异常。Printer 的结束日志位于正常路径之后,因此 异常路径不能依赖 <<<<< Finished 作为配对信号。
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}因此字符串 Printer 监控可能看到开始而没有结束;若需要可靠异常配对,应使用 Observer 的 token 回调或外围未捕获异常机制,而不是猜测“下一条日志就是结束”。
10. Observer token
源码文件:frameworks/base/core/java/android/os/Looper.java
Looper.Observer 是隐藏接口,提供 messageDispatchStarting()、messageDispatched() 和 dispatchingThrewException()。loopOnce() 在读取消息前保存静态 Observer 快照,再为当前 消息创建 token。
public interface Observer {
Object messageDispatchStarting();
void messageDispatched(Object token, Message msg);
void dispatchingThrewException(Object token, Message msg, Exception exception);
}
public static void setObserver(@Nullable Observer observer) {
sObserver = observer;
}token 必须在正常完成或异常完成两条路径中恰好交回一次。Observer 直接拿到 Message 对象, 不需要解析 Printer 文本;但它是 hidden API,不能把它当普通应用公开 SDK 能力。
11. 主线程配置
源码文件:frameworks/base/core/java/android/app/ActivityThread.java
ActivityThread 的 main() 创建主 Looper 后进入 Looper.loop();源码中默认没有打开 setMessageLogging,对应代码被包在 if (false) 中。这说明主线程消息日志不是默认生产行为。
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();应用或测试可以在 Looper 已建立后显式设置 Printer,但必须考虑日志拼接、Printer 回调和 主线程本身的额外开销。
12. 监控时序
源码文件:frameworks/base/core/java/android/os/Looper.java
把真实控制流压缩成时序时,应保留 delivery 起点和 dispatch 起点的差异:
图中 Printer 不是异常闭环,Observer 才有显式成功/失败分支;两者都发生在同一个 Looper 线程上。
13. 可执行验证
源码文件:frameworks/base/core/java/android/os/Looper.java
第一组验证使用一个受控 HandlerThread:设置 Printer,投递一个 Runnable,记录两条文本和 Runnable 线程;断言开始文本先于 Runnable、结束文本在 Runnable 正常返回后出现。它证明 Printer 的正常顺序,不证明消息未出队前的等待时间。
第二组验证设置 slow delivery/dispatch 阈值:先让队列处理一个耗时 Runnable,再投递第二条 消息,分别观察 delivery 与 dispatch 日志。需要记录 msg.when、dispatch 开始和结束,不能 只用一条总耗时替代两个区间。
第三组验证设置 Observer,让 Handler 抛出 Exception,断言 token 进入 dispatchingThrewException() 而不是 messageDispatched();同时接受 Printer 可能只有开始 日志。该输入证明异常配对边界,不证明进程一定继续运行。
14. 复查命令
源码文件:
frameworks/base/core/java/android/os/Looper.javaframeworks/base/core/java/android/util/Printer.javaframeworks/base/core/java/android/app/ActivityThread.java
rg -n "setMessageLogging|getMessageLogging|mLogging|Dispatching to|Finished to" \
frameworks/base/core/java/android/os/Looper.java
rg -n "setSlowLogThresholdMs|showSlowLog|mSlowDeliveryDetected|Slow delivery|Slow dispatch" \
frameworks/base/core/java/android/os/Looper.java
rg -n "interface Observer|setObserver|messageDispatchStarting|messageDispatched|dispatchingThrewException" \
frameworks/base/core/java/android/os/Looper.java
rg -n "setMessageLogging|Looper.loop\(\)" \
frameworks/base/core/java/android/app/ActivityThread.java复查一次监控结论时,必须标出 MessageQueue.next()、Printer 开始、dispatch 开始、dispatch 结束、Observer 结束和 Message recycle 的先后。只看到 >>>>>/<<<<< 两行,无法区分 delivery 慢、dispatch 慢和异常退出。
15. 适用边界
Printer 适合快速观察消息文本和正常 dispatch 区间;slow log 适合 framework 根据 delivery/ dispatch 阈值输出结构化警告;Observer 适合内部结构化 telemetry 和异常配对。它们都不能 自动定位业务根因、抢占主线程、补回被队列丢弃的消息或证明整个 UI 帧已完成。
当出现“主线程卡顿”时,应先判断是消息尚未取出、delivery 等待、dispatch 执行,还是 GC/锁/下游 Binder 阻塞;再选择相应观测点。把所有时间都归因于 Printer 记录的两行日志,会丢失最关键的 队列和异常边界。
