3. 主线程阻塞排查:定位并修复触摸延迟的根源
触摸响应延迟,十有八九是主线程被堵住了。你想想看,用户手指点下去,系统要把触摸事件交给应用,应用要在主线程上处理、布局、绘制,最后才显示到屏幕上。这一路但凡有个环节卡顿,延迟就上来了。
我个人习惯,排查触摸延迟的第一步,不是看代码,而是先确认「主线程到底在忙什么」。说白了,就是找到那个占着茅坑不拉屎的耗时操作。
核心原则:主线程只做 UI 相关的事。任何 IO、网络、复杂计算、大文件读写,都不该出现在主线程上。
3.1 主线程耗时操作的常见类型
我在项目中遇到过好几次,明明代码写得挺干净,触摸就是卡。后来一查,问题都出在下面这几类操作上:
| 操作类型 | 典型场景 | 影响 |
|---|---|---|
| 布局(Layout) | 复杂嵌套的 RelativeLayout、多次调用 requestLayout() | 触发整棵树重新测量、布局,耗时随层级指数增长 |
| 绘制(Draw) | 过度绘制、自定义 View 的 onDraw 中创建对象 | GPU 渲染管线阻塞,掉帧明显 |
| IO 操作 | 在主线程读 SharedPreferences、读数据库、读文件 | 磁盘速度远慢于 CPU,几十毫秒的卡顿很常见 |
| Bitmap 解码 | 直接在主线程 decodeResource() 加载大图 | 一张 1080p 的图解码可能耗时 50-200ms |
| 对象创建与 GC | 频繁在触摸事件中 new 对象,触发 GC | GC 暂停会导致 8-16ms 的抖动 |
注意:很多人以为只有网络请求才算耗时操作。其实一次 SharedPreferences 的 commit(),在写入量大的时候也能卡住主线程 100ms 以上。我曾经在一个项目里发现,某个页面启动时读了一个 200KB 的 JSON 配置文件,直接导致首帧触摸无响应。
3.2 使用 Systrace / Perfetto 定位卡顿
光靠猜是不行的。你得用工具看,到底哪一段代码占了主线程的时间。我个人最常用的就是 Systrace,新项目我推荐直接用 Perfetto,功能更强。
3.2.1 Systrace 快速上手
Systrace 的原理很简单:它在系统层面打标记,记录每个线程在做什么。你只需要跑一下命令,然后操作手机,就能看到完整的时序图。
# 抓取 5 秒的 trace,重点关注 gfx 和 input
python systrace.py -t 5 -o trace.html gfx input view sched freq
抓完之后打开 trace.html,你会看到这样的界面:
- 最上面是时间轴,每一行代表一个线程
- 主线程(通常是包名) 下面有各种色块,代表不同操作
- 橙色/红色色块 就是卡顿点,鼠标点上去能看到具体方法名
我一般先看「Frame」这一行。如果某个帧的渲染时间超过 16ms,它就会变成红色。然后我顺着红色帧往下找,看主线程在那个时间段里执行了什么方法。
小技巧:在 Systrace 里按 "M" 键可以添加标记线,方便对比多个帧之间的差异。我习惯在触摸事件发生前后各打一条线,这样能精确看到从输入到渲染的完整链路。
3.2.2 Perfetto 更现代的方案
Perfetto 是 Systrace 的升级版,支持更长的抓取时间、更大的数据量,而且界面更友好。Android 10 以上的设备推荐直接用。
# 使用 adb 直接抓取
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 10s sched freq gfx input view
抓完后拉到电脑上,打开 https://ui.perfetto.dev 就能分析。Perfetto 的搜索功能很强,你可以直接搜方法名、线程名,甚至搜某个字符串。
我记得有一次排查一个诡异的触摸延迟,Systrace 上怎么都看不出问题。后来用 Perfetto 的「SQL 分析模式」,直接查了主线程上所有超过 10ms 的 slice,才发现是一个第三方 SDK 在后台偷偷做了加密计算。
3.3 使用 StrictMode 检测主线程违规
Systrace 和 Perfetto 是事后分析,但更聪明的做法是「事前预防」。StrictMode 就是 Android 提供的一个开发工具,它能在运行时检测主线程上的违规操作,然后直接弹警告或者崩溃。
3.3.1 开启 StrictMode
我建议在 Application 的 onCreate 里就开启,而且 debug 模式下要开最严格的策略。
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 检测主线程读磁盘
.detectDiskWrites() // 检测主线程写磁盘
.detectNetwork() // 检测主线程网络请求
.detectCustomSlowCalls() // 检测自定义慢调用
.penaltyLog() // 打印日志
.penaltyFlashScreen() // 屏幕闪烁提醒(Android 9+)
.build());
}
}
}
3.3.2 看懂 StrictMode 的输出
开启之后,一旦主线程出现违规操作,Logcat 里会打印类似这样的信息:
StrictMode: StrictMode policy violation; ~duration=32 ms: android.os.StrictMode$StrictModeDiskReadViolation
StrictMode: at com.example.app.MainActivity.onCreate(MainActivity.java:45)
StrictMode: at android.app.Activity.performCreate(Activity.java:8000)
看到这个,你就知道第 45 行有个磁盘读操作,耗时 32ms。32ms 看起来不多,但你要知道,一帧只有 16ms。这一个操作就吞掉了两帧的时间。
注意:StrictMode 只在 debug 版本生效,release 版本一定要关掉。不然用户手机上频繁弹崩溃或者闪屏,体验极差。我见过有团队把 StrictMode 开到了线上,结果用户反馈「手机屏幕一直闪」,最后发现是 penaltyFlashScreen 搞的鬼。
3.4 主线程阻塞排查流程图
下面这张图是我自己总结的排查路径,从现象到根因,一步步往下走:
3.5 实战:一次真实的排查过程
我拿一个实际案例来说吧。之前有个项目,用户反馈在列表页快速滑动时,触摸响应有明显延迟。我一开始怀疑是列表的 Item 布局太复杂,但优化布局后问题依旧。
然后我抓了 Systrace,发现主线程上每隔几百毫秒就出现一个 80ms 左右的红色块。点进去一看,是 DiskLruCache.get() 方法。原来图片缓存框架在主线程做了磁盘读取。
解决方案很简单:把图片的磁盘缓存读取放到子线程,或者使用内存缓存做一级缓存。改完之后,再抓 trace,主线程上的红色块消失了,触摸延迟从 120ms 降到了 30ms 以内。
我的建议:每个项目在开发阶段,都应该把 StrictMode 开起来。不要等到线上用户反馈卡顿了再排查。提前发现一个主线程违规,可能就避免了未来几百个用户的差评。
3.6 总结一下排查要点
- 先抓 trace,再猜原因。 不要凭感觉优化,用数据说话。
- Systrace 看宏观,Perfetto 看细节。 老设备用 Systrace,新设备用 Perfetto。
- StrictMode 是守门员。 开发阶段就拦住主线程违规操作。
- 布局、绘制、IO、解码、GC 是主线程阻塞的五大元凶。
嗯,主线程阻塞这块就聊到这儿。工具和方法都给你了,关键是养成习惯——每次写完代码,跑一遍 trace 看一眼,心里踏实。