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 viewsView 操作不在主线程runOnUiThread() / observeOn(mainThread)
Can't toast on a thread that has not called Looper.prepare()Toast 不在 Looper 线程切到主线程
WindowManager$BadTokenExceptionDialog/Window 操作异常检查 Activity 生命周期 + 主线程
Cannot call this method while RecyclerView is computing a layoutRecyclerView 布局中修改数据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后台线程池❌
ScheduledThreadPoolExecutorRxJava/定时线程池❌
只有 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)
“怎么走到这的”从底往上读,还原调用流
”谁的锅”找我们的代码和框架代码的边界