3、冻屏问题根因分析:Input系统阻塞、VSYNC信号异常、App ANR 关联

冻屏,说白了就是手机突然“僵住”了。屏幕画面卡死,点哪都没反应。用户的第一反应往往是“死机了”。但作为工程师,我们得知道——冻屏和黑屏不一样,屏幕还亮着,只是系统不响应了。

我个人习惯把冻屏问题分成三类来排查:Input系统阻塞VSYNC信号异常App ANR关联。这三者经常纠缠在一起,但根因往往只有一个。

3.1 Input系统阻塞——触控事件的“交通堵塞”

先说说Input系统。Android的触摸事件从硬件到应用,要经过一个很长的链路:

触摸屏 → 驱动 → InputReader → InputDispatcher → 目标窗口 → App

任何一个环节卡住,都会导致冻屏。我在项目中遇到过最典型的情况——InputDispatcher的“饥饿”问题

为什么会这样?

InputDispatcher内部有一个等待队列。当App的主线程被长时间占用(比如在做GC或者布局计算),InputDispatcher会等待App处理完上一个事件,才能发送下一个。如果App一直不响应,InputDispatcher就会超时,然后抛出ANR。

但冻屏往往发生在ANR之前。嗯,这里要注意:

关键点:InputDispatcher的超时阈值是5秒。但用户可能在1-2秒内就感觉到卡顿。所以冻屏的根因排查,不能只盯着ANR日志。

我曾经排查过一个案例:某款手机在低电量时频繁冻屏。抓取systrace后发现,InputReader线程被频繁block住,原因是触摸屏驱动的中断处理耗时过长。驱动在低电量模式下进入了某种“省电轮询”状态,导致事件上报延迟。

排查Input阻塞,我建议重点关注这几个地方:

  • dumpsys input:查看InputDispatcher的等待队列长度
  • systrace中的InputReader/InputDispatcher:看是否有长时间不调度
  • logcat中的“Input event delivery timed out”:这是最直接的信号

3.2 VSYNC信号异常——画面更新的“心跳”停了

VSYNC是Android渲染的节拍器。SurfaceFlinger和App都依赖VSYNC信号来同步渲染。如果VSYNC信号异常,画面就会停止更新——这就是冻屏的另一种表现。

VSYNC信号的来源有两个:

信号源 说明 常见问题
硬件VSYNC 由显示控制器(Display Controller)产生 驱动bug、帧率切换时信号丢失
软件VSYNC 由SurfaceFlinger的EventThread模拟 线程优先级被抢占、CPU负载过高

我记得有一次排查一个冻屏问题,现象很诡异:手机在滑动桌面时突然卡住,但过几秒又自己恢复了。抓取trace后发现,SurfaceFlinger的EventThread在某个时刻停止了发送VSYNC信号

根因是什么?

是某个第三方App注册了一个Choreographer.FrameCallback,但回调里做了耗时操作(网络请求)。这个回调运行在EventThread的同一个线程上,导致EventThread被阻塞。说白了,就是“一个老鼠屎坏了一锅粥”。

排查技巧:在systrace中搜索“VSYNC”关键字,看信号是否连续。如果出现长时间空白,说明VSYNC信号中断了。再结合线程状态,看是哪个线程占用了EventThread。

3.3 App ANR关联——冻屏的“前奏曲”

冻屏和ANR的关系,就像感冒和发烧。冻屏往往是ANR的前兆,但并不是所有冻屏都会演变成ANR。

从系统角度看,ANR的触发条件很明确:

  • Input事件5秒内未处理
  • BroadcastReceiver 10秒内未处理
  • Service 20秒内未处理

但冻屏的触发条件更模糊。用户觉得“卡住了”,可能只是某个帧的渲染超过了16ms,导致画面停顿。这种停顿如果持续几秒,用户就会认为是冻屏。

我个人的经验是:冻屏问题中,有60%以上最终会触发ANR。所以排查冻屏时,一定要同时看ANR日志。

举个例子:

// 典型的ANR日志片段
ANR in com.example.app (com.example.app/.MainActivity)
  Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago)
  Load: 8.5 / 6.2 / 4.1
  CPU usage from 0ms to 5000ms:
    100% 12345/com.example.app: 99% user + 1% kernel
    // 主线程堆栈
    "main" prio=5 tid=1 Blocked
      at com.example.app.MainActivity.onClick(MainActivity.java:45)
      - waiting to lock <0x0c4d> (a java.lang.Object)
      at com.example.app.SyncManager.syncData(SyncManager.java:120)
      - locked <0x0c4d> (a java.lang.Object)

看到没?主线程在等待一个锁,而这个锁被另一个线程持有。这就是典型的死锁导致的冻屏。用户点击后,Input事件无法被处理,画面卡死。5秒后,系统弹出ANR对话框。

核心结论:冻屏的根因,90%以上可以归结为“主线程被阻塞”。无论是Input阻塞、VSYNC异常还是ANR,最终都是主线程(UI线程)无法正常工作。

3.4 三者之间的关联与排查思路

这三者不是孤立的。我画个简单的关联图:

App主线程阻塞
    ↓
Input事件无法分发 → 用户触摸无响应 → 冻屏
    ↓
VSYNC信号无法消费 → 画面不更新 → 冻屏
    ↓
5秒超时 → ANR弹出 → 用户看到“应用无响应”

所以排查冻屏时,我的思路是这样的:

  1. 先看ANR日志:如果有ANR,直接定位主线程堆栈
  2. 再看systrace:看InputDispatcher和SurfaceFlinger的线程状态
  3. 最后看dumpsys:确认Input队列和VSYNC状态

我曾经遇到一个案例,冻屏持续了3秒就恢复了,没有触发ANR。抓trace后发现,是CPU频率被限制导致渲染超时。系统在高温时降频,但降得太猛,导致App无法在16ms内完成渲染。画面卡了几帧,用户就觉得冻屏了。

这种问题,说白了就是温控策略太激进。调整一下CPU调频参数就解决了。

避坑指南:不要一看到冻屏就怀疑代码bug。有时候是系统服务的问题,比如SurfaceFlinger的优先级被降低、或者CPU的big核被关掉。我曾经花了一周排查一个冻屏问题,最后发现是内核的cpufreq governor配置错误。

总结一下:冻屏的根因分析,核心是找到“谁阻塞了主线程”。Input系统阻塞、VSYNC信号异常、App ANR,这三者只是表象。真正的根因,往往藏在主线程的堆栈里,或者系统服务的调度策略里。