20、Android性能优化之卡顿优化:掉帧原理、Systrace与Profiler工具使用、主线程耗时任务排查
卡顿,是Android开发中最让人头疼的问题之一。用户可能不会在意你用了多酷炫的动画,但一旦滑动列表时“卡”了一下,他立马就会觉得这个App很“廉价”。
我刚开始做性能优化那会儿,也踩过不少坑。有一次线上版本反馈说某个页面滑动特别卡,我排查了两天才发现——居然是一个循环里做了Bitmap的频繁缩放。嗯,从那以后,我对主线程上的任何“重活”都特别敏感。
这一讲,我们就来彻底搞懂卡顿的本质,以及怎么用工具把它揪出来。
掉帧原理:为什么60fps是黄金标准?
先问一个问题:为什么我们总说“保持60fps”?
人眼对画面的流畅度有个感知阈值。当每秒刷新的画面数(fps)低于24时,你会觉得画面“一顿一顿”的。而60fps,也就是每16.6ms刷新一帧,人眼基本感觉不到卡顿。
Android系统的UI渲染是基于VSync(垂直同步)机制的。简单来说,系统每16.6ms发出一个VSync信号,通知CPU和GPU开始准备下一帧的数据。如果CPU或GPU在16.6ms内没干完活,这一帧就“掉”了,也就是我们常说的“掉帧”(Jank)。
核心公式:
掉帧 = 一帧的渲染耗时 > 16.6ms
掉帧越多,用户感知到的卡顿就越明显。
我个人的习惯是,把掉帧率控制在1%以内才算合格。超过3%,用户基本就会开始抱怨了。
Systrace:系统级的“黑匣子”
Systrace是Google官方提供的系统级性能分析工具。它能把CPU、GPU、线程、进程等所有活动都记录下来,生成一张时间线图。说白了,它就是Android系统的“黑匣子”。
我排查卡顿时,第一件事就是抓Systrace。它不会告诉你“哪里卡”,但会告诉你“哪个时间段卡”,以及“卡的时候CPU在干什么”。
怎么抓Systrace?
有两种方式:
- 命令行方式(推荐):
# 抓取5秒的trace,保存到/tmp/trace.html
python systrace.py -t 5 -o /tmp/trace.html
- Android Studio Profiler:直接点击CPU Profiler里的“Record”按钮,选择“Trace System Calls”即可。
我的经验:抓Systrace时,建议只抓3-5秒。抓太久文件会很大,而且分析起来也费劲。另外,记得在抓取前先“热身”一下App,让用户操作路径稳定下来。
怎么看Systrace?
打开生成的trace.html文件,你会看到一张密密麻麻的时间线图。别慌,我教你几个关键点:
- 看“Frames”行:每一帧用一个小圆点表示。绿色代表正常,黄色代表轻微掉帧,红色代表严重掉帧。
- 看“主线程”行:主线程上如果有大段的“Blocked”或“Sleeping”,说明有耗时任务在阻塞UI。
- 看“SurfaceFlinger”行:如果这里出现红色,说明GPU渲染超时了。
我曾经遇到过一个案例:Systrace显示主线程上有个“inflate”操作耗时200ms。点进去一看,原来是一个复杂的布局文件在反复解析。后来改成ViewStub懒加载,问题就解决了。
Profiler:Android Studio自带的“瑞士军刀”
如果说Systrace是系统级的,那Profiler就是应用级的。它集成在Android Studio里,可以实时查看CPU、内存、网络、能耗等数据。
我个人习惯用Profiler来做“定点排查”。比如我知道某个页面卡,就打开Profiler,在那个页面上反复操作,看CPU使用率有没有突然飙升。
CPU Profiler的使用步骤
- 打开Android Studio,连接设备或模拟器。
- 点击底部“Profiler”标签,选择你的App进程。
- 点击CPU时间线,选择“Trace System Calls”或“Sample Java Methods”。
- 点击“Record”开始录制,操作App,然后点击“Stop”。
- 分析生成的火焰图(Flame Chart)或调用树(Call Tree)。
火焰图怎么看?
火焰图是自底向上的。底部是调用链的起点,顶部是具体的函数。越宽的“火焰”代表该函数占用的CPU时间越多。如果你看到某个函数特别宽,那它就是卡顿的元凶。
主线程耗时任务排查:揪出“元凶”
卡顿的根源,90%以上是主线程干了不该干的活。主线程的任务包括:
- 处理用户输入(点击、滑动)
- 执行UI布局和绘制
- 执行四大组件的生命周期回调
- 执行Handler中的消息
如果你在主线程里做网络请求、大文件读写、Bitmap解码、JSON解析……那卡顿几乎是必然的。
常见的“主线程杀手”
| 操作类型 | 典型场景 | 解决方案 |
|---|---|---|
| 网络请求 | 在onClick里直接调用HttpURLConnection | 使用协程/线程池/OkHttp异步 |
| Bitmap解码 | 在getView里解码大图 | 使用Glide/Picasso,或提前压缩 |
| JSON解析 | 在主线程解析大JSON | 使用Gson/Jackson异步解析 |
| 布局inflate | 在列表滑动时动态inflate复杂布局 | 使用ViewStub/RecyclerView复用 |
| 文件IO | 在主线程读写SharedPreferences | 使用异步IO或MMKV |
注意:SharedPreferences的apply()虽然是异步写磁盘,但第一次加载时会在主线程同步读文件。如果文件很大,也会卡。我建议用MMKV替代,性能好很多。
排查步骤总结
当你遇到卡顿问题时,按这个顺序排查:
- 复现问题:确定卡顿的操作路径。
- 抓Systrace:看掉帧发生在哪个时间段,主线程在干什么。
- 用Profiler定位:找到具体哪个函数耗时最长。
- 分析代码:看那个函数里有没有在主线程做耗时操作。
- 修复并验证:把耗时操作移到子线程,再抓一次Systrace确认掉帧消失。
嗯,这套流程我用了好几年,基本没失手过。你想想看,卡顿优化其实没那么玄乎,核心就是一句话:别让主线程闲着,也别让它干重活。
避坑指南:我曾经在某个项目里,发现一个列表滑动卡顿,查了半天发现是onBindViewHolder里调了Log.d()。虽然Log本身不耗时,但字符串拼接("xxx" + data)在循环里会频繁创建对象,触发GC。所以,生产环境记得关掉冗余日志。
好了,这一讲的内容就到这里。记住,工具只是辅助,关键是你要有“主线程敏感”的意识。下次写代码时,多问自己一句:这个操作能放到子线程吗?
公众号:蓝海资料掘金营,微信deep3321