Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

卡顿分析方法、典型场景与案例

我们从可复现的场景和问题窗口入手,用 FrameTimeline(逐帧时间线) 锁定异常帧,再结合线程状态、调度、GPU 和系统合成证据建立因果关系。案例在这里只用来演示证据怎样收敛,结论要靠现场数据。

源码基线:AOSP android-17.0.0_r1;内核 android17-6.18-2026-06_r6。

⚠️ 校勘(2026-10-23):旧文把平台返回动画回调标成 API 33,并声称 AndroidX 没有进度入口;已按平台 API 参考和 AndroidX API 参考更正为 API 34、Activity 1.8.0。下文同时区分指标时长、实际执行、帧完成与显示呈现。

从异常帧到责任阶段

分析目标:把异常帧变成可复核因果链

这一节要解决的问题是:拿到 trace 之后,怎样把观察写成别人可以复核的结论。只写“主线程有一个长 slice”或“CPU 频率较低”,别人复核不了;要把结论写扎实,就得记录复现场景、目标 SurfaceFrame(某个 Surface 的一帧) 与 DisplayFrame(一次显示合成帧)、责任落在哪个阶段、关键线程或 fence(同步栅栏),以及修复后的同场景对照。

我们按下面的顺序收集和使用证据:

  1. 复现并固定环境;
  2. 检查 trace(跟踪记录) 是否已包含所需 probe(采集探针);
  3. 定位异常 SurfaceFrame 和对应 DisplayFrame(整屏显示帧记录);
  4. 按 JankType 选择 App、SurfaceFlinger、HWC(Hardware Composer,硬件合成器)/display(显示末端) 或 buffer(图形缓冲区) 路径;
  5. 在帧窗口内分析线程状态、Binder、GC(垃圾回收)、I/O、GPU 与 fence;
  6. 用相同设备状态和操作脚本验证修复。

每一步都要记下帧 token(标识)、进程、线程、layer 和时间区间;界面里位置相邻的 slice 可能属于后台事件,凭位置归组容易误归因。

抓取前先固定现场

进入采集之前,我们先固定现场。复现记录至少要包含:设备与 build、应用版本、刷新率/显示模式、温控状态、是否充电、前后台状态、页面与手势脚本、复现次数,以及首次运行还是稳态运行。涉及 SurfaceView、WebView、Camera、Video、Flutter、游戏或 Native Graphics 时,还要记录 Producer(图形内容生产者)、Surface 与 layer 结构。

“竞品不卡”是一条比较线索:两个应用可能使用不同的刷新率请求、Surface 结构、解码路径和画质,设备或系统瓶颈要先排除。问题只在低端设备上复现时,我们先把计算量、内存带宽、GPU、I/O 与温控因素分开看,再判断是不是 CPU 调度的问题。

采集时长由复现概率决定。稳定复现适合短 trace,以减少无关数据;偶发问题适合使用 ring buffer(环形缓冲区)、触发式停止或多轮采样。trace buffer(跟踪缓冲区) 的大小要覆盖目标窗口,采集结束还要检查有没有丢数据。

基础 Perfetto 配置

下面的配置用于开发和测试环境中的标准 HWUI(Android 硬件加速 UI 渲染器) 卡顿采集,包名、时长和 trace buffer 大小应按现场调整。

buffers: {
  size_kb: 65536
  fill_policy: RING_BUFFER
}

data_sources: {
  config {
    name: "linux.ftrace"
    target_buffer: 0
    ftrace_config {
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "sched/sched_blocked_reason"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "binder/binder_transaction"
      ftrace_events: "binder/binder_transaction_received"
      ftrace_events: "binder/binder_return"

      atrace_categories: "gfx"
      atrace_categories: "view"
      atrace_categories: "input"
      atrace_categories: "wm"
      atrace_categories: "am"
      atrace_categories: "dalvik"
      atrace_apps: "com.example.app"
    }
  }
}

data_sources: {
  config {
    name: "android.surfaceflinger.frametimeline"
    target_buffer: 0
  }
}

duration_ms: 15000

该配置提供 FrameTimeline、应用/系统 atrace(Android 跟踪标记)、调度、blocked reason(阻塞原因)、CPU 频率和 Binder 基线。GPU render stages(GPU 渲染阶段)、memory counters(内存计数器)、heapprofd(堆分析数据源)、thermal 或厂商图形数据源,需要根据设备支持情况和问题类型追加;一次加入所有高频 probe 会增加采集开销并缩短有效窗口。

采集结束后要检查 traced / traced_probes 错误、数据源是否启动、目标 App 是否出现在 atrace 中、FrameTimeline 表是否有记录,以及 trace buffer 是否发生 overwrite(旧数据被覆盖)。某项数据缺失时,先检查数据源支持、采集配置、目标时间窗口、过滤条件,以及该路径是否在工具覆盖范围内;现场也可能没有触发相应事件。空轨道只说明这一列没有数据;结论里应写清哪些证据暂时拿不到,以及下一步补采什么。

帧级定位:Expected、Actual 与两类 token(帧标识)

在 Perfetto 里圈定异常帧,Android 12 / API 31 起的标准入口是 FrameTimeline。应用的 SurfaceFrame 和 SurfaceFlinger 的 DisplayFrame 各有一对 expected/actual timeline(预期/实际时间线);Perfetto 通过 flow(跨轨道关联线) 把两者关联起来。surface_frame_token 与 display_frame_token 是不同标识,一个 DisplayFrame 可以合成多个 layer frame。

选择应用 Actual Timeline slice 后,依次记录:

  • Layer Name 与 Is Buffer;
  • surface_frame_token 与 display_frame_token;
  • On time finish、Present Type、Jank Type;
  • 对应的 expected window(预期时间窗口);
  • flow 指向的 DisplayFrame,以及 SF(SurfaceFlinger) 侧的 GPU composition(GPU 合成) 与 present(呈现) 状态。

颜色只用于导航。红色、黄色或浅绿色都只是界面提示,我们读取的是字段本身:JankType 是位标志,同一帧可能有多个原因。分析 BufferStuffing、Dropped、PredictionError 和显示模式切换时,还要读取相邻帧。

SurfaceView、视频 overlay(硬件叠加层)、Camera 和某些引擎路径可能没有完整的 App FrameTimeline。这时我们改从目标 layer、buffer update、acquire fence(等待生产完成的同步栅栏)、SF latch(锁定本帧 buffer)、composition type(合成类型)、present fence(显示呈现栅栏)与 release fence(buffer 可复用栅栏)组织证据。这几个对象各管一段:acquire fence(缓冲区可读栅栏)等的是生产完成,SF latch 把本帧 buffer 锁定下来。present fence 按 HWC 接口定义,常见物理屏上对应本帧开始扫描显示、或开始向面板内存传送的 VSync,全屏扫描在这之后才结束;虚拟显示则对应输出 buffer 写完。release fence 表示相应消费者已读完 buffer,随后才可安全重写,实际仍要遵守缓冲队列的所有权规则。接口语义见 HWC 基线头文件。出图类型识别参见 渲染管线总览。

从帧回到线程与系统

帧记录把责任指向某个阶段之后,我们回到线程和系统侧收集证据:先看应用侧的 MainThread 与 RenderThread,再看 SurfaceFlinger 与显示末端,最后是 CPU 调度与等待状态。

MainThread 与 RenderThread(渲染线程)

Choreographer#doFrame 是应用帧调度入口,常见 callback(回调) 顺序为 Input(输入)、Animation(动画)、Insets Animation(系统栏等区域的动画)、Traversal(测量、布局和绘制遍历)、Commit(提交),其中 Traversal 覆盖测量、布局和绘制,Commit 处理 traversal 之后的 post-draw 回调,并可能校正回调可见的 frame time,是应用侧帧工作的收尾阶段。GPU 提交与显示 present 是否完成,要另外确认。DrawFrame 位于 RenderThread,覆盖 HWUI 的渲染准备与提交工作。这两个 slice 适合作为回溯的起点,帧的完成与呈现结论仍要看 FrameTimeline。

doFrame、DrawFrame 与帧完成边界

图:Commit 是 traversal 后的回调阶段;GPU 完成、App actual 结束和显示 present 要从各自的轨道或 fence 判断。主线程、RenderThread 与 GPU 可以流水重叠。

主线程 doFrame 开始较晚时,我们先检查前序 Looper(消息循环) 消息、同步 Binder、锁、I/O 和 wakeup(唤醒) latency(唤醒延迟)。doFrame 内部耗时较长时,再分别检查 callback 与业务 slice。MainThread 在 syncFrameState 附近等待,可能是与 RenderThread 的预期同步,也可能被前一帧、纹理上传或 buffer backpressure(缓冲区背压) 放大;这时沿 wait 的唤醒源继续追。

RenderThread 的 DrawFrame 较长时,我们先分清时间去了哪里:Running(运行中) 是正在执行,Runnable(可运行但未获得 CPU) 是可运行但还没拿到 CPU,buffer/fence wait(等待) 是在等缓冲或栅栏,GPU completion(GPU 完成时间) 要看 GPU 侧何时完成。DrawFrame 的 slice 长度是该记录区间的 wall time,实际 CPU 执行还要从 Running 区间统计。整帧与 GPU 是否按时完成,要核对 FrameTimeline 的 finish、GPU track(轨道)与 acquire fence。

SurfaceFlinger 与显示末端

遇到 SurfaceFlingerCpuDeadlineMissed,我们检查 SF 主线程、transaction(图层状态事务)、layer snapshot(图层快照)、composition strategy(合成策略) 和 HWC validate(能力校验)/present(验证合成方案/提交显示)。遇到 SurfaceFlingerGpuDeadlineMissed(SF GPU 合成错过截止时间),检查 RenderEngine、client target(客户端合成目标) 和 GPU fence(同步栅栏)。遇到 DisplayHAL,继续检查 Composer HAL、present fence、显示模式与驱动。

onMessageReceived 是旧版和底层 trace 中常见的 SF 工作入口,但 slice 名称、拆分方式和厂商插桩都会变化。现代分析优先依赖 DisplayFrame、flow 与明确的 SF/HWC 事件;SF 有没有做合成,以 DisplayFrame 与 flow 为准,slice 名称只作参考。

CPU 调度与等待状态

线程状态要放回 Linux 调度语义里解释,我们逐个状态给出对应的动作(Perfetto 调度文档、基线状态编码):

状态含义分析动作
Running正在某个 CPU 上执行检查调用栈、slice、核心与频率
Runnable (R)可运行但尚未获得 CPU量化 wakeup-to-running(从唤醒到运行)的时长,检查竞争者与调度组
Sleeping (S)可中断等待查找等待对象、Binder flow、锁 owner(持有者)或唤醒源
Uninterruptible Sleep (D)内核不可中断等待结合 io_wait、blocked_function 与内核事件

Runnable(可运行但尚未获得 CPU) duration(持续时间)量的是等待 CPU 的时间,要测它就用 thread_state 的 Runnable 区间,必要时结合 sched_waking 的唤醒关系;前一条 sched_slice 的 dur 量的是上一次在 CPU 上的运行。sched_slice.end_state = R 或 R+ 表示线程离开 CPU 时仍可运行,适合解释它为何被切出,后续的等待时长则回到 Runnable 区间读。

区间耗时如何拆成运行与等待

图:统计线程 Running 区间才得到 CPU 执行;Runnable 区间反映等待调度,D/S 需要继续找等待对象。sched_slice.end_state 说明的是线程离开 CPU 时的状态;后续等待了多久,要回到对应状态区间去读。

D 状态的范围比存储 I/O 宽:fence、驱动、页错误和其他内核 wait queue(等待队列) 都可能形成不可中断等待。具体在等什么资源,要有 blocked_function、io_wait 与相邻事件共同支持才写得上去。

FrameMetrics 与 JankStats(应用内卡顿监测库) 的线上角色

Window.OnFrameMetricsAvailableListener 从 API 24 起提供 HWUI 窗口的逐帧指标。listener(监听器) 会在指定 Handler(消息处理器) 上回调,处理过慢会产生 dropCountSinceLastInvocation;这个 drop count 表示指标回调丢失,和显示帧被 SurfaceFlinger 丢弃是两回事。跨线程保存数据时应复制 FrameMetrics,避免后续回调复用同一对象带来数据变化。

指标API 边界解释
INPUT_HANDLING_DURATION、ANIMATION_DURATION、LAYOUT_MEASURE_DURATION、DRAW_DURATIONAPI 24App UI / View 阶段耗时
SYNC_DURATION、COMMAND_ISSUE_DURATION、SWAP_BUFFERS_DURATION、TOTAL_DURATIONAPI 24HWUI 同步、提交和总时长
INTENDED_VSYNC_TIMESTAMP、VSYNC_TIMESTAMPAPI 26计划与采用的 VSync 时间
GPU_DURATION、DEADLINEAPI 31GPU 工作时长与该应用帧获分配的时长预算(单位均为 ns)
FRAME_TIMELINE_VSYNC_IDAPI 36把 HWUI frame 与 compositor timeline(合成器时间线)关联

TOTAL_DURATION 里部分阶段可能并行执行,各阶段耗时简单相加未必等于总数。API 31 及更高版本可以用 TOTAL_DURATION < DEADLINE 判断应用是否满足该帧预算;这仍是 HWUI 窗口视角,独立 Surface 的像素生产和显示末端要另找证据。

JankStats 在 FrameMetrics 和平台计时数据上增加启发式 jank 判断和 UI state(界面状态)。它适合回答“哪个页面或交互经常出问题”;Perfetto 用于解释具体帧的跨进程原因。线上数据按设备、刷新率、场景和版本聚合,不同 API 级别的精度要分开对待。

第三方应用的 Perfetto SDK 主要提供进程内 Track Event(轨道事件) 和受支持的 tracing session(跟踪会话);要读取任意 ftrace(Linux 内核跟踪)、SurfaceFlinger 或其他进程的数据,需要平台权限。开发环境可以通过 adb/Android Studio 采集系统 trace;生产环境要使用平台允许的 profiling API(性能分析接口)、设备策略或应用内指标,并清楚记录缺少哪些数据源。

Perfetto SQL:从表结构开始

这一节的 SQL 查询都运行在 Trace Processor(跟踪数据处理器)上:它导入并分析 trace;SQL 模块和表结构取决于分析时使用的 Trace Processor 版本,不由采集设备的 Android 版本单独决定。AOSP(Android 开源项目) 参考实现位于 platform/external/perfetto 的 android.frames.timeline、android.binder 与 android.binder_breakdown 模块。字段或模块不存在时,先核对查询与分析器的兼容性;数据源未采到或现场没有对应事件时,表也可能只是空表。SQL 报错和零行结果,我们分别排查。

本文三段查询已用 Perfetto v58.2 在官方测试 trace上运行校验,并分别检查内置标准库与上述 r1 SQL 模块。此校验确认的是示例的执行与区间筛选语义;换一份 trace,数据覆盖要重新确认。

查询异常 SurfaceFrame

下面的查询使用 android_frames_layers 提供的 actual/expected id(实际/预期记录标识) 配对,避免根据 slice 名称或 track_id 猜测帧关系。

INCLUDE PERFETTO MODULE android.frames.timeline;

SELECT DISTINCT
  f.process_name,
  f.upid,
  a.id AS actual_slice_id,
  f.frame_id,
  f.layer_name,
  a.surface_frame_token,
  a.display_frame_token,
  a.ts,
  a.dur / 1e6 AS actual_ms,
  e.dur / 1e6 AS expected_ms,
  ((a.ts + a.dur) - (e.ts + e.dur)) / 1e6 AS finish_delta_ms,
  a.on_time_finish,
  a.present_type,
  a.jank_type
FROM android_frames_layers AS f
JOIN actual_frame_timeline_slice AS a
  ON a.id = f.actual_frame_timeline_id
LEFT JOIN expected_frame_timeline_slice AS e
  ON e.id = f.expected_frame_timeline_id AND e.dur >= 0
WHERE a.dur >= 0
  AND (a.on_time_finish = 0
       OR a.jank_type != 'None'
       OR a.present_type != 'On-time Present')
ORDER BY a.ts;

finish_delta_ms 比较的是 actual end(实际结束时间) 与 expected end(预期结束时间),只表示该 SurfaceFrame 的窗口偏差;最终责任仍以 jank_type、DisplayFrame 和 flow 为准。android_frames_layers 适合分析关联到 HWUI doFrame/DrawFrame 的逐 layer 记录,android_frames 则按进程与 frame 聚合;两者覆盖的都是加工后的视角,全部 producer 的原始 FrameTimeline 数据,要回到未聚合的 timeline 记录去看。基线模块对 DrawFrame 与 layer 的匹配仍有 TODO,同一 actual slice 可能因多个 DrawFrame 重复出现。上述查询按 actual slice 身份去重;缺少 expected 或有效配对时,偏差为 NULL,读作缺失,别补成 0。

帧查询的联结、去重与缺失值

图:先包含模块并检查分析器版本,再由模块提供的 ID 连接 actual/expected。多 DrawFrame 可能使同一 actual slice 重复出现;缺少 expected 时保留 NULL,缺失照实记,不补成零。

查询主线程与 RenderThread 的等待

下面的查询列出目标进程关键线程中最长的 Runnable 与不可中断等待,包名需要替换。

WITH params(process_name) AS (
  VALUES ('com.example.app')
)
SELECT
  p.name AS process_name,
  t.name AS thread_name,
  s.ts,
  s.dur / 1e6 AS wait_ms,
  s.state,
  s.io_wait,
  s.blocked_function
FROM thread_state AS s
JOIN thread AS t USING (utid)
JOIN process AS p USING (upid)
JOIN params ON params.process_name = p.name
WHERE (t.is_main_thread = 1 OR t.name = 'RenderThread')
  AND (s.state IN ('R', 'R+') OR s.state GLOB 'D*')
  AND s.dur > 0
ORDER BY s.dur DESC
LIMIT 100;

R / R+ 行的 duration 是等待调度的时长,R+ 表示被抢占后仍可运行;以 D 开头的状态可能组合 K 等标志,仍需结合 io_wait 和 blocked_function 解释。查询没有限制帧窗口,适合初步筛选;形成单帧结论时,再与目标 frame 的 [ts, ts + dur) 半开时间区间计算重叠——包含起点、不包含终点。

等待筛选与单帧归因之间的步骤

图:全局 Top 等待只用于筛选。形成单帧结论前,要保留线程身份,与实际帧半开区间计算重叠,再补等待对象、调度竞争或驱动证据。

把 Binder 限制在 AppDeadlineMissed 帧内

下面的查询只保留目标应用 UI/RenderThread 与 App Deadline Missed 实际帧窗口有重叠的同步 Binder,包括窗口之前发起、窗口内仍未返回的事务;它用来找依赖候选;从全局慢事务走到帧责任,中间要经过这道帧窗口重叠。

INCLUDE PERFETTO MODULE android.frames.timeline;
INCLUDE PERFETTO MODULE android.binder;

WITH params(process_name) AS (
  VALUES ('com.example.app')
),
janky_frames AS (
  SELECT DISTINCT
    f.frame_id,
    a.id AS actual_slice_id,
    f.layer_name,
    a.ts,
    a.dur,
    f.upid,
    f.ui_thread_utid,
    f.render_thread_utid
  FROM android_frames_layers AS f
  JOIN actual_frame_timeline_slice AS a
    ON a.id = f.actual_frame_timeline_id
  JOIN params ON params.process_name = f.process_name
  WHERE a.jank_type GLOB '*App Deadline Missed*'
    AND a.dur > 0
)
SELECT
  jf.frame_id,
  jf.actual_slice_id,
  jf.layer_name,
  b.binder_txn_id,
  b.aidl_name,
  b.method_name,
  b.client_thread,
  b.server_process,
  b.server_thread,
  b.client_dur / 1e6 AS client_ms,
  (MIN(b.client_ts + b.client_dur, jf.ts + jf.dur)
   - MAX(b.client_ts, jf.ts)) / 1e6 AS overlap_ms,
  b.server_dur / 1e6 AS server_ms
FROM janky_frames AS jf
JOIN android_binder_txns AS b
  ON b.client_upid = jf.upid
 AND (b.client_utid = jf.ui_thread_utid
      OR b.client_utid = jf.render_thread_utid)
 AND b.client_ts < jf.ts + jf.dur
 AND b.client_ts + b.client_dur > jf.ts
WHERE b.is_sync = 1 AND b.client_dur > 0
ORDER BY b.client_dur DESC;

查询结果只能证明 Binder 与责任线程的帧窗口重叠。要解释延迟来源,还要包含 android.binder_breakdown,按 binder_txn_id 汇总 android_binder_client_breakdown 和 android_binder_server_breakdown 中的 reason。server_ts - client_ts 比较客户端事务 slice 与服务端处理 slice 的起点;这段差值可能含传输和调度等阶段,线程池排队时间、服务线程首次获得 CPU 的时刻,都要从其他证据里读。判断线程池是否饱和,看服务端 worker(工作线程)、Runnable、嵌套 Binder 与队列证据;固定毫秒阈值替代不了它们。

同步 Binder 与实际帧窗口的交集

图:重叠筛选会包含帧开始前发起、帧内仍未返回的同步 Binder。它只建立候选依赖;线程池是否饱和,要看服务端分解、线程状态和嵌套事务,固定时间差给不出这个结论。

大型 trace

分析大型 trace 时,我们先用时间窗口、进程/线程条件和 LIMIT 缩小查询范围,再考虑使用 MATERIALIZED CTE(物化公共表表达式)或临时表。命令行 trace_processor_shell trace.pb --query-file analysis.sql 适合离线批处理;SQL 文件与 trace、Trace Processor 版本和查询参数一起归档,结果才可以重放。

标准化分析检查清单

现场与 trace 完整性

  • 记录设备、build、应用版本、刷新率、thermal 和操作脚本;
  • 标出目标问题的开始、结束和复现次数;
  • 确认 FrameTimeline、sched、atrace 和所需专项 probe 有数据;
  • 检查 buffer overwrite、数据丢失与 tracing 开销;
  • 确认 Producer、Surface、layer 和合成路径。

帧与责任边界

  • 记录 SurfaceFrame token、DisplayFrame token、layer name;
  • 读取 expected / actual、finish、present 与 JankType;
  • 沿 flow 找到对应 DisplayFrame;
  • 检查相邻帧是否存在 stuffing(帧堆积)、drop、mode/power transition(显示或电源模式切换);
  • 按 App、SF CPU、SF GPU、Display 或 buffer 选择分支。

线程与依赖

  • App 分支拆 MainThread、RenderThread、应用 GPU 和 buffer;
  • SF 分支拆主线程、RenderEngine、HWC 与 present;
  • 对长等待区分 Runnable、Sleeping(睡眠)、D、Binder、锁和 fence;
  • GC 只计算与目标线程重叠的 pause(暂停),不把并发阶段全算作停顿;
  • thermal、频率、内存压力和后台负载保留为需要时序验证的系统因素。

结论与验证

  • 写出异常帧、责任方、关键路径事件和排除项;
  • 区分直接证据、组合证据与相关现象;
  • 修复前后使用同一设备状态、场景和 trace 配置;
  • 比较 jank 类型、帧间隔分布、关键 slice/wait(执行区间/等待) 和业务指标;
  • 结论复现不出来时,保留不确定性和下一轮采集项。

常见失误

失误修正
看到红色 slice 就写根因读取 token、字段、flow 与责任线程
用固定 16.67 ms 处理所有设备使用该帧 expected / deadline 和当时显示模式
doFrame 短便排除 App继续检查 RenderThread、应用 GPU、buffer 与独立 Surface
主线程 Sleeping 便认定 App 无责找等待对象、服务端、owner 或 fence
D 状态都算磁盘 I/O检查 blocked_function、io_wait 与驱动事件
Binder dispatch 超过某阈值便认定线程池耗尽检查 server worker、调度、锁、嵌套事务和 breakdown(分段耗时)
FrameMetrics listener drop 当作显示掉帧它表示指标回调丢失
App 启动 Perfetto SDK 就能拿系统 trace区分进程内 Track Event 与受权限保护的系统数据源
trace 越长越有价值选择能覆盖复现且数据完整的最小窗口

平台与内核基线

  • Platform(平台层):FrameTimeline、FrameMetrics、HWUI、SurfaceFlinger 和 Perfetto stdlib(标准库)的机制以文首的源码基线为准。
  • Kernel(内核):调度、Binder、blocked reason、dma-fence(设备缓冲同步栅栏)、PSI 与内存回收同样以文首的内核基线为准。
  • 版本演进:Android 12 前没有 FrameTimeline;API 24 起有 FrameMetrics,API 31 增加 GPU_DURATION / DEADLINE,API 36 增加 FRAME_TIMELINE_VSYNC_ID。历史工具可保留,分析时优先使用现代字段。

滚动、动画、启动与交互场景

这一节按场景展开:滚动、动画、启动与页面切换的帧生产方式不同,起止点和关键轨道也不同,我们逐个场景列出要看的对象和检查顺序。

从场景名到渲染链路

“列表卡”“转场卡”“通知栏卡”说的是用户看到了什么,排查的第一步是把它翻译成“哪条渲染链路迟到”。同一个列表里可以同时出现普通 View、SurfaceView 视频和 TextureView 地图;一次页面切换又可能包含应用窗口的 buffer、Shell transition(系统窗口过渡) 的 leash(用于统一控制窗口动画的临时图层)变换、壁纸、IME 与 SurfaceFlinger 合成。如果我们从场景名称直接跳到某个线程,很容易把证据落到错误的对象上。

定位顺序如下:

  1. 记录发生卡顿的交互阶段、显示屏、刷新率和时间区间。
  2. 列出屏幕上的内容生产者,以及各自产出的 Surface、BufferQueue(连接内容生产者和消费者的缓冲队列) 和 SurfaceFlinger layer(图层)。
  3. 从 FrameTimeline 的异常 SurfaceFrame/DisplayFrame,或目标 layer 的异常 present 反查。
  4. 沿 token、frame number(帧序号)、transaction、buffer 与 fence 找到最早迟到的阶段。
  5. 回到责任线程,检查执行时间、Runnable 等待、锁、Binder、I/O、GC、GPU 和温控状态。

60 Hz 的名义刷新间隔约为 16.67 ms,120 Hz 约为 8.33 ms,但它们只是参考值,直接拿来当任意线程的固定预算会出错:Choreographer 回调相位、应用 deadline(截止时间)、BufferQueue 状态、SurfaceFlinger 调度和显示模式都会改变一帧的可用时间。诊断目标应写成“该帧相对 expected timeline(预期时间线) 在哪里开始偏离”,避免写成“整条管线必须在一个 VSync(垂直同步) 间隔内全部结束”。

一张场景取证表

每次复现都建议先把这张表填好。缺了哪一列,结论就少覆盖哪一块。

维度需要记录的内容常用证据
交互手指拖动、fling(惯性滑动)、点击、返回进度、窗口进入、通知更新input event(输入事件)、应用 marker(跟踪标记)、CUJ(Critical User Journey,关键用户操作流程)
显示display id(显示屏标识)、刷新率、分辨率、显示模式SurfaceFlinger/Winscope、Perfetto
窗口App Window、Dialog、Splash、IME、壁纸、transition leashWindowManager、Shell Transitions
内容生产者UI/RenderThread、MediaCodec、GL/Vulkan、WebView renderer、地图引擎线程、进程、SDK/provider(实现提供方)版本
提交对象ViewRoot buffer、SurfaceView child layer(子图层)、TextureView 输入、task snapshot(任务快照)BufferQueue、Layer、transaction
帧结果expected/actual(预期/实际)、present type(呈现类型)、jank type(卡顿类型)、dropped/duplicated(丢弃/重复)FrameTimeline、FrameTracer、fence
责任边界应用、SystemUI、Launcher、system_server、SF/HWC(SurfaceFlinger/Hardware Composer,系统合成服务/硬件合成器)、内核/驱动sched(调度)、Binder、GPU/HWC、kernel trace(内核跟踪)

列表滑动:先区分拖动与 fling

列表滑动至少有两段不同的驱动方式。

  • 手指按住并拖动时,输入分发和应用消费决定滚动位置何时更新。检查 MotionEvent(触摸事件) 到主线程处理的间隔、输入回调耗时,以及同一帧的 traversal(测量、布局和绘制遍历)。
  • 手指抬起进入 fling 后,滚动由动画时钟和 RecyclerView/ScrollView 的滚动计算继续推进。此时没有持续的触摸移动事件;要检查动画回调是否及时,以及每帧滚动工作是否稳定。
  • 两个阶段都可能受 RenderThread、GPU、BufferQueue 或显示合成影响。主线程短只排除了部分应用 CPU 工作。

拖动与 fling 的驱动入口

图:拖动时重点看输入分发、消费与位置更新;fling 时重点看动画时钟和滚动计算。两者都可能受后续 bind/layout、RenderThread、GPU、buffer 与显示链影响。

FrameTimeline 可以先圈定异常帧。随后我们把异常帧与正常帧放在一起比较,查看 UI Thread(主线程)、RenderThread、对应的 App Window buffer 和 DisplayFrame。对照相邻的正常帧,稳定存在的初始化或后台任务往往就能从嫌疑里排除;只盯一个长 slice,常会把它们误判成根因。

RecyclerView 的三组内建线索

AndroidX RecyclerView 在系统跟踪中留下了自己的一组 slice;不同库版本的名称可能略有差别,官方慢帧文档常用以下三组线索:

线索代表的工作常见原因处理方向
RV OnBindView把数据绑定到已有 ViewHolder格式化、同步读取、复杂 span、监听器反复创建把纯数据准备移出 bind,缓存稳定结果
RV CreateViewinflate 并创建 ViewHolderitem 树过深、View 类型多、复用不足简化高频 item,按 viewType 检查创建频率
RV PrefetchGapWorker 执行预取嵌套列表、预取量不合适、共享池边界错误按真实滚动方向和嵌套关系调整参数

onBindViewHolder() 运行在主线程,但它不一定嵌在名为 measure 或 layout 的 slice 里。判断 bind 是否拖慢一帧,我们直接看 RecyclerView slice 或应用自定义 marker,再观察它与 Choreographer#doFrame、traversal(界面遍历) 的时间关系。

onCreateViewHolder() 偶尔出现是正常行为。只有它在用户可感知的滚动区间反复出现、并与异常帧对齐,创建或复用才需要处理。盲目增大 RecycledViewPool 会增加常驻 View 的数量,也可能错误共享配置或语义不同的 ViewHolder;缓存大小由 viewType 分布、窗口尺寸、嵌套列表结构和内存实验共同决定。

布局容器没有固定的性能排名。ConstraintLayout 可以减少某些嵌套,也可能因约束求解、helper(辅助对象) 或频繁变化增加工作。我们优化时依据的是目标 item 的 measure/layout 次数和耗时,先把这个量出来,再谈选哪个容器。

图片完成后仍可能影响三条路径

成熟的图片库通常会把网络请求和解码移到后台线程,但显示阶段仍会回到用户可见的渲染链路:

  1. 结果回调在主线程更新 ImageView;
  2. 尺寸或 drawable(可绘制对象) 状态改变可能触发 invalidate() 或 requestLayout();
  3. 首次使用纹理时,RenderThread/GPU 可能产生上传与采样成本。

列表图片应在绑定前确定稳定的目标尺寸或宽高比。复用 ViewHolder 时,要取消或替换旧请求,并确认回调仍属于当前绑定项。图片预取要结合缓存命中率、解码尺寸和内存占用评估,只追求更早加载并不划算。

图片后台完成后的显示成本

图:后台下载或解码完成后,图片应用到 View 仍可能触发布局、绘制和纹理成本。稳定尺寸、绑定身份和取消旧请求分别解决不同问题,fixed-size 覆盖不了这些检查,仍要逐项做。

RecyclerView.setHasFixedSize(true) 表达的是 RecyclerView 自身尺寸不受 adapter(列表数据适配器) 内容变化影响;bind 照常执行,子项也可能继续请求布局。RecyclerView 1.4.0 会据此选择部分测量和 adapter 更新处理路径,效果仍要按实际布局次数验证。

小核、频率与调度结论

RenderThread 在某一帧运行于低容量 CPU,只能说明“当时在哪里执行”。要确认调度因素,需要同时检查:

  • wakeup 到 Running(开始运行) 的等待时间;
  • 线程的调度策略、优先级、uclamp(CPU 利用率约束) 与 task group(任务组);
  • CPU capacity(算力容量)、频率、idle(空闲状态) 退出和迁核;
  • 同核更高优先级任务的抢占;
  • thermal throttling(温控降频) 与持续复现下的变化;
  • 相同工作量在正常帧和异常帧上的执行时间。

应用侧据此要求线程固定运行在某个大核,通常行不通;系统或厂商团队若要修改调度策略,还需在 android17-6.18-2026-06_r6 对应的设备内核和 SoC(片上系统) 调度实现上验证。通用内核提供调度、CPU 拓扑和 CPUFreq 等机制;设备的实际拓扑、频率/容量映射以及 GPU/HWC 可观测事件,还取决于 SoC、驱动、固件与构建配置。

页面切换:拆开内容准备、窗口事务与显示

Activity transition

Activity 跳转可能发生在同一进程,也可能拉起已有进程或新进程。诊断前我们先区分热启动、温启动和冷启动,并记录目标页面的 TTID(Time to Initial Display,首次显示时间)与 TTFD(Time to Full Display,完全显示时间)以及第一帧。把所有跳转都写成“两个应用进程协同”,会漏掉同进程场景,也会掩盖冷启动中的进程创建和类加载。

现代窗口过渡需要同时观察三条线:

  • 应用侧:源/目标 Activity 生命周期、inflate、首个 traversal、RenderThread 和窗口 buffer;
  • 窗口侧:WindowManager Shell transition、参与者、sync、SurfaceControl leash 与几何事务;
  • 显示侧:目标 layers 的 buffer/transaction、SurfaceFlinger composition(合成)和 display present。

源窗口和目标窗口的内容提交与 leash 动画可以来自不同线程、不同进程。应用首帧准备较晚时,Shell 可能继续显示 starting window(启动占位窗口)、snapshot(任务快照)或旧 Surface;应用侧按时而整屏仍迟到时,应查看 SurfaceFlinger/HWC。Winscope(窗口与图层分析工具) 的 Shell Transitions、Window Manager、SurfaceFlinger Layers 和 Transactions 可以复原窗口关系,Perfetto 更适合比较线程调度、buffer 与帧时间。

Activity 切换中的三条并行证据线

图:先区分同进程/跨进程与冷、温、热启动。应用内容提交、Shell leash 事务和显示 present 是不同边界;Winscope 查参与者与图层,Perfetto 对齐线程和帧依赖。

Fragment transaction

AndroidX Fragment 的 commit() 会把事务加入 FragmentManager 队列:它经 enqueueAction() / scheduleCommit(),通过宿主的 Handler 调用 post,投递一个 mExecCommit,随后由宿主主线程执行 pending actions(待处理操作)。mExecCommit 与 Choreographer 帧回调共享同一个主 Looper,实际执行顺序取决于主 MessageQueue(消息队列) 中已有消息、同步屏障、异步 Choreographer 消息,以及 commit() 的发生时刻。这条链路与 VSync 之间同样没有对齐承诺,“execPendingActions() 必定早于或晚于某次 doFrame”的固定顺序并不存在。

使用 commitNow() 会把工作放进当前调用栈,改变这一相对位置。一次切换可能把 Fragment 状态推进、View 创建/移除、SpecialEffectsController(转场与动画效果控制器)、measure/layout 和动画准备集中到相邻几帧。

几个 API 的边界需要分清:

  • commit() 异步排队;返回时事务通常尚未执行。
  • commitNow() 在调用线程同步执行,要求主线程,也不支持与 addToBackStack() 组合。它会把工作提前到当前调用点,当通用的流畅度开关用并不合适。
  • executePendingTransactions() 会执行当前待处理事务,影响范围可能超过某一次提交。
  • setReorderingAllowed(true) 允许 FragmentManager 优化同一批操作的状态变化,并改善 transition/lifecycle(转场/生命周期) 语义。布局、业务初始化或 GPU 工作仍要另行处理。
  • commitAllowingStateLoss() 改变的是保存状态后的提交约束,用它规避卡顿会引入状态丢失风险。

取证时可以分别标记“发起 commit”“pending actions 开始/结束”“目标 Fragment 首次可见”和“第一帧 present”。卡点在 onCreateView()、onViewCreated() 或首个 layout,说明要处理页面构建;App buffer 已经按时提交,我们就继续查 transition transaction 和 display frame。更完整的源码链路见 22.11 Fragment、Predictive Back 与 Navigation Compose 页面切换。

Fragment 提交与帧回调的相对位置

图:以 Fragment 1.8.9 源码核对异步提交路径。commit 经宿主 Handler 排队,commitNow 在当前主线程调用点执行;它们都可能产生构建、布局和转场工作,和某次 doFrame 没有固定前后关系。

Shared element

共享元素转场的成本取决于具体实现和元素类型,可能涉及源/目标 View 的名称匹配、布局坐标捕获、overlay(覆盖层)/ghost(叠加层/临时镜像视图)、snapshot、图片资源准备、matrix/clip(变换矩阵/裁剪) 更新,以及两个窗口的可见性协调。把所有共享元素都概括为“复制一张 bitmap(位图)”,会漏掉大部分工作。

常见的断点包括:

  • 目标元素尚未完成布局,终点 bounds(边界) 不稳定;
  • 大图在转场开始后才解码或上传;
  • 元素层级在转场期间触发额外 layout;
  • 源窗口、目标窗口与 transition leash 的时序没有对齐;
  • alpha(透明度)、圆角、模糊或遮罩改变了合成条件。

应同时记录元素准备回调、目标页首个 traversal、窗口 transition 和对应 layer present。源窗口迟到或显示合成迟到时,优化目标 Activity 的 XML 覆盖不到这两处,要把排查范围扩到源窗口和显示端。

共享元素转场的多项依赖

图:共享元素成本随实现而变。资源就绪、坐标/状态捕获和窗口可见性需要协调;目标页 XML 优化只能覆盖其中部分,显示合成仍须单独核验。

窗口动画:Splash、返回手势与浮层

SplashScreen 与 starting window

Android 12(API 31)起,系统 SplashScreen API 为冷启动和温启动提供统一的启动画面,热启动通常不会显示该画面。Splash screen(启动画面) 是一个独立窗口,会在应用可以绘制前覆盖目标窗口,并在应用第一帧就绪前后退出。

一次“启动时闪顿”应分别记录以下边界:

  1. 启动请求进入 ActivityTaskManager(Activity 与任务管理服务);
  2. starting/splash window(启动占位窗口/启动画面)可见;
  3. 应用目标窗口提交第一块可用 buffer;
  4. 目标窗口相关 DisplayFrame 的 present;
  5. splash 退出动画结束/移除的时刻。present 时刻和 splash 退出时刻要分开记;交接期间,目标内容可能在 splash 仍存在时先参与合成。

若第 2 到第 3 个时间点间隔很长,应检查进程启动、Application/Activity 主线程、资源和首帧。若应用 buffer 已经到达而交接仍然抖动,我们检查 splash exit listener(启动画面退出监听器)、Shell/WMS transaction(Shell/WindowManagerService 窗口事务)、目标 layer 和 SurfaceFlinger。自定义退出动画完成后还要移除 splash view;持续保留它会延长交接窗口。

Splash 交接要分别记录的边界

图:启动请求、splash 可见、目标 buffer 可用、相关 DisplayFrame 的 present,以及退出动画结束/移除,都应分别打点。交接期间各阶段可能重叠,一条首帧 slice 解释不了全部闪顿。

Predictive Back

Android 15 移除了预测性返回的开发者选项。应用完成 opt-in(显式启用) 后,系统可以提供返回桌面、跨 Activity 和跨任务的预测动画;具体效果仍受导航结构、回调类型和系统实现影响。相关的平台与 AndroidX 回调接口需要分清,三者语义不同,要分开使用:

  • OnBackPressedCallback(AndroidX Activity):handleOnBackPressed() 处理返回提交;Activity 1.8.0 增加 handleOnBackStarted(BackEventCompat)、handleOnBackProgressed(BackEventCompat) 和 handleOnBackCancelled()。系统框架只在 API 34 及更高版本分发这些进度回调;低版本仍可使用提交回调。
  • OnBackInvokedCallback(平台,API 33 及更高版本):只有 onBackInvoked(),在返回“提交”时回调。它同样没有 started/progressed/cancelled(开始/进行/取消) 的进度语义。
  • OnBackAnimationCallback(平台,API 34 及更高版本):在启用预测性返回的窗口上提供 onBackStarted(BackEvent)、onBackProgressed(BackEvent)、onBackCancelled() 和继承的 onBackInvoked()。应用可以直接注册平台接口,也可以通过 AndroidX Activity 1.8.0 及更高版本的上述回调处理进度。

诊断要同时检查三个问题:

  • 平台或 AndroidX 手势进度回调是否短小、连续,取消路径(onBackCancelled / handleOnBackCancelled)能否恢复界面状态;
  • 当前回调是否消费了系统返回,让系统预测动画失去运行机会;
  • 当前窗口、目标窗口或 home/task surface(桌面/任务 Surface) 的 leash 与 display frame 是否按时。

不要预设现场存在名为 predictive_back_progress 的标准 counter。返回手势的输入、进度和参与者可沿 SystemUI EdgeBackGestureHandler、WindowManager Shell transition 与 Winscope 的相关记录追踪;Perfetto 中可见的事件取决于平台版本、插桩和已启用的数据源。

需要验证 progress 回调时序时,用应用自身插桩(例如 Trace.beginSection("onBackProgressed"))或 Winscope 的 Shell 参与者。

返回手势的提交与取消路径

图:开始和进度回调要保持短小,取消时恢复状态,提交时执行导航。先查启用与消费规则,再查参与 surfaces 的帧证据;预测动画缺失与渲染掉帧是两类问题,要分开判断。

Android 16(API 36)起,可以使用 PRIORITY_SYSTEM_NAVIGATION_OBSERVER 观察系统导航而不消费返回,Android 17 继续保留这一能力。默认优先级或 overlay(硬件叠加平面) 优先级回调会参与消费决策。注册方式错误时可能出现“没有预测动画”,这与渲染掉帧属于两类问题。

返回预览的内容要从现场确认:跨 Activity、跨任务和返回桌面时,参与的 surfaces 由导航状态与 Shell transition 决定,我们以 Winscope 的参与者和 layer tree(图层树) 为准。

Dialog 与 PopupWindow

Dialog 和 PopupWindow 都会向 WindowManager 增加窗口对象,并拥有各自的 ViewRoot 与 Surface;它们可以与宿主处于同一进程和同一 UI Looper(界面消息循环)。弹出时的成本可能来自:

  • 首次 inflate、measure/layout 和窗口首帧;
  • 同一主线程上宿主窗口与浮层窗口的 traversal 排队;
  • IME/Insets(输入法与系统栏等占用区域) 变化;
  • dim(背景变暗)、blur(模糊)、圆角、阴影和动画;
  • 新 layer 加入后,HWC 的 DEVICE(显示硬件合成)/CLIENT(GPU 客户端合成) 分配变化;
  • GPU 带宽、client target(GPU 合成后的整屏目标缓冲区) 或 present fence 延迟。

“出现额外 layer”离“走 GPU client composition(客户端合成)”还差一步:HWC 是否使用 overlay,要看整组 layers 的格式、变换、混合、保护属性、硬件资源和厂商能力。我们把弹出前后的 layer composition type(合成类型)、client target、GPU 时长与 present 结果放在一起比较。深入案例见 HWC Overlay Plane 与合成降级排查。

浮层出现后的两条成本路径

图:Dialog / PopupWindow 的新 ViewRoot 通常仍与宿主共用 UI Looper。新 layer 既可能增加主线程与渲染工作,也可能改变 HWC 分配;是否 CLIENT 必须看实际合成类型和显示结果。

Notification 展开与折叠:责任进程在 SystemUI

通知面板的展开、折叠、分组和 Quick Settings(快捷设置) 动画,主要由 SystemUI 生成 UI 帧。普通应用在发布或更新通知时通过 Binder 提交 Notification;动画期间如果通知内容没有更新,应用进程可能完全不在关键路径上,关键路径基本落在 SystemUI 一侧。

需要分别检查:

阶段责任对象典型证据
发布/更新通知应用、system_server、SystemUIBinder、NotificationManagerService、SystemUI pipeline(处理管线)
应用 RemoteViews/模板SystemUI 主线程inflate/reapply(创建/重新应用视图)、图片/图标、measure/layout
Shade(通知面板)动画SystemUI UI Thread/RenderThreadFrameTimeline、CUJ、traversal、DrawFrame
整屏合成SurfaceFlinger/HWCvisible layers(可见图层)、composition type、present

频繁更新进度、反复改变通知布局或提交大图片,会增加跨进程传输和 SystemUI 的处理成本。RemoteViews(可跨进程应用的视图描述) 更新时,可能复用已有 View,也可能需要重新应用布局;每次 notify() 到底走哪条路,以 trace 和通知差异为准。

以 Android 12(API 31)及更高版本为目标的应用,自定义通知会被系统放入标准模板,以保持图标、展开区域和操作的一致性。自定义内容的处理成本仍然存在:复杂 RemoteViews、图片尺寸、更新频率和分组规模依旧作用在 SystemUI 上。

如果 SystemUI 的 SurfaceFrame 按时而 DisplayFrame 迟到,我们再检查遮罩、壁纸、状态栏、导航栏、当前 App 和通知面板的合成。应用自己的 App FrameTimeline 代表不了通知面板。

通知发布与面板动画的责任进程

图:应用提交通知数据,SystemUI 负责面板内容应用与动画。通知未更新时,普通应用可能不在展开动画的关键路径上;应看 SystemUI 的帧以及整屏合成。

桌面滑动与多任务切换:以 OEM(设备厂商) 现场为准

Launcher 桌面

AOSP 的 Launcher3 提供 Workspace、CellLayout、Widget 与 Quickstep(手势导航和最近任务组件) 的参考实现,量产设备可能替换 Launcher 或修改动画。桌面滑动常见的内容包括图标、文件夹、AppWidget、壁纸和搜索/推荐区域;其中 Widget 更新、动态壁纸和 Launcher 帧可能来自不同的内容生产者。

诊断顺序可按对象展开:

  1. Launcher 主线程的输入、动画和 traversal;
  2. Launcher RenderThread/GPU;
  3. AppWidget 更新是否在同一时间进入 Launcher;
  4. 壁纸 layer 或 WallpaperService 是否更新;
  5. SurfaceFlinger 的可见 layers、composition 与 present;
  6. sched、CPU frequency、thermal 和 GPU 证据。

发现 RenderThread 位于小核时,还要继续验证唤醒等待、CPU capacity、频率与同帧工作量。单凭一帧的 CPU 编号,还撑不起“调度器导致卡顿”的结论。

Recents / Overview

AOSP Quickstep 由 Launcher3 实现,窗口组织和动画还依赖 WindowManager Shell、ActivityTaskManager、SurfaceControl transactions(图层事务) 与 SurfaceFlinger。OEM 可以更换参与者或动画实现,因此进程名和 slice 名称应从目标设备采集。

多任务手势可能操作以下对象:

  • 当前任务的 live surface(实时任务画面) 与 transition leash;
  • 其他任务的 snapshot;
  • Launcher 的 Recents UI;
  • 壁纸、系统栏和手势相关 surfaces;
  • 即将恢复的目标任务窗口。

Task snapshot 通过 TaskSnapshot 携带 HardwareBuffer(硬件图形缓冲区)、色彩空间、方向和裁剪等信息。Launcher 采样硬件 buffer,与执行普通图片文件解码是两回事;压力更多来自 snapshot 获取时机、卡片数量、纹理采样、显存/带宽和整屏合成。某些阶段会继续使用 live task surface(实时任务 Surface),把每张卡片都解释成静态截图会出错。

在 Winscope 中,应检查 Shell transition 的参与者、WindowManager 状态、SurfaceFlinger layers 和 transactions;再在 Perfetto 中对齐 Launcher/SystemUI/system_server 的线程、输入、snapshot 相关 Binder、GPU 与 DisplayFrame。若动画卡片移动正常而内容停住,需要辨别当前看到的是 snapshot、旧 buffer 还是 live surface。

Recents 中实时画面与任务快照

图:TaskSnapshot 携带 HardwareBuffer 等信息;卡片内容也可能来自 live task surface。移动正常而内容停住时,先辨别 snapshot、旧 buffer 或实时内容,再查各自更新和合成。

视频:UI 帧与视频帧

视频通常由 MediaCodec 或播放器渲染器向 Surface 输出 buffer。使用 SurfaceView 时,视频通常拥有独立的 child layer(子图层);使用 TextureView 时,视频 buffer 先进入 SurfaceTexture,再由宿主 HWUI 在 App Window 中采样。两条路径的责任线程、buffer 数量和 FrameTimeline 覆盖范围不同。

视频进入显示端的两种常见路径

图:SurfaceView 通常以独立视频 layer 进入 SF;TextureView 经 SurfaceTexture 由宿主 HWUI 采样到 App Window。目标 layer 是否使用 DEVICE/overlay,还要看现场 HWC 分配。

视频“卡”的含义至少有三种:

  • 解码器没有按节奏产出可用 buffer;
  • buffer 已 queue(入队),但 fence、latch、合成或显示时刻迟到;
  • 播放器主动丢帧或重复帧,以维持音视频同步。

应记录媒体 presentation timestamp(PTS,呈现时间戳)、解码输入/输出、目标 Surface 的 frame number、queue/acquire/release(入队/获取/释放)、display present 和音频时钟。UI 的 App FrameTimeline 正常时,独立视频 layer 未必在连续更新;视频连续时,控制栏动画也未必流畅,两条线要分别核对。

HWC overlay 能减少 GPU 合成压力,但是否可用取决于格式、缩放、旋转、HDR(高动态范围)、受保护内容、其他 layers 与硬件资源。我们检查目标 layer 的实际 composition type,不依据 SurfaceView 或 MediaCodec 名称推断 overlay。详见视频 Overlay、Media3 与专业编解码管线。

地图与 WebView 的承载方式

地图 SDK

地图 SDK 可能使用 SurfaceView、TextureView、GLSurfaceView、自建 SurfaceControl,或把部分内容画进宿主窗口。瓦片下载、矢量解析、标注布局和 GL/Vulkan 提交也可能分属不同线程。要把“地图卡顿”归到固定的 GL 线程,先得拿到 SDK 版本、实际 View 类型和 layer tree。

排查时先确认:

  • 地图主体是独立 layer,还是作为纹理合入 App Window;
  • 相机移动由手势线程、主线程还是渲染线程驱动;
  • 瓦片 I/O/解码是否阻塞渲染依赖;
  • shader/pipeline(着色器/图形管线) 创建、纹理上传和 GPU 执行是否与异常帧对齐;
  • 独立 layer 与宿主控件的更新是否落在同一 display frame。

SurfaceView 与 TextureView 的差别参见SurfaceView 与 TextureView 渲染管线。

WebView

标准硬件加速 WebView 的网页主体通常经 Chromium renderer(渲染进程)、compositor/GPU 服务和 WebView functor(连接 Chromium 与 Android HWUI 的绘制桥接对象) 合入宿主 App Window。视频、受保护内容、provider overlay(WebView 实现提供方添加的叠加层) 或定制内核可能增加独立的 SurfaceControl layer。网页主体与媒体 overlay 需要分开追踪。

WebView 是可以独立更新的组件。平台源码可以锚定 android-17.0.0_r1,分析 Chromium 行为时还必须记录设备上的 WebView provider 包名、版本与 revision(修订版本)。某个 Chromium slice 名称或进程结构,要从这些 provider 信息里确认,平台版本标签只覆盖平台那一半。

现象优先查看
JS 长任务后页面不动renderer main thread(渲染进程主线程)、V8(JavaScript 引擎)、DOM/layout(文档对象模型处理/布局)依赖
页面 paint/raster(绘制/栅格化)晚Blink paint(网页绘制)、compositor、raster/GPU service(栅格化/GPU 服务)
宿主控件和网页一起晚App UI Thread、HWUI functor、host RenderThread(宿主渲染线程)
视频晚而页面滚动正常独立媒体 layer、codec(编解码器)、fence、HWC
host buffer(宿主缓冲区)已提交但屏幕晚SurfaceFlinger/HWC、DisplayFrame

Renderer 退出应结合进程生命周期、LMK(低内存终止)/OOM(内存不足) 证据与 WebViewClient.onRenderProcessGone() 判断。除非应用自行插桩,别预设 trace 中存在名为 render_process_gone 的 slice。完整结构见 WebView 渲染管线 和 WebView 性能优化实战。

WebView 网页、宿主与独立媒体层

图:典型硬件加速路径中,网页经 renderer、compositor/GPU 服务和 functor 接入宿主 HWUI;GPU 服务通常在宿主进程。独立视频或 provider overlay 要另追 layer,分析时同时记录 provider 版本。

从症状到证据的速查表

我们把上面的场景对应关系压成一张速查表:先按症状找到第一组对象,再沿“继续验证”一列深入,最后一列提醒每行最容易踩的判断坑。

场景症状第一组对象继续验证容易误判的结论
列表拖动立即跟手差input、UI Thread、RecyclerViewRenderThread、App Window、DisplayFrame“一定是 onBind”
fling 周期性顿挫animation、RV bind/create/prefetch图片回调、GC、sched、GPU“每帧都必须少于刷新间隔”
Activity 切换开头停顿目标首帧、启动类型Shell transition、source/target layers(源/目标图层)“总有两个进程”
Fragment 切换卡pending actions、生命周期、layoutSpecialEffectsController、RenderThread“改用 commitNow 就会快”
Splash 退场抖动splash 与目标窗口交接exit listener(退出监听器)、transaction、present“只有 Application 启动慢”
返回动画缺失opt-in、callback 消费Shell 参与者“缺失就是掉帧”
Dialog 出现后整屏变慢新 ViewRoot/layer、dim/blurHWC composition、GPU/present“多一个 layer 必走 CLIENT”
通知栏卡SystemUI FrameTimelineRemoteViews、SF/HWC“发布通知的 App 在画 Shade”
Recents 卡片内容停住snapshot/live surfaceQuickstep/Shell、SF transaction“所有卡片都是 bitmap 解码”
视频停顿、控件流畅codec producer(编解码器生产者)、视频 layerPTS、fence、HWC/present“App FrameTimeline 正常就没掉视频帧”
WebView 页面卡provider renderer/compositorhost HWUI、媒体 overlay、SF“只查宿主主线程”

版本记录与厂商差异

RecyclerView、Fragment、WebView provider 和地图 SDK 都可以独立更新,复现报告要相应记录它们的版本。厂商 Launcher、SystemUI、HWC、GPU 驱动与调度策略也可能偏离 AOSP 参考实现。

内核侧的通用证据包括 sched wakeup/switch(调度唤醒/切换)、CPU frequency/idle(频率/空闲状态)、thermal、dma-buf(设备间共享缓冲区) 与 dma-fence;设备可见的 GPU、display、HWC 和厂商调度事件由 SoC 与构建配置决定。缺少某个厂商 tracepoint 时,就承认“不足以继续归因”,线程名称或 CPU 编号补不出这个结论。

复盘模板

一份可复核的场景结论至少回答以下问题:

  1. 哪次交互、哪块显示屏、哪个刷新率下复现?
  2. 用户感知对应哪个 SurfaceFrame、DisplayFrame 或目标 layer present?
  3. 画面由哪些 producer(内容生产者)、Window、Surface 和 layer 构成?
  4. 最早偏离 expected timeline 的事件是什么?
  5. 迟到线程当时处于 Running、Runnable、Sleeping、Blocked(阻塞) 还是 fence wait(栅栏等待)?
  6. 应用 buffer、窗口几何 transaction 与 display present 分别何时完成?
  7. 修复改变了哪项可测量证据,相邻正常帧和异常帧是否收敛?
  8. 结论依赖的平台、AndroidX、WebView provider、OEM 和 kernel 版本是什么?

场景归类的作用是减少待检查对象;归因仍要依靠源码、trace 和对照实验。证据到 Surface、进程或显示边界为止时,结论也停在当前层级。

案例:从现场证据到修复判断

场景模板帮我们提出假设;案例则要回答两件事:哪条证据排除了其他原因,修复后哪项指标发生了变化。

案例证据怎样使用

以下五个公开工程案例覆盖主线程、GC/内存、调度、SurfaceFlinger 合成和温控。案例来源分成三类:

  • 腾讯音乐技术团队的 WeSing 复盘给出了设备、测试动作、版本差异和若干 trace 数据;
  • AndroidPerformance 的系统案例给出了 Systrace(旧版 Android 系统跟踪工具) 截图和对照数据,但仓库没有原始 trace 文件;
  • 温控案例采用 Android Developers 发布的 Netmarble ADPF(Android Dynamic Performance Framework,Android 动态性能框架)案例,保留官方披露的效果数字。

截图只能证明作者当时观察到的现象;可以逐项查询、按时间核对的原始 trace 才算数。下面每个案例都把“公开材料中的事实”“Android 17 下的解释”和“仍缺少的证据”分开说明。历史数据不作为 android-17.0.0_r1 的实测结果;Android 17 源码只用于校正机制和工具入口。

统一复盘格式

字段要回答的问题
现场哪台设备、哪个 build(系统构建版本)、什么动作、持续多久
用户结果哪一帧、哪个 CUJ(Critical User Journey,关键用户操作流程)或哪段启动变差
关键证据线程状态、slice(时间区间)、counter(计数轨道)、heap(堆内存)、layer(图层)、fence(同步栅栏)、thermal(温控)状态
排除项哪些相似原因已经排除
根因哪条因果关系有对照实验支持
修复改了哪一段工作或资源配置
效果同条件下哪些指标发生变化
证据缺口缺少原始 trace、样本量、设备覆盖或统计定义中的哪一项

案例一:WeSing 歌房进房的一条主线程消息过重

现场与公开数据

腾讯音乐技术团队在 WeSing 歌房进房场景做过两轮优化。公开测试条件是 OnePlus 10 Pro、Android 12、进程冷启动,点击进房后等待 8 秒让 UI 稳定。原文报告,在 5.65 与 5.70 两轮优化后,PerfDog(移动端性能测试工具)统计的卡顿率改善接近 50%。

其中一条主线程消息集中创建微服务(可独立初始化的应用服务模块),并派发 Activity、Fragment 和音视频组件的生命周期事件。原文文字报告服务实例创建时间为 312 ms;同一复盘还记录了 40 ms 的生命周期分发、115 ms 的音视频 SDK 初始化、103 ms 的 bitmap 模糊和 18 ms 的日志参数拼接。

WeSing 服务实例集中创建代码摘录

图:来源截图展示服务实例创建映射代码;312 ms 来自原文文字报告,图内没有耗时时间轴。

这些数字只适用于该团队的设备、版本,以及 PerfDog 的指标定义和计算方式,直接换算成 Android vitals(Play Console 的应用性能指标) 或其他应用的收益并不可靠。

从现象到根因

“主线程有一个 312 ms 长任务”只是起点。团队接着把这条消息里的各项工作拆了开来:

  1. 微服务框架允许 lazy(按需) 初始化,但业务不断把服务标成进房预加载;
  2. 多项工作集中在同一条 Looper message(消息循环中的一条消息) 里,首批 UI 更新只能排在它们之后;
  3. 部分工作有严格的 UI 或生命周期顺序,全部丢到线程池行不通;
  4. bitmap 处理、配置 JSON 和部分 SDK 初始化可以脱离主线程;
  5. 日志方法即使最终不输出,调用前的字符串拼接和序列化已经发生。

所以根因除了“单个方法很慢”,还有进房依赖没有分类:立即完成、可以延后、适合预热和可以异步的工作混在了同一条消息中。

修复

公开复盘采用了五组动作:

  • 删除不必要的预加载,把服务默认改为 lazy;
  • 将不依赖 UI 的解析、bitmap 处理等移到工作线程;
  • 对进房后立即使用且类加载昂贵的组件做有条件预热,即预计会使用时提前加载;
  • 把必须在主线程执行的生命周期工作拆成小段,并保持业务顺序;
  • 避免关闭日志后仍构造昂贵参数。

原案例为了控制消息顺序使用过 postAtFrontOfQueue()。这个 API 会把消息插到队首,可能延迟输入、traversal 和其他消息。迁移到新项目时,我们把依赖写成明确的状态机(用状态及其转换表达执行顺序)或阶段队列,并为每段工作设置时间预算和取消条件;“插到队首”这一实现细节不适合直接复制。

效果与证据边界

原文披露了两项结果:整体 PerfDog 卡顿率接近减半;有条件预热让线上进房平均耗时减少 250 ms。由于没有公开完整样本量、分位数和全部修改前后 trace,这两项数据只描述 WeSing 当时的发布结果。

复验同类修改时,应同时核对:

  • 目标 CUJ 的 FrameTimeline 与进房 marker;
  • 每条主线程 message 的 wall time(从开始到结束的实际经过时间)、Running 与 Runnable 时间;
  • 冷启动类加载/JIT(即时编译)、后台预热占用的 CPU 和内存;
  • 第一帧、内容稳定时刻与用户可交互时刻;
  • 拆分后是否出现时序错误或首次点击延迟。

来源:Android 深入卡顿分析与实践(QQ 音乐技术团队)。

案例二:反复进退房后的内存增长与 GC

现场与公开数据

同一篇 WeSing 复盘记录了“开始流畅,反复进退歌房后越来越卡”的问题。Profiler(性能分析工具) 显示房间退出后仍有对象存活,其中一个根因是弹窗关闭后动画没有停止,引用链继续持有页面对象。原文还记录了内存紧张时进房更容易触发频繁 GC。

WeSing 原文中的 TypedArray 回收代码示例

图:原文另列的 TypedArray 回收问题,截图展示 finally 中补充 recycle() 的处理;图内未展示内存增长曲线或动画泄漏引用链。

公开材料没有给出该泄漏修复前后的 GC pause(垃圾回收暂停) 分位值或 JankStats 对照。因此这里只保留“修复引用链与生命周期”这一结论,该动画泄漏修复前后的堆大小、GC 次数和卡顿率缺少对应公开数据,不采用。

从现象到根因

“使用一段时间后变慢,重启恢复”可能由多种因素造成:Java/native(Java 堆/原生内存) 泄漏、图片/GPU 缓存、线程增长、热限制、系统内存压力或存储 I/O。把下面四组证据放在一起,可以构建“泄漏或缓存增长 → 分配/回收压力 → 异常帧”的强证据链;短期分配高峰在没有泄漏时也可能触发停顿,这类停顿另有来源:

  1. 相同操作循环下,Java/native/graphics(原生/图形) 内存或存活对象持续增长;
  2. ART(Android Runtime,Android 运行时) GC 的 pause 或 allocation(内存分配) stall(分配停顿) 更频繁,并与异常帧相交;
  3. Heap dump(堆转储)、heapprofd(原生堆分析器) 或引用分析指向释放不掉的对象;
  4. 修掉引用或限制缓存后,内存曲线、GC 事件和耗时较长的尾部帧同步改善。

ART 的并发 GC 仍包含暂停阶段,但整个 Concurrent GC slice(并发回收区间)里,属于主线程 Stop-The-World(所有线程暂停)时间的只是其中一部分。我们查看 trace 中明确的 pause、线程状态和分配等待。不同 Android/ART 版本的事件名称会变化,只搜索固定名称 GC For Alloc 容易漏掉其他事件。

并发 GC 与主线程实际停顿

图:并发 GC 的整体区间可与应用工作重叠;真正影响本帧的暂停和 allocation stall 要按目标线程、时间重叠及唤醒关系核验。图中轨道是概念示意,不代表某种收集器的固定暂停阶段。

原案例中“弹窗关闭后动画仍持有页面”的引用链能解释对象为何存活。修复动作是结束动画、移除回调或监听器,并释放与页面生命周期绑定的对象。缓存问题还要分别统计 Java heap(Java 堆)、native allocation(原生内存分配)、GraphicBuffer/dma-buf(图形缓冲区/设备间共享缓冲区) 和 GPU 资源。

动画生命周期与页面对象存活

图:原案例确认弹窗关闭后动画仍持有页面对象。结束动画、移除回调/监听器和释放页面资源针对这条引用链;GC 暂停与异常帧是否相关,仍需实际 pause、分配等待和前后 trace。

整机低内存对照

AndroidPerformance 还公开过一组整机低内存冷启动对照:

条件bindApplication 到第一帧Block I/O 与 Uninterruptible Sleep/WakeKillRunning
低内存约 2.0 s约 750 ms约 600–682 ms
正常内存约 1.22 s约 130 ms约 624 ms

低内存冷启动 trace

正常内存冷启动 trace

表中原文把两个等待类别合计:Uninterruptible Sleep | WakeKill - Block I/O 与普通 Uninterruptible Sleep。其中 D 表示不可中断等待,组合 K 才含“可被致命信号唤醒”的标志;解释 D 状态要按这些标志拆开,WakeKill 和存储 I/O 只是其中的可能项。Running 是线程实际执行时间;原文在低内存一节分别写了 682 ms 与约 600 ms,因此表格保留该来源差异。两次 Running 时间接近,提示优先排查额外等待与内存活动;要把调用路径、频率或计算量的变化一并排除,还需调用栈与同条件对照。

整机内存的证据链上,我们查看 PSI memory(内存压力指标)、direct reclaim(内存回收)、kswapd(内核后台回收线程)、swap/zram(压缩内存交换区) I/O(交换空间/压缩内存读写)、major fault(需要从存储读取数据的主缺页)、lmkd(低内存终止守护进程) 事件和前台线程状态。现代 lmkd 主要依据 PSI 与进程优先级工作,旧内核中的 lowmemorykiller 日志和固定 minfree 阈值方案直接套用行不通。

修复与效果

应用泄漏路径的修复目标是让房间退出后对象可以回收,并限制可重建缓存。整机低内存路径则需要减少前后台常驻对象、避免前台线程直接回收内存或等待 I/O,并由系统团队在目标设备上调校 lmkd、zram、内存回收和存储策略。

Android 14 起,应用不再收到部分旧的 TRIM_MEMORY_RUNNING_*(运行中内存压力) 回调;对应常量在 API 35 被弃用。应用仍可结合 TRIM_MEMORY_UI_HIDDEN(界面进入后台)、后台状态和自身内存预算释放可重建资源;旧式“运行中低内存”通知在当前版本已经收不到,释放时机由应用自己掌握。

验收至少包含重复进退房的 heap 曲线、GC pause、FrameTimeline、native/graphics 内存、PSI 和热状态。只看到 Java heap 下降,还证明不了 GPU buffer(图形缓冲区) 或整机内存压力已经改善。

来源:WeSing 复盘、Android 低内存案例、ComponentCallbacks2。

案例三:SDK 升级增加线程后,主线程获得 CPU 变慢

现场与关键数据

WeSing 5.68 的版本对比发现:

  • 相比上个版本,进程增加近 30 个线程;
  • file descriptor(文件描述符,简称 FD)增加约 250 个;
  • 团队使用的卡顿率从 15% 上升到 20%;
  • 增量线程在退出歌房后仍未减少;
  • APK/版本二分(在版本区间中反复折半定位变化)把问题定位到 TRTC SDK 升级;
  • Perfetto SQL 统计显示,升级后名为 DefaultDispatch 的线程 CPU 时间超过 UI Thread 和 RenderThread。

SDK 升级后的一次线程 CPU 时间汇总

图:来源截图按线程名汇总一次采集中的 CPU 时间;升级前后差异来自原文复测叙述,截图本身只显示一组查询结果。

从相关性到根因

线程数增加本身还只是间接现象;这个案例的证据价值来自版本二分、线程来源定位和 CPU 时间汇总——SDK 方移除与业务无关的功能后,相关指标恢复。

在 Android 17 上,还应补两项证据:

  • 异常帧里 UI Thread/RenderThread 是否长时间处于 Runnable,wakeup 到 Running 的等待是否增加;
  • 新线程在同一时间是否处于 Running,占用了哪些 CPU,是否带来频率、迁核、thermal、内存或 GC 变化。

如果新增线程多数处于 Sleeping,调度影响可能很小;如果一个新增 CPU worker(工作线程) 长时间处于 Running,即使线程总数不高,也可能推迟交互线程获得 CPU。文件描述符增加说明的是资源占用发生了变化;CPU 调度的结论,要从线程状态和 CPU 时间里找。

修复

团队把问题提交给 SDK 方,去掉升级时引入但当前业务不需要的功能。通用处理包括:

  • 为 SDK 和业务线程提供稳定、可聚合的名字;
  • 对线程池设置有界并发、队列和取消;
  • 页面退出时停止会话与 worker;
  • 用版本开关或依赖回退完成 A/B(对照实验);
  • 分开统计线程数、CPU time(CPU 执行时间)、Runnable latency(可运行等待时间)、RSS/PSS(进程实际占用/按比例分摊的内存) 和 FD。

手动把 UI Thread 或 RenderThread 固定到某个“大核”,会把 SoC 拓扑、热状态和厂商调度差异固化到代码中;无效工作本身仍要移除。

效果与证据边界

原文只写了“修复后各项指标正常”,没有披露修复后的卡顿率、线程数和 FD 数。因此可复核的结论到此为止:SDK 升级稳定复现了资源占用和卡顿指标的退化,二分和 CPU 统计指向新增 worker,SDK 修复消除了这些变化。“20% 回到某个百分比”这类说法,我们不自行补造。

来源:Android 深入卡顿分析与实践。

案例四:SurfaceFlinger CLIENT 合成与显示链迟到

⚠️ 校勘:旧文将这组截图直接解释为“GPU 合成很慢”。原图展示的是 CLIENT 标记、missed frame 与 SF/HWC 处理区间;下文据此保留候选机制,GPU 执行时间需另行补证。

公开 trace 观察

AndroidPerformance 的系统案例展示了 hasClientComposition、FrameMissed 和较长的 SurfaceFlinger 处理区间;放大截图还把 HWC service 执行时间列为线索。独立 GPU 执行轨道和 App 侧的完整排除,都超出了这份旧现场的范围,要靠后面的重建步骤补齐。原始 Systrace 图片仍可访问:

SurfaceFlinger GPU 合成案例一

SurfaceFlinger GPU 合成案例二

这是一份早于 FrameTimeline 的旧版 Systrace 现场。图片支持“CLIENT 合成参与时伴有 missed frame 与 SF/HWC 处理延迟”的观察;GPU 是否为最早瓶颈,以及 jank type、完整 layer 属性和 HWC validate/present 结果,需要当前平台的完整 trace 补齐。

Android 17 下怎样重建证据

在现代设备上,我们从目标 DisplayFrame 反查:

  1. App SurfaceFrame(应用 Surface 的帧记录) 是否按时提交;
  2. SurfaceFlinger 的 SurfaceFlingerCpuDeadlineMissed 或 SurfaceFlingerGpuDeadlineMissed 是否与用户看到的帧对应;
  3. HWC validate 后,哪些 layers 使用 DEVICE composition(显示硬件合成),哪些进入 CLIENT composition(SurfaceFlinger 使用 GPU 合成);
  4. CLIENT 帧中的 CompositionEngine / RenderEngine::drawLayers() 和 GPU fence 等待是否变长;
  5. 同一时刻的可见 layer 集合、格式、alpha、transform(变换)、crop(裁剪区域)、dataspace(颜色空间描述)、保护属性、刷新率和 display mode(显示模式);
  6. present fence 何时 signal(发出完成信号)。

CLIENT composition 只表示 SurfaceFlinger 需要把相关 layers 渲染进 client target,本身是正常且受支持的路径。CLIENT 分配变化、RenderEngine/GPU 时长和 missed DisplayFrame(错过截止时间的显示帧)在时间上相互对应,可形成合成压力的候选证据;还需沿 fence、线程状态和 HWC 调用定位最早迟到依赖,并用受控修改验证原因。

“屏幕上有五层、硬件只有四个 plane”这类观察离根因还有一段距离。plane 是显示硬件可独立处理的合成平面;AOSP 没有向普通应用提供固定的 overlay plane(硬件叠加平面) 数量查询接口。HWC 决策还受格式、缩放、旋转、混合、带宽和厂商策略影响。

修复

公开旧案例没有披露对应产品的代码改动和前后数据,这里不补造修复结果。针对同类现场,可验证的候选动作包括:

  • 减少不必要的独立 Surface/Window;
  • 避免让可合成 layer 带上没有视觉收益的 alpha、复杂 transform 或大面积 blur;
  • 对视频/相机检查 SurfaceView、TextureView 和 overlay 的实际选择;
  • 系统侧检查 HWC capability(硬件合成能力)、validate/present(能力校验/显示提交)、client target 和驱动 fence;
  • 固定亮度、分辨率、刷新率与 layer 集后做前后 trace。

成功标准是目标设备上的 CLIENT/DEVICE 分配或 GPU 工作发生预期变化,同时 SurfaceFlinger jank type 和 present 的高分位耗时改善;App 主线程变短之外,还要看到显示侧指标的收敛。

Android 17 源码锚点是 SurfaceFlinger.cpp、CompositionEngine 的 Output.cpp、RenderEngine 和 HWComposer.cpp。详细步骤见 HWC Overlay Plane 与合成降级排查。

来源:Android 系统平台性能案例、Hardware Composer HAL。

案例五:Netmarble 根据热反馈维持持续帧率

现场与公开结果

Android Developers 的 Netmarble 案例介绍了《Game of Thrones: Kingsroad》在长时间高负载后出现热限制和帧率波动。团队逐项测量画质设置,发现动态分辨率比阴影、纹理等选项更适合承担主要的降负载任务;随后根据 ADPF Thermal API(温控接口) 同时调整分辨率和目标帧率。

官方案例披露的结果为:

  • 平均 thermal headroom(预测热状态接近严重限温阈值程度的指标) 从 1.04 降到 0.92,降幅 11%;
  • 未接入该策略时,热限制区间的帧率会在约 40–56 FPS 波动;
  • 接入后,持续帧率通常保持在约 50–60 FPS;
  • 动态目标帧率最低可降到 30 FPS,以避免不可持续的高负载。

这些数值来自官方开发者案例,页面没有完整列出设备覆盖、环境温度和样本分布;要用到其他游戏的 SLA(服务等级协议)上,需要按目标设备重新验证。

从现象到根因

温度升高和频率下降同时出现,还只是并存现象;要坐实 thermal 是最早原因,我们需要下面这些证据:

  • 相同内容和输入下,frame time(单帧耗时) 随会话时间变差;
  • thermal status/headroom(温控等级/热余量) 或 cooling state(冷却状态) 同期变化;
  • CPU/GPU 可用容量减少或频率上限降低;
  • 内存泄漏、后台负载、亮度和充电条件已记录;
  • 冷却或降低工作量后,持续 frame time 恢复。

Netmarble 先测量不同画质项对热负载的影响,再选择动态分辨率,从而明确收到热信号后应该减少哪项工作。

修复

Thermal API 提供 thermal status 与 headroom。应用需要自行决定降低哪些负载,例如 render scale(渲染分辨率比例)、阴影、后处理、粒子、视距、模拟频率或 target FPS(目标帧率)。调整策略应加入滞回(升档和降档使用不同阈值),并设置最短保持时间,防止系统在阈值附近反复重建资源。

热反馈怎样改变可持续工作量

图:应用根据热反馈主动调整实际工作量,并用滞回和保持时间稳定档位。Performance Hint 是调度提示;持续帧率还取决于负载、设备容量和热保护,频率与绑核由系统侧决定,提示本身给不了这个保证。

ADPF Performance Hint Session(性能提示会话) 可以报告参与周期性工作的一组线程,以及 target duration(目标耗时) 和 actual duration(实际耗时)。它向系统提供的是调度提示;频率是否锁定或提高、CPU 是否绑定,都由系统侧决定,热保护仍可能限制性能。

官方最佳实践建议进行长时间运行测试,至少覆盖 15 分钟,以观察温度和性能趋于稳定后的状态。测试还要固定亮度、充电状态、环境温度、网络和游戏内容。

效果与 Android 17 边界

这个案例的收益来自提前降低长期维持不住的负载,画质和目标帧率本身也发生了变化。比较时要同时报告画质档位、实际 render scale、目标/显示/提交帧率、功耗和 thermal headroom,只比较平均 FPS 会漏掉关键差异。

Android 17 / API 37 继续提供 Thermal API、ADPF 与 CPU/GPU headroom 相关能力。设备支持和厂商映射仍有差异;getThermalHeadroom() 返回 NaN(无有效数值) 时,要考虑调用间隔或设备不支持。相同的热状态等级也换算不成统一的 CPU/GPU 频率。

来源:Netmarble ADPF 案例、ADPF Thermal API、ADPF 最佳实践。

五个案例放在一张责任表里

案例用户侧结果最早异常证据责任边界修复类型
WeSing 进房进入阶段多次长停顿一条 UI message 聚集 312 ms 创建及其他任务应用主线程与初始化架构lazy、异步、预热、按依赖分阶段
进退房内存使用时间越长越卡对象无法释放、GC/分配压力应用生命周期;另查整机内存断引用、停动画、限制缓存
SDK 线程退化新版本卡顿率上升+30 个线程、+250 个 FD、worker CPU 上升SDK 并发与调度竞争移除无关功能、有界线程池
SF CLIENT/显示链历史截图含 missed framehasClientComposition、SF/HWC 长区间;GPU 尚需补证SurfaceFlinger/HWC/GPU按最早依赖验证 layer 条件或 HWC/驱动
Netmarble 热限制长会话帧率波动thermal headroom 与持续性能应用负载、Power/Thermal HAL(功耗/温控硬件抽象层)、SoC动态分辨率与目标帧率

表中的“责任边界”不表示团队归属。应用 layer 属性可能增加显示侧成本,系统内存压力也会放大应用 I/O;这个字段表示下一步需要哪类证据和修改权限。

Android 17 复现实验清单

App 主线程、GC 与调度

  • FrameTimeline、目标 CUJ 和应用 marker;
  • UI Thread、RenderThread、Binder、sched wakeup/switch;
  • ART GC pause、allocation、HeapTaskDaemon(ART 堆任务线程);
  • Java/native/graphics memory(Java/原生/图形内存)、heapprofd、PSI;
  • 线程 CPU time、Runnable latency、FD 与任务队列。

SurfaceFlinger 与 HWC

  • SurfaceFlinger FrameTimeline、layers、transactions;
  • target layer(目标图层) 的 buffer/frame number(缓冲区/帧序号) 与 acquire fence;
  • DEVICE/CLIENT composition、client target;
  • RenderEngine/GPU 与 present fence;
  • display id(显示屏标识)、刷新率、亮度和分辨率。

Thermal

  • thermal status/headroom、CPU/GPU headroom;
  • CPU/GPU frequency(频率)、idle、调度与 ADPF session(会话);
  • 实际画质、render scale、target FPS 与提交节奏;
  • 环境温度、充电、亮度和至少覆盖热稳定点的测试时长。

ART、AndroidX、WebView、游戏引擎、GPU/HWC 与 OEM 策略还要记录各自版本。旧 Systrace 案例可以帮助识别现象,当前结论必须由目标 build 的 Perfetto、Winscope 或厂商数据重新验证。

相关章节

参考资料

分析方法

典型场景

案例来源