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 主线程阻塞排查流程图

下面这张图是我自己总结的排查路径,从现象到根因,一步步往下走:

主线程阻塞排查流程图 主线程阻塞排查流程图 触摸响应延迟 ① 使用 Systrace/Perfetto 抓取 trace 主线程有 耗时操作? 是 ② 定位具体方法 ③ 分析操作类型 ④ 子线程/优化方案 否 检查渲染管线 检查 GPU 过度绘制 ⑤ 验证修复效果 延迟降低,问题解决 提示:如果 trace 上找不到明显耗时操作,可以开启 StrictMode 做持续监控,捕获偶现的违规调用

3.5 实战:一次真实的排查过程

我拿一个实际案例来说吧。之前有个项目,用户反馈在列表页快速滑动时,触摸响应有明显延迟。我一开始怀疑是列表的 Item 布局太复杂,但优化布局后问题依旧。

然后我抓了 Systrace,发现主线程上每隔几百毫秒就出现一个 80ms 左右的红色块。点进去一看,是 DiskLruCache.get() 方法。原来图片缓存框架在主线程做了磁盘读取。

解决方案很简单:把图片的磁盘缓存读取放到子线程,或者使用内存缓存做一级缓存。改完之后,再抓 trace,主线程上的红色块消失了,触摸延迟从 120ms 降到了 30ms 以内。

我的建议:每个项目在开发阶段,都应该把 StrictMode 开起来。不要等到线上用户反馈卡顿了再排查。提前发现一个主线程违规,可能就避免了未来几百个用户的差评。

3.6 总结一下排查要点

  • 先抓 trace,再猜原因。 不要凭感觉优化,用数据说话。
  • Systrace 看宏观,Perfetto 看细节。 老设备用 Systrace,新设备用 Perfetto。
  • StrictMode 是守门员。 开发阶段就拦住主线程违规操作。
  • 布局、绘制、IO、解码、GC 是主线程阻塞的五大元凶。

嗯,主线程阻塞这块就聊到这儿。工具和方法都给你了,关键是养成习惯——每次写完代码,跑一遍 trace 看一眼,心里踏实。