堆栈基础知识:读懂卡顿的第一现场
做性能优化,说白了就是跟堆栈打交道。我刚开始接触Android时,看到一大串地址和函数名就头大——这玩意谁能看懂?后来被线上卡顿问题搞了几次,才明白堆栈就是我们定位问题的第一现场。这一章,咱们就聊聊堆栈那点事儿。
调用栈结构:函数调用的“记账本”
调用栈(Call Stack)是程序执行时的内部记录。每调用一个方法,系统就把当前方法的返回地址、参数、局部变量压到一个后进先出的栈里。方法执行完,再弹出来。
每次函数调用都会产生一个栈帧(Stack Frame)。它在栈上长这样:
| 栈帧组成部分 | 说明 |
|---|---|
| 返回地址 | 调用者的下一条指令地址 |
| 参数 | 传入的参数值(部分可能通过寄存器传递) |
| 局部变量 | 方法内定义的变量 |
| 保存的寄存器 | 调用者寄存器的备份 |
举个例子。你在Activity的onCreate里调了initViews,initViews里又调了findViewById。堆栈从底部往上看就是:
main -> ActivityThread.performLaunchActivity -> Activity.onCreate -> initViews -> findViewById
底层函数在栈顶,调用链在下面。我们看卡顿堆栈,通常关注的就是最上面的几个函数——它们正在“忙”什么。
线程堆栈差异:每个线程都有自己的“账本”
每个线程都有自己的调用栈,互不干扰。但不同线程的堆栈特征差别很大。
| 线程类型 | 堆栈特征 | 常见卡顿原因 |
|---|---|---|
| 主线程(main) | 通常深度较浅,频繁回到Looper.loop | 布局测量、绘制、反射、文件IO、网络 |
| Binder线程(binder_1/2…) | 常见Binder transaction、onTransact调用 | 系统服务耗时、跨进程调用阻塞 |
| 异步线程(AsyncTask、Thread) | 深度可能很深,或者卡在某个循环/锁上 | 死循环、锁竞争、大数据量处理 |
| RenderThread | 处于Choreographer或RenderPipeline中 | GPU等待、vsync延迟、绘制指令过多 |
注意一个坑:很多人看到主线程堆栈卡在Looper.loop()就以为没事。其实loop()是主线程的正常睡眠点——卡住是因为其他函数还没返回。你需要看堆栈里紧挨着Looper.loop上面那个函数。
主线程堆栈特征:一眼认出的三个模式
主线程堆栈有几个典型模式,我总结成三类:
- UI绘制卡顿:堆栈顶部通常是
View.measure、View.layout、View.draw或ViewGroup.dispatchDraw。如果深层次发现RelativeLayout.onMeasure反复调用,大概率是布局嵌套过深或权重计算复杂。 - 输入处理卡顿:堆栈顶部出现
View.onTouchEvent、ViewGroup.dispatchTouchEvent。我遇到过的一个案例:ListView的item点击事件里做了大图片解码,直接让触摸响应延迟了200ms。 - 消息处理卡顿:堆栈显示
Handler.handleMessage或某个自定义Runnable的run()。这种往往是一个异步任务不小心在主线程执行了耗时操作。
我曾经排查过一个启动白屏问题,ANR堆栈主线程一直卡在 SharedPreferencesImpl.getDiskReadLock。一看代码,某个第三方SDK在Application.onCreate里做了大量SP写入,还把写入放到了异步任务里——但读却走了主线程,导致死锁。这种问题不看堆栈根本猜不到。
如何读懂堆栈:三步走法
拿到一个堆栈,别急着看具体行号。我自己的流程是这样的:
- 确认线程身份:是不是主线程?如果是渲染卡顿,还得检查RenderThread。
- 定位顶部函数:栈顶三个函数是什么?是执行中的操作,还是在等待(比如Object.wait)?等待状态的堆栈通常有Lock、park、wait字样,说明被其他线程阻塞了。
- 回溯调用链:从栈顶往下看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所有线程,找持有那个锁的线程,看它卡在哪里了。
Thread.sleep、Object.wait、MessageQueue.nativePollOnce)通常是正常的阻塞点,不需要惊慌。真正的问题在于这些阻塞点持续的时间远超预期。为了帮大家快速建立整体认知,我画了一张堆栈知识体系图,把核心概念串起来:
StrictMode,并把主线程写文件、读SP等操作记录下来。等堆栈出来再补救,成本往往高10倍。最后留个小练习。下次遇到卡顿或ANR,打开日志把堆栈贴到文本编辑器里,对着上面三步走一遍。不出三次,你就能像我一样扫一眼就知道问题在哪。