堆栈基础知识:读懂卡顿的第一现场

做性能优化,说白了就是跟堆栈打交道。我刚开始接触Android时,看到一大串地址和函数名就头大——这玩意谁能看懂?后来被线上卡顿问题搞了几次,才明白堆栈就是我们定位问题的第一现场。这一章,咱们就聊聊堆栈那点事儿。

调用栈结构:函数调用的“记账本”

调用栈(Call Stack)是程序执行时的内部记录。每调用一个方法,系统就把当前方法的返回地址、参数、局部变量压到一个后进先出的栈里。方法执行完,再弹出来。

每次函数调用都会产生一个栈帧(Stack Frame)。它在栈上长这样:

栈帧组成部分说明
返回地址调用者的下一条指令地址
参数传入的参数值(部分可能通过寄存器传递)
局部变量方法内定义的变量
保存的寄存器调用者寄存器的备份

举个例子。你在Activity的onCreate里调了initViews,initViews里又调了findViewById。堆栈从底部往上看就是:

main -> ActivityThread.performLaunchActivity -> Activity.onCreate -> initViews -> findViewById

底层函数在栈顶,调用链在下面。我们看卡顿堆栈,通常关注的就是最上面的几个函数——它们正在“忙”什么。

线程堆栈差异:每个线程都有自己的“账本”

每个线程都有自己的调用栈,互不干扰。但不同线程的堆栈特征差别很大。

我有个习惯:拿到anr或者卡顿堆栈,先看一眼当前线程名字。名字能告诉我这个线程是干什么的。
线程类型堆栈特征常见卡顿原因
主线程(main)通常深度较浅,频繁回到Looper.loop布局测量、绘制、反射、文件IO、网络
Binder线程(binder_1/2…)常见Binder transaction、onTransact调用系统服务耗时、跨进程调用阻塞
异步线程(AsyncTask、Thread)深度可能很深,或者卡在某个循环/锁上死循环、锁竞争、大数据量处理
RenderThread处于Choreographer或RenderPipeline中GPU等待、vsync延迟、绘制指令过多

注意一个坑:很多人看到主线程堆栈卡在Looper.loop()就以为没事。其实loop()是主线程的正常睡眠点——卡住是因为其他函数还没返回。你需要看堆栈里紧挨着Looper.loop上面那个函数。

主线程堆栈特征:一眼认出的三个模式

主线程堆栈有几个典型模式,我总结成三类:

  1. UI绘制卡顿:堆栈顶部通常是 View.measure、View.layout、View.draw 或 ViewGroup.dispatchDraw。如果深层次发现 RelativeLayout.onMeasure 反复调用,大概率是布局嵌套过深或权重计算复杂。
  2. 输入处理卡顿:堆栈顶部出现 View.onTouchEvent、ViewGroup.dispatchTouchEvent。我遇到过的一个案例:ListView的item点击事件里做了大图片解码,直接让触摸响应延迟了200ms。
  3. 消息处理卡顿:堆栈显示 Handler.handleMessage 或某个自定义Runnable的run()。这种往往是一个异步任务不小心在主线程执行了耗时操作。
关键判断准则:主线程堆栈里只要出现文件IO(FileInputStream/OutputStream)、网络(OkHttp call)、BitmapFactory.decode、数据库查询(SQLiteSession),基本就是卡顿元凶。

我曾经排查过一个启动白屏问题,ANR堆栈主线程一直卡在 SharedPreferencesImpl.getDiskReadLock。一看代码,某个第三方SDK在Application.onCreate里做了大量SP写入,还把写入放到了异步任务里——但读却走了主线程,导致死锁。这种问题不看堆栈根本猜不到。

如何读懂堆栈:三步走法

拿到一个堆栈,别急着看具体行号。我自己的流程是这样的:

  1. 确认线程身份:是不是主线程?如果是渲染卡顿,还得检查RenderThread。
  2. 定位顶部函数:栈顶三个函数是什么?是执行中的操作,还是在等待(比如Object.wait)?等待状态的堆栈通常有Lock、park、wait字样,说明被其他线程阻塞了。
  3. 回溯调用链:从栈顶往下看3~5层,找到业务代码入口(通常是有包名的类)。比如看到 com.myapp.MyFragment.onCreateView,我就知道是哪段代码出了问题。

举个例子。下面是一个真实的ANR堆栈片段:

"main" prio=5 tid=1 Blocked
  at java.lang.Object.wait(Native method)
  - waiting on <0x0c8f...> (a com.myapp.MyTask)
  at com.myapp.MainActivity$1.run(MainActivity.java:45)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:224)

看到Blocked、waiting on——主线程在等一个锁。锁对象是MyTask,说明另一个线程拿着这个锁没释放。这时候你要dump所有线程,找持有那个锁的线程,看它卡在哪里了。

注意:堆栈里的native方法(比如 Thread.sleep、Object.wait、MessageQueue.nativePollOnce)通常是正常的阻塞点,不需要惊慌。真正的问题在于这些阻塞点持续的时间远超预期。

为了帮大家快速建立整体认知,我画了一张堆栈知识体系图,把核心概念串起来:

Android 堆栈知识体系 调用栈结构 栈帧(Stack Frame) 返回地址 + 参数 局部变量 + 寄存器 PCB(线程控制块)引用 线程堆栈差异 主线程 → Looper.loop() Binder线程 → Binder.onTransact 异步线程 → 自定义Runnable RenderThread → 绘制流水线 主线程堆栈特征 UI绘制:measure/layout/draw 输入处理:onTouchEvent 消息处理:handleMessage 耗时IO/DB → 卡顿 三步骤读懂堆栈 ① 确认线程身份 ② 定位栈顶函数 ③ 回溯业务代码入口 区分等待 vs 执行中 ⚠️ 常见误解:Looper.loop() 就是卡顿?错!它只是休眠,要看它上面那条调用
我个人强烈建议:在开发阶段就开启 StrictMode,并把主线程写文件、读SP等操作记录下来。等堆栈出来再补救,成本往往高10倍。

最后留个小练习。下次遇到卡顿或ANR,打开日志把堆栈贴到文本编辑器里,对着上面三步走一遍。不出三次,你就能像我一样扫一眼就知道问题在哪。