Stack Trace 的结构
异常信息 ← 1️⃣ 第一眼:什么类型的错误
at A.method(A.java:10) ← 最近的调用(异常抛出点)
at B.method(B.java:20) ← 调用了 A
at C.method(C.java:30) ← 调用了 B
...
at Thread.run(Thread.java:xx) ← 2️⃣ 第二眼:哪个线程
- 从上到下 = 调用栈的逆序(最近 → 最远)
- 顶部 = 异常实际抛出的位置
- 底部 = 线程入口
四步阅读法
第 1 步:看底部 → 判断线程
// ✅ 主线程
at android.os.Looper.loop(Looper.java:xxx)
at android.app.ActivityThread.main(ActivityThread.java:xxx)
// ❌ 后台线程池
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:xxx)
at java.lang.Thread.run(Thread.java:xxx)
// ❌ RxJava IO 线程
at rx.schedulers.CachedThreadScheduler$EventLoopWorker...
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask...
// ❌ HandlerThread(有 Looper 的后台线程)
at android.os.Looper.loop(Looper.java:xxx)
at android.os.HandlerThread.run(HandlerThread.java:xxx)
第 2 步:看顶部 → 异常类型和抛出点
顶部前几行告诉你:
- 什么类型的错误(
NullPointerException?线程问题?IndexOutOfBounds?) - 异常在哪个方法抛出的(框架代码还是我们的代码)
第 3 步:从底往上读 → 还原执行流
沿着调用链从底向上,理解代码是怎么一步步走到出错点的。关注:
- 从哪个入口进来的(触发源)
- 经过了哪些关键节点
- 有没有线程切换(
observeOn/Handler.post/runOnUiThread)
第 4 步:找关键转折点 → 定位修复位置
在调用链中找到”应该切线程/做检查但没做的位置”,这就是修复点。
实战示例
以”快捷发布气泡 Crash”为例:
// 顶部 → 异常:动画操作需要 Looper 线程
AndroidRuntimeException: Animators may only be run on Looper threads
at AnimatorSet.cancel(...)
at Marker.cancelAnimation(...)
at MapServiceMarkerRenderLayer.removeQuickPublishGuideMarkers(...)
at MapServiceLayer.removeQuickPublishGuideMarkers(...)
at NearByMapPresenterV2.dismissQuickPublishGuide(...)
at NearByMapPresenterV2.scheduleQuickPublishGuideDismiss(...)
at NearByMapPresenterV2.lambda$initSubscription$20(...)
...
at PublishSubject.onNext(...)
at MapServiceMarkerRenderLayer$2.markerShow(...)
...
at MarkerCache$1.run(...)
// 底部 → 后台线程池
at ThreadPoolExecutor.runWorker(...)
at Thread.run(...)
还原执行流(从底往上):
ThreadPoolExecutor (后台线程)
→ MarkerCache.run // 后台渲染 Marker
→ markerShow() // 渲染完成回调(仍在后台线程)
→ PublishSubject.onNext() // 同步触发订阅者(仍在后台线程)
→ scheduleQuickPublishGuideDismiss()
→ dismissQuickPublishGuide()
→ removeQuickPublishGuideMarkers()
→ marker.cancelAnimation() → 💥 crash
关键转折点:dismissQuickPublishGuide() 在后台线程直接调用了需要主线程的 Marker 操作。
修复:用 Au.post() 将 removeQuickPublishGuideMarkers() 投递到主线程执行。
常见 Android 线程相关异常
| 异常 | 含义 | 解决 |
|---|---|---|
Animators may only be run on Looper threads | 动画操作不在 Looper 线程 | Au.post() / runOnUiThread() |
Only the original thread that created a view hierarchy can touch its views | View 操作不在主线程 | runOnUiThread() / observeOn(mainThread) |
Can't toast on a thread that has not called Looper.prepare() | Toast 不在 Looper 线程 | 切到主线程 |
WindowManager$BadTokenException | Dialog/Window 操作异常 | 检查 Activity 生命周期 + 主线程 |
Cannot call this method while RecyclerView is computing a layout | RecyclerView 布局中修改数据 | post() 延迟执行 |
各类线程的 Stack Trace 特征
主线程(Android Main Thread)
at android.os.Handler.dispatchMessage(Handler.java:xxx)
at android.os.Looper.loop(Looper.java:xxx) ← 🔑 消息循环
at android.app.ActivityThread.main(ActivityThread.java:xxx) ← 🔑 主线程入口
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:xxx)
看到 ActivityThread.main → 100% 是主线程。这是 Android 应用主线程的入口方法。
后台线程池(ThreadPoolExecutor)
at MyTask.run(MyTask.java:xxx) ← 任务的 run()
at java.util.concurrent.ThreadPoolExecutor.runWorker(...) ← 🔑 关键标识
at java.util.concurrent.ThreadPoolExecutor$Worker.run(...) ← 🔑 Worker 线程
at java.lang.Thread.run(Thread.java:xxx)
看到 ThreadPoolExecutor.runWorker → 后台线程池。
HandlerThread(有 Looper 的后台线程)
at android.os.Handler.dispatchMessage(Handler.java:xxx)
at android.os.Looper.loop(Looper.java:xxx) ← 有 Looper
at android.os.HandlerThread.run(HandlerThread.java:xxx) ← 🔑 HandlerThread
有 Looper.loop 但底部是 HandlerThread.run 而不是 ActivityThread.main → 后台 Looper 线程。
RxJava IO/Computation 线程
at rx.internal.schedulers.ScheduledAction.run(...) ← RxJava 调度
at java.util.concurrent.FutureTask.run(...)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(...)
at java.util.concurrent.ThreadPoolExecutor.runWorker(...) ← 底层还是线程池
at java.lang.Thread.run(...)
普通 Thread
at com.example.MyTask.run(MyTask.java:xxx)
at java.lang.Thread.run(Thread.java:xxx) ← 只有这一行
速判口诀
| 底部关键词 | 线程类型 | 有 Looper? |
|---|---|---|
ActivityThread.main | 主线程 | ✅ |
HandlerThread.run | 后台 Looper 线程 | ✅ |
ThreadPoolExecutor.runWorker | 后台线程池 | ❌ |
ScheduledThreadPoolExecutor | RxJava/定时线程池 | ❌ |
只有 Thread.run | 普通后台线程 | ❌ |
线程名辅助判断
Stack trace 顶部有时带线程名:
Thread ID: Name: main ← 主线程
Thread ID: Name: pool-3-thread-1 ← 线程池
Thread ID: Name: RxIoScheduler-2 ← RxJava IO 线程
线程名可能是自定义的(如 exception),不一定直观,底部调用栈更可靠。
快速速查
| 想知道什么 | 怎么看 |
|---|---|
| ”什么炸了” | 看顶部(异常类型 + 抛出点) |
| “在哪个线程” | 看底部(Thread.run / ThreadPoolExecutor / Looper.loop) |
| “怎么走到这的” | 从底往上读,还原调用流 |
| ”谁的锅” | 找我们的代码和框架代码的边界 |