4. 定义主线程卡顿判断规则:帧时长阈值、调度延迟标记、关键线程识别

主观“卡顿”在不同设备、不同应用上感知千差万别。要系统化排查,必须先建立可量化的判断规则。本章以 帧时长、调度延迟、关键线程 三个维度定义卡顿标准,并给出故障树和排查表,让团队统一卡顿判定口径。

核心观点:一次卡顿 = 帧时长超标 或 关键线程因调度延迟被阻塞 或 多个关键线程同时抢占CPU导致丢帧。

4.1 帧时长阈值(Frame Duration Threshold)

屏幕刷新周期决定了逻辑帧的“生死线”。不同目标帧率对应不同阈值:

目标帧率 (fps) 单帧预算 (ms) 轻度卡顿阈值 (ms) 严重卡顿阈值 (ms)
60 16.67 20 ~ 33 > 33
90 11.11 14 ~ 22 > 22
120 8.33 10 ~ 16 > 16

判断规则1: 在 Perfetto trace 中,取 actualFrameTimeline 或 FrameLifecycle 的 duration。只要单帧时长超过对应阈值的黄色区域(轻度卡顿),即标记为潜在卡顿帧;超过红色区域则为明确卡顿帧。

4.2 调度延迟标记(Scheduling Delay Markers)

即使帧时长未超标,关键线程被长时间就绪但未运行也会造成人眼可感知的停顿。调度延迟标记用于识别此类场景。

Perfetto 关键事件标记:
sched_wakeup + sched_blocked_reason + sched_switch 能精确定位线程被调度出去 → 再次运行的间隔。
间隔 > 5ms 且跨越 Vsync 边界 → 大概率产生掉帧。

判断规则2: 在 trace 中查询 ftrace/sched/sched_switch 事件,计算关键线程的就绪等待时间(state = Runnable → Running 的时间差)。若该值 > 5ms 且落在帧周期窗口内,则标记为 调度延迟卡顿。

-- 示例 SQL:从 Perfetto 中提取主线程调度延迟
SELECT
  ts - prev_ts AS wait_duration,
  utid
FROM (
  SELECT
    ts,
    LAG(ts) OVER (PARTITION BY utid ORDER BY ts) AS prev_ts,
    state
  FROM sched_switch
  WHERE utid = 1234   -- 主线程 utid
) WHERE state = 'R' AND prev_ts IS NOT NULL AND wait_duration > 5000000; -- 5ms

4.3 关键线程识别(Critical Thread Identification)

并非所有线程阻塞都会引发卡顿。只有影响UI渲染管道的线程才是关键线程。需要明确哪些线程应纳入监控。

线程角色 典型名称 作用 是否必须监控
主线程 (UI Thread) main (Android), UI Thread (Flutter) 事件分发、布局、绘制指令提交 是
渲染线程 (RenderThread) RenderThread, GpuThread GPU 命令编码、同步、Fence 等待 是
Binder 线程 Binder:XXXX_1, … 跨进程调用(窗口管理、输入转发) 视情况(当卡顿伴随 IPC 超时)
SurfaceFlinger SurfaceFlinger 图层合成,影响最终显示 需要
GPU 驱动线程 kgsl_worker, gpu_workqueue GPU 驱动调度 深层次排查时加入

判断规则3: 在 Perfetto 中创建 Thread Track 分组,手动或通过 ProcessSummary 标记这些线程。如果在 同一 Vsync 周期内 有两个或以上关键线程同时处于 Running → Runnable → Blocked 状态切换频发,视为多线程竞争卡顿。

4.4 综合判断故障树

将上述三个规则组合成故障树,在 trace 中自上而下诊断:

主线程卡顿判断故障树 Trace 中是否存在卡顿帧? 帧时长 > 阈值? 调度延迟 > 5ms? 主线程 CPU 繁忙 渲染线程等待 Gpu 大量耗时操作(GC、布局) Fence 等待 / GPU 帧堆积 关键线程被抢占 高优先级 GC / 系统服务抢跑 任何一个分支命中 → 标记为“卡顿帧” 故障树使用说明 1. 一条路径命中即可判定卡顿; 2. 多条路径同时满足时,取最早到达的根因; 3. 框架层主动丢弃帧(DropFrame)不归入此规则,单独处理。

4.5 排查清单与后续动作

获取 trace 后,按照以下顺序执行检查:

  1. 检查帧时长:查询 FrameTimeline jank_type 或手工计算相邻帧的 BufferProducer 时间戳差。
  2. 检查调度延迟:在 sched_switch 中定位 main / RenderThread 的 Runnable → Running 间隙。
  3. 检查关键线程冲突:观察 CPU utilization 视图,看是否多个关键线程在 同一时间段 争抢小核或大核。
⚠️ 常见误判:
① 帧时长虽超标但调度延迟 < 1ms → 根因大概率是主线程 同步耗时(如 I/O、锁等待)。
② 帧时长正常但调度延迟 > 10ms → 根因是 CPU 资源争抢(e.g. 前台服务、后台刷榜)。
③ 两个关键线程同时出现 Utask 块 → 检查是否为 死锁或优先级反转。

4.6 修复动作与复测

  • 帧时长超标:优化布局层次、延迟加载、使用 AsyncLayoutInflater;将耗时计算移出 UI 线程。
  • 调度延迟超标:降低非关键线程的优先级(android.os.Process.setThreadPriority);绑定大核(setAffinity)给 RenderThread。
  • 多线程冲突:检查是否 FragmentManager 在 RenderThread 上执行了 commitNow;使用 Choreographer 合并帧操作。

修复后重新采集 30 秒以上的 trace,覆盖滑动、动画、列表加载等场景。若连续 3 次 trace 中帧时长 < 16ms 且调度延迟 < 3ms,则视为修复通过。

4.7 预防:自动化卡顿规则引擎

在 CI/CD 或质量平台中集成如下规则:

// 伪代码:自动检测规则
if (frameDuration > 20ms) {
    markAs("Jank");
    checkSchedulingDelay();
}
if (thread_priority_inversion_detected) {
    report("Priority inversion");
}
if (two_critical_threads_runnable_in_same_period) {
    report("Thread contention");
}

规则引擎输出结构化的 JankReport,直接关联 Perfetto trace 的时间戳和线程名,供线下分析。