阅读视图

发现新文章,点击刷新页面。
🔲 ☆

SmartPerfetto 架构文章 Q&A:8 个深度技术问答

这篇文章收集了《从 Trace 到洞察:SmartPerfetto AI Agent 的 Harness Engineering 实战》发布后收到的技术问题,以问答形式展开讨论。


Q1:为什么不用 Claude Code 的标准 Skill 系统,而要自建 YAML Skill?

提问背景: Claude Code 的 Skill 系统支持 scripts/ 目录放确定性脚本,避免 LLM 泛化。既然可以用 scripts/ 执行固定的 SQL,为什么还要自建一套 YAML Skill 系统?YAML Skill 是不是本质上是一个让性能工程师按预定义规则执行 SQL 的工具?

关键区分:两套 Skill 不在同一个层面

Claude Code Skills 和 SmartPerfetto YAML Skills 解决的是不同阶段的问题:

1
2
3
4
5
开发阶段(我写代码时):
Claude Code + Skills/Hooks → 帮我开发 SmartPerfetto

运行阶段(用户分析 trace 时):
SmartPerfetto Backend + YAML Skills → 帮用户分析性能数据

Claude Code 的 Skill 运行在开发者的终端里,是 CLI 工具的扩展。SmartPerfetto 的 YAML Skill 运行在 Express 后端的 Skill Engine 中,是 Agent 在运行时通过 MCP 工具 invoke_skill 调用的分析单元。两者的执行环境、调用方式、数据流完全不同。

即使只看「确定性执行」,YAML Skill 有几个针对性设计

1. 参数化 SQL,不是固定脚本

性能分析的 SQL 不是写死的——同一个 Skill 需要接受不同的参数(进程名、时间范围、帧 ID 列表):

1
2
3
4
5
6
7
8
9
10
steps:
- id: thread_state_distribution
type: atomic
sql: |
SELECT state, SUM(dur) as total_dur
FROM thread_state ts
JOIN thread_track tt ON ts.track_id = tt.id
WHERE tt.utid = ${main_thread_utid}
AND ts.ts BETWEEN ${start_ts} AND ${end_ts}
GROUP BY state

${main_thread_utid}${start_ts} 是 Claude 调用 invoke_skill 时传入的参数。YAML Skill Engine 做参数替换后执行 SQL。如果用 scripts/,要么写 shell 脚本接收参数拼 SQL(容易出注入问题),要么写完整的 Python/Node 脚本——复杂度比 YAML 高很多。

2. 自描述的输出格式(DataEnvelope)

1
2
3
4
5
display:
level: detail
columns:
- { name: state, type: string }
- { name: total_dur, type: duration }

每个 step 声明了输出列的名称和类型。前端根据这个 schema 自动渲染表格——duration 类型自动格式化为 ms,timestamp 类型支持点击跳转到 Perfetto 时间线。scripts/ 方式的输出是自由文本,前端没法自动渲染。

3. 可组合(composite + iterator)

一个 composite Skill 可以引用多个 atomic Skill,iterator 可以遍历数据行逐帧分析。这种组合在 YAML 中是声明式的,Skill Engine 负责编排执行。scripts/ 方式要实现同样的组合需要自己写编排逻辑。

4. 面向性能工程师,不是面向开发者

提问者说对了:YAML Skill 本质上是一个让性能工程师按预定义规则贡献分析逻辑的工具。性能工程师知道该查什么 SQL、该看什么指标,但不一定会写 TypeScript。YAML 格式让他们直接定义 SQL 查询和输出格式,不需要碰后端代码。修改后 DEV 模式刷新浏览器即可生效。

对比总结

维度Claude Code scripts/SmartPerfetto YAML Skill
运行环境开发者终端 (CLI)Express 后端 (runtime)
调用者开发者通过 /skill 命令Agent 通过 invoke_skill MCP 工具
参数化需要自己处理`${param
输出格式自由文本DataEnvelope (schema-driven)
前端渲染不涉及自动表格/图表
组合能力手动编排composite / iterator / conditional
贡献门槛需要写脚本只写 YAML + SQL

两者不是替代关系,而是在不同层面解决不同问题。


Q2:「确定性 + 灵活性混合」具体是怎么实现的?

提问背景: 文章说「已知场景用 Strategy 文件约束必检项,但每个阶段内的具体查询和深钻方向由 Claude 自主决定」。这个约束和自主之间的边界在哪里?具体是怎么做到的?

三层机制配合

这个混合设计靠三层机制配合实现:Strategy 文件定义「必须做什么」,Planning Gate 强制「先计划再执行」,Verifier 事后检查「是否真的做了」。

第一层:Strategy 文件 — 有硬约束也有软指引

以滑动分析的 scrolling.strategy.md 为例,它定义了多个分析阶段,但每个阶段的约束强度不同:

硬约束(必须执行,跳过会触发验证错误):

Phase 1.9 根因深钻是最严格的阶段,策略文件里直接用了 🔴 标记和「禁止」字样:

1
2
3
4
5
6
7
8
9
10
**Phase 1.9 — 根因深钻(🔴 强制执行,不可跳过):**

`batch_frame_root_cause` 中占比 >15% 的每个 reason_code,
**必须**选最严重的 1 帧执行深钻。
**⛔ 禁止**仅靠 batch_frame_root_cause 的统计分类直接出结论。

| 条件 | 深钻动作 |
| 任何 reason_code Q4>20% | invoke_skill("blocking_chain_analysis", ...) |
| binder_overlap >5ms | invoke_skill("binder_root_cause", ...) |
| ...

软指引(建议但可跳过):

Phase 1.5(架构感知分支)和 Phase 1.7(根因分支)用的是「建议」「改用」等措辞,Claude 可以根据实际数据决定是否执行:

1
2
3
4
5
**Phase 1.5 — 架构感知分支:**

| 架构 | 调整动作 |
| Flutter | 改用 flutter_scrolling_analysis |
| WebView | 注意 CrRendererMain 线程 |

Strategy 文件的全部内容被原文注入 System Prompt,注入时加了一行硬性说明:

1
2
场景策略(必须严格遵循)
对于以下常见场景,已有验证过的分析流水线。必须完整执行所有阶段,不可跳过。

Claude 直接在 System Prompt 中看到这些阶段定义、🔴 标记和「禁止」字样。

第二层:Planning Gate — 强制先计划,但不限制计划内容

Claude 在执行任何 SQL 查询或 Skill 调用之前,必须先调用 submit_plan 提交分析计划。没有提交计划就调用 execute_sqlinvoke_skill 会被直接拒绝:

1
2
3
4
function requirePlan(toolName: string): string | null {
if (analysisPlanRef.current) return null; // 已有计划,放行
return `必须先调用 submit_plan 提交分析计划,然后才能使用 ${toolName}`;
}

关键点在于:Gate 只要求计划存在,不要求计划和 Strategy 的阶段完全对应。 Claude 可以提交任何结构的计划——它可以把 Phase 1 和 1.5 合并,可以加入 Strategy 没提到的额外步骤,也可以根据初步数据调整深钻方向。

提交计划时,系统会做场景感知的关键词检查(比如滑动场景检查计划中是否提到了「帧」「jank」等词),但这只是 warning 级别——计划即使不包含这些词也会被接受。

这个设计的目的是:强制 Claude 在动手之前先想清楚要做什么(规划纪律),但不限制它怎么想(规划自由)。

第三层:Verifier — 多维度事后检查

计划和执行之间可能有偏差——Claude 可能提交了计划但实际跳过了某个关键步骤。Verifier 在分析结束后做多维度事后检查,以启发式行为检查为主,同时补充计划/假设/场景完整性校验:

a) 场景完整性检查——分析输出是否覆盖了场景的核心内容:

1
2
3
4
5
6
7
8
9
10
11
// 滑动场景:检查是否有显著掉帧但没做 Phase 1.9 深钻
case 'scrolling': {
const hasSignificantJank = /* 检测文本中是否提到大量掉帧 */;
const hasDeepDrill = /* 检测是否调用了 blocking_chain / binder_root_cause 等 */;
if (hasSignificantJank && !hasDeepDrill) {
issues.push({
severity: 'error',
message: '滑动分析有掉帧但缺少 Phase 1.9 根因深钻 — reason_code 只是分类标签,不是真正的根因'
});
}
}

b) 假设闭环检查——所有 submit_hypothesis 是否都有对应的 resolve_hypothesis

c) 因果链深度检查——CRITICAL/HIGH 级别的 findings 是否包含足够的因果连接词和机制性术语(启发式文本匹配)。

d) 可选的 LLM 复核——用独立的 Haiku 模型做证据支撑度验证(可关闭)。

如果检查发现 ERROR 级别问题,会触发 Correction Prompt 让 Claude 补做。

注意 Verifier 不检查 Claude 的计划阶段是否匹配 Strategy 的阶段编号——它检查的是「关键分析动作是否体现在输出中」,不是「计划格式是否正确」。

完整的约束光谱

把三层机制叠在一起,不同阶段的约束强度形成了一个光谱:

阶段Strategy 语气Planning GateVerifier 检查约束强度
Phase 1(概览)建议需要计划不单独检查
Phase 1.5(架构分支)建议不检查
Phase 1.7(根因分支)建议+条件不检查
Phase 1.9(根因深钻)🔴 必须/禁止检查是否调用了深钻工具
Phase 2(补充深钻)可选不检查
Phase 3(综合结论)必须覆盖分布检查结论完整性

general.strategy.md(未匹配到场景时的 fallback)则完全是软指引:只给了一个按用户关注方向的路由决策树(CPU → cpu_analysis,内存 → memory_analysis),没有任何必须执行的阶段。Claude 在 general 场景下有完全的自主权。

一句话总结

Strategy 文件告诉 Claude「分析滑动问题至少要做这几件事」,Planning Gate 确保它先想后做,Verifier 事后检查关键步骤有没有真的做。 但在这个框架内,具体查什么数据、用哪个工具、按什么顺序,都是 Claude 根据实际数据自主决定的。


Q3:Agent 和 Workflow 最大的区别在哪里?Agent 的能力边界在哪里、由什么决定?

提问背景: 我们在建设 Agent 的过程中,从最初默认 Agent 有能力理解并决策一切给它的 Skill,到现在几乎在 Skill 中写死了一棵决策树,这中间踩坑的出发点都是:「我们认为 Agent 有 xx 能力,但它没有」,导致其输出偏离我们的预期,于是我们不断地给 Skill 加边界,最后变成了一个写死的 Workflow。

本质区别:决策权在谁手里

Agent 和 Workflow 不是两个工具、两个框架——它们是同一个光谱的两端:

1
2
3
4
5
写死的 Workflow ◄──────────────────────────────────► 完全自主的 Agent
│ │
开发者控制每个分支 LLM 决定一切
│ │
高确定性、低灵活性 低确定性、高灵活性
维度WorkflowAgent
控制流开发者在代码中写死 if/elseLLM 自主选择下一步
工具选择预定义执行顺序LLM 根据数据按需选择
分支条件代码中的条件判断LLM 推理判断
失败处理try/catch + 重试逻辑LLM 自我反思 + 换方向
可预测性确定性高不确定性高
适应新场景需要开发者加分支可以自主探索

但工程实践中,几乎没有人处在光谱的任何一端。 纯 Workflow 无法处理未知场景;纯 Agent 在关键步骤上不可靠。实际落地的系统都在光谱的某个中间位置。

你们踩坑的根因:对 Agent 能力做了全局假设

「默认 Agent 有能力理解一切 → 发现它做不到 → 不断加约束 → 变成写死的 Workflow」——这个路径的根本问题在于:对 Agent 的能力做了全局一刀切的判断。

但 Agent 的能力在不同环节差异巨大:

能力维度LLM 可靠度该交给谁
意图理解(用户想干什么)高 ✅Agent(但简单场景可以用关键词匹配替代)
计划制定(分几步做、先做什么)中等 ⚠️需要约束框架:Strategy 文件给框架,LLM 填细节
数据收集(该查什么)中等 ⚠️半自主:Skill 定义了查什么,Agent 决定顺序和参数
数据推理(看到数据后归因)高 ✅Agent——这是 LLM 最大的价值
精确计算(数值统计)极低 ❌工具系统(SQL / Skill Engine)
自我评估(知道自己对不对)低 ⚠️外部 Verifier,不信任 Agent 自评

正确的做法不是「全局选 Agent 或全局选 Workflow」,而是按环节分配

1
2
3
4
5
场景识别     → Workflow(确定性逻辑,不需要 LLM 参与)
数据收集 → 半 Workflow(Skill 定义查什么,Agent 决定顺序和参数)
推理归因 → Agent(这是 LLM 的核心价值,给足数据就能做好)
输出格式 → Workflow(模板化,确定性)
质量验证 → Workflow(规则检查)+ Agent(LLM 复核)

SmartPerfetto 的做法:约束强度光谱

SmartPerfetto 没有在 Agent 和 Workflow 之间二选一,而是对不同分析阶段设定了不同的约束强度(详见 Q2)。这里从「能力边界」的角度重新审视这个设计:

高约束(Phase 1.9 根因深钻)——因为 Agent 在「决定是否深钻」这件事上不可靠:

1
2
3
4
5
6
7
# scrolling.strategy.md 中的 Phase 1.9

**Phase 1.9 — 根因深钻(🔴 强制执行,不可跳过):**

对 batch_frame_root_cause 中占比 >15% 的每个 reason_code,
**必须**选最严重的 1 帧执行深钻。
**⛔ 禁止**仅靠 batch_frame_root_cause 的统计分类直接出结论。

为什么要硬约束?因为我们发现 Agent 有一个系统性偏差:它倾向于在拿到概览数据后就直接出结论,跳过深钻。这不是模型不够聪明——Claude 完全有能力做根因深钻——而是模型存在「路径依赖」:概览数据已经包含了统计分类(reason_code),对模型来说「直接用分类标签出结论」比「花 3 轮工具调用做逐帧深钻」的认知成本低得多。

低约束(Phase 1.5 架构分支)——因为 Agent 在「根据数据选择工具」上足够可靠:

1
2
3
4
5
6
7
# scrolling.strategy.md 中的 Phase 1.5

**Phase 1.5 — 架构感知分支:**

| 架构 | 调整动作 |
| Flutter | 改用 flutter_scrolling_analysis |
| WebView | 注意 CrRendererMain 线程 |

这里用「改用」「注意」等建议性措辞,不强制。因为架构检测的结果(Flutter/WebView/Standard)已经被确定性代码放进了 system prompt,Agent 看到这个信息后选择正确 Skill 的概率很高。

零约束(general 场景)——因为 Agent 在「未知场景自主探索」上是唯一选择:

1
2
3
4
5
6
7
# general.strategy.md — 只给路由决策树,不给任何必须执行的步骤

场景: general
priority: 99

当前查询未匹配到特定场景策略。请根据用户关注的方向,
使用以下决策树选择合适的分析路径。

general 场景没有任何硬约束,因为进入 general 意味着用户的问题超出了预定义场景,Workflow 无法处理。此时只能信任 Agent 的自主探索能力。

Agent 能力边界的决定因素

Agent 的能力边界不取决于模型参数量或 benchmark 分数,而取决于三个工程因素:

1. 观测能力——Agent 能”看到”什么数据

同一个模型,给它 scrolling_analysis Skill 的 L2 结构化帧数据 vs 让它自己写 SQL 查原始表,分析质量差距非常显著。Agent 的上限由你给它的数据工具决定。SmartPerfetto 用 164 个 YAML Skill 封装了领域专家的查询逻辑,Agent 通过 invoke_skill 拿到的是处理过的、结构化的分析数据,而不是原始的百万行 trace 事件。

2. 约束框架——Agent 在什么范围内决策

不约束的 Agent 像一个没有任务清单的实习生——知识够但不知道该先做什么。Strategy 文件、Planning Gate、Verifier 多层机制共同定义了 Agent 的决策边界:Strategy 告诉它「至少要做什么」,Planning Gate 强制它「先想后做」,Verifier 事后检查「分析是否充分」(启发式检查 + 假设闭环 + 场景完整性 + 可选的 LLM 复核)。

3. 反馈质量——Agent 做错后能否被纠正

Agent 的 findings 中存在相当比例的问题(浅层归因、假阳性、遗漏关键步骤)。单纯依赖模型自我纠错效果有限。SmartPerfetto 用多层验证 + 外部纠错 prompt 来闭环:

1
2
3
Verifier 发现 ERROR → 生成 Correction Prompt → 触发 SDK 重试
↑ ↓
Learned patterns ← 累积历史误判模式

补充:Strategy 文件就是 SOP,这没问题

有人会指出:scrolling.strategy.md 读起来就像一份 SOP(标准作业程序)——有编号的 Phase、条件表、必做项、甚至直接写了 invoke_skill("scrolling_analysis", {...})。这和「在 Skill 里写死决策树」有什么区别?

直说:在数据收集阶段,SmartPerfetto 的滑动分析就是一个 Workflow。 Strategy 文件就是 SOP,它把领域专家的分析经验编码成了确定性步骤。这是故意的。

关键在于理解 SOP 覆盖了什么、没覆盖什么:

SOP 能覆盖的(数据收集)——Strategy 文件干的事:

1
2
3
4
5
scrolling.strategy.md:
Phase 1: "调用 scrolling_analysis" ← 写死了收集什么数据
Phase 1.5: "Flutter 改用 flutter_scrolling_analysis" ← 写死了条件分支
Phase 1.7: 条件表 → 深钻动作 ← 写死了 if/then 表
Phase 1.9: "🔴 占比>15% 的 reason_code 必须深钻" ← 写死了必做项

SOP 写不出来的(推理归因)——Agent 的价值所在:

1
2
3
4
- 47 帧掉帧中,哪些帧是同一根因?(数据聚类)
- 19 帧 workload_heavy 里,哪帧"最严重"值得深钻?(优先级判断)
- 深钻发现 Binder 阻塞 23ms + 热降频同时存在,因果方向是什么?(因果推理)
- 最终结论怎么组织?给 App 开发者 vs 平台工程师的建议如何区分?(表达决策)

不同场景的 SOP 程度不同:

Strategy 文件SOP 程度原因
scrolling.strategy.md — Phase 编号 + 条件表 + 必做项滑动分析方法论最成熟,最优数据收集路径已知
startup.strategy.md中高 — 有 Phase 结构,深钻方向更开放启动场景更多样(冷/温/热、不同瓶颈)
anr.strategy.md — 2-skill pipeline,但根因分析全靠推理ANR 根因高度多样
general.strategy.md — 只有路由决策树,无必做项未知场景,无法 SOP 化

scrolling.strategy.md 是 SOP 程度最高的,因为滑动分析方法论最成熟。general.strategy.md 几乎没有 SOP,因为用户问题完全不可预测。

所以正确的理解是:SmartPerfetto = 「SOP 驱动的数据收集 + Agent 驱动的推理归因」。

SOP 解决「分析滑动问题至少要看哪些数据」——这个问题有确定性答案,用 SOP 是对的。Agent 解决「拿到数据后怎么推理因果、怎么组织结论」——这个问题每个 trace 不同,写不成 SOP。

回到你们的踩坑:问题不是「Skill 变成了 SOP」——数据收集阶段就应该用 SOP。问题是「SOP 吃掉了推理」——如果 SOP 连结论都写死了(”看到 X 就输出 Y”),Agent 就真的退化成 Workflow 了。关键是让 SOP 止步于数据收集,把推理留给 Agent。

一句话总结

Agent 和 Workflow 的区别不是「智能 vs 写死」,而是「决策权的分配方式」。Agent 的能力边界由「观测能力 × 约束框架 × 反馈质量」共同决定。正确的做法是按环节分配决策权——在 Agent 可靠的环节给自主权,在 Agent 不可靠的环节加约束——而不是全局一刀切。


Q4:Agent 的架构需要从业务视角进行改进吗?

提问背景: Agent 的架构有过不同的设计和演进——从最初的 ReAct 架构,到 LangGraph 的节点式架构,不同的 Agent 架构设计会给它带来怎样的影响?在搭建自己的业务 Agent 时,是否需要考虑架构对 Agent 性能的影响?例如 SmartPerfetto 是在 Claude Agent SDK 的基础上基于业务理解加入了不同的 Skill 加载模式。

三种主流架构的本质差异

架构控制流模型开发者角色适合场景
ReAct线性循环:Think → Act → Observe → Think…定义工具工具少、路径短的简单任务
LangGraph 节点式DAG 图:节点=步骤,边=条件跳转设计图结构 + 定义节点 + 写跳转条件步骤明确、分支有限的确定性流程
SDK 原生SDK 管理 turn loop,开发者只定义 tools定义工具 + 注入上下文工具多、路径不可预测、需要 LLM 自主编排

它们的核心区别在于「谁来决定下一步做什么」

  • ReAct:LLM 在每一步都做完整决策(想什么、做什么、用什么工具),框架只负责转发
  • LangGraph:开发者预定义了所有可能的路径(节点+边),LLM 只在节点内部做局部决策
  • SDK 原生:SDK 管理对话循环,LLM 自主选择工具,开发者通过 system prompt 和工具设计来间接约束

SmartPerfetto 选择 SDK 原生 + 自建约束层的原因

SmartPerfetto 的架构是 Claude Agent SDK(SDK 原生)+ 三层约束(Strategy/Planning Gate/Verifier)

1
2
3
4
5
6
7
8
Claude Agent SDK 提供:                SmartPerfetto 自建:
├─ Turn loop(自动管理多轮对话) ├─ Scene Classification(场景路由,<1ms)
├─ Tool dispatching(MCP 工具调用) ├─ Strategy Injection(按场景注入分析策略)
├─ Streaming(SSE 事件流) ├─ Planning Gate(强制先规划再执行)
├─ Session resume(多轮上下文恢复) ├─ Verifier(事后验证 + 纠错重试)
└─ Sub-agent orchestration ├─ ArtifactStore(3 级缓存压缩 token)
├─ Conditional Tool Loading(按场景注入/隐藏工具)
└─ Cross-Session Memory(模式记忆 + 负面记忆)

为什么不用 LangGraph?

性能分析的根因推理路径是不可预测的。同样一个「滑动卡顿」,根因可能是:

  • Binder 阻塞 → 需要追踪 system_server 端的线程状态
  • GPU 渲染慢 → 需要查 GPU frequency 和 fence wait
  • GC 暂停 → 需要看 Java heap 和 GC events
  • 热降频 → 需要查 thermal zone 和 CPU frequency
  • 锁竞争 → 需要查 monitor contention
  • 以上多个原因组合

如果用 LangGraph,你需要为每一种根因路径预定义一个 DAG 节点和跳转条件。性能分析有 21 个 reason_code,每个可以组合深钻——排列组合后的路径数量使得 DAG 图变得不可维护。

更根本的问题是:在看到数据之前,你不知道该走哪条路径。LangGraph 的 DAG 图假设开发者能提前预知所有分支条件,但性能分析的分支条件取决于运行时数据。

SDK 原生架构的优势:

LLM 自主选择工具路径,但通过三层约束确保关键步骤不被跳过:

1
2
3
4
5
6
7
8
9
# LangGraph 需要预定义的 DAG:
graph.add_edge("overview", "check_binder")
graph.add_edge("overview", "check_gpu")
graph.add_edge("overview", "check_gc")
# ... 每加一种根因就要改图结构

# SmartPerfetto 的 Strategy 只需要声明:
# "对占比 >15% 的每个 reason_code,必须选最严重的 1 帧执行深钻"
# Agent 自主决定具体调哪个深钻工具

关键业务导向的架构设计

以下是 SmartPerfetto 基于业务理解做的架构改进,每一个都直接对应一个业务问题:

1. 条件化工具加载——减少 Agent 的决策空间

SmartPerfetto 的 MCP 工具总数最多 20 个(9 个 always-on + 11 个条件注入),但一次分析只注入其中的子集:

1
2
3
4
5
6
7
8
9
10
11
// claudeMcpServer.ts — 按模式切换工具集

if (options.lightweight) {
// 事实性查询(如"帧率是多少"):只给 3 个工具
toolEntries = [executeSql, invokeSkill, lookupSqlSchema];
} else {
// 完整分析:9 个 always-on (含 recall_patterns) + 按上下文条件注入
// 比较模式 → 注入 compare_skill, execute_sql_on, get_comparison_context
// 假设管理 → 注入 submit_hypothesis, resolve_hypothesis, flag_uncertainty
// 规划工具 → 注入 submit_plan, update_plan_phase, revise_plan
}

业务原因: 工具越多,Agent 选错工具的概率越大。一个只需要快速回答「帧率是多少」的查询,如果看到十几个规划/假设/比较工具,Agent 可能会过度分析。

2. Sub-Agent 场景门控——避免不必要的并行开销

1
2
3
4
5
6
7
8
9
10
// claudeAgentDefinitions.ts — 只在复杂场景启用 sub-agent

const ORCHESTRATOR_ONLY_TOOLS = new Set([
'submit_plan', 'update_plan_phase', 'revise_plan',
'submit_hypothesis', 'resolve_hypothesis', 'flag_uncertainty',
'compare_skill', 'execute_sql_on', 'get_comparison_context',
]);

// Sub-agent 只拿到数据收集工具,不拿规划/假设工具
// 设计原则:sub-agents collect evidence, orchestrator makes diagnosis
场景Sub-Agent 配置原因
scrollingframe-expert + system-expert帧分析和系统分析适合拆分,由 orchestrator 协调
startupstartup-expert + system-expert启动阶段分析和资源竞争分析适合拆分
anr无 sub-agentANR 是 2-skill 管线,额外 sub-agent 反而增加开销

注:sub-agent 的实际并行性取决于 SDK 内部调度策略,我们按并行采证来设计 prompt,但实际执行可能是串行的。

3. Lightweight vs Full 双模式——快问快答不走完整管线

当用户问「这个 trace 的帧率是多少」时,不需要走完整的 Planning → Skill → Verification 管线。SmartPerfetto 的 ClaudeRuntime 在入口处做复杂度分类:

1
2
3
4
5
6
7
8
9
analyze(query)

queryComplexity === 'quick'
analyzeQuick(): 3 个工具,无 Planning Gate,无 Verifier
→ 直接回答事实性问题

queryComplexity === 'full'
→ 完整管线:PlanningSkillVerificationCorrection
→ 系统化分析

业务原因: 用户的提问中有相当比例是事实性查询(「帧率多少」「有没有 ANR」),走完整管线会不必要地增加延迟。

对「是否需要从业务角度改进架构」的回答

需要,但改进方向不是换底层框架(ReAct → LangGraph),而是在现有框架上加业务约束层。 具体建议:

  1. 从你的 Skill 决策树中提取 Strategy 文件:把写死在代码里的 if/else 决策逻辑,改写为自然语言的分析策略(Markdown 文件),按场景注入 system prompt。这样领域专家可以直接修改分析逻辑,不需要碰代码。

  2. 加 Planning GaterequirePlan() 的实现极其简单(不到 10 行代码),但效果显著——强制 Agent 先想后做,经验上能大幅减少跑偏。

  3. 加事后 Verifier:不检查 Agent 的中间步骤是否「正确」(这个很难判断),只检查关键步骤是否「发生了」(这个很容易判断)。

  4. 按场景/复杂度动态调整工具集和约束强度:不是所有查询都需要同样的分析深度,给简单查询开一条快速通道。

一句话总结

架构选择是业务问题,不是技术问题。ReAct/LangGraph/SDK 只是控制流的不同实现方式——真正影响 Agent 性能的是你在控制流之上搭建的约束层。SmartPerfetto 选择 SDK 原生不是因为它最先进,而是因为性能分析的根因路径不可预测,预定义 DAG 不如让 Agent 在约束框架内自主探索。


Q5:性能 AI 智能体应该如何做好场景识别?

提问背景: 想做场景识别、路由到正确的 Skill,在建设这部分的时候应该更多基于「用户原声」还是「日志」,或者有什么更好的方法?场景识别的几条路径各有问题:代码匹配(关键词匹配会导致筛选结果过大或过小)、LLM 理解(LLM 的理解不一定准确)、日志还原(能筛选有无滑动,但不一定是用户关注的问题)。

SmartPerfetto 的做法:三层信号、分工明确

SmartPerfetto 的场景识别不是靠单一信号源,而是三层信号配合,每层解决不同的问题:

1
2
3
Layer 1: 用户原声 — 关键词匹配 → 场景类型(scrolling / startup / anr / ...)
Layer 2: Trace 数据 — 确定性检测 → 架构信息(Flutter / WebView / Standard / Compose)
Layer 3: 数据完整性 — 表存在性检查 → 可用分析维度(有无 GPU 数据、有无热降频数据)

Layer 1:用户原声(关键词匹配,<1ms)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// sceneClassifier.ts — 46 行代码,完成了全部场景分类

export function classifyScene(query: string): SceneType {
const scenes = getRegisteredScenes(); // 从 12 个 .strategy.md 的 frontmatter 加载
const lower = query.toLowerCase();

const sorted = scenes
.filter(s => s.scene !== 'general')
.sort((a, b) => a.priority - b.priority); // ANR(1) > startup(2) > scrolling(3) > ...

for (const scene of sorted) {
// 先匹配 compound patterns(更具体,如「启动.*慢」)
if (scene.compoundPatterns.some(p => p.test(query))) return scene.scene;
// 再匹配单关键词
if (scene.keywords.some(k => lower.includes(k))) return scene.scene;
}
return 'general'; // 兜底
}

关键词定义在每个 Strategy 文件的 YAML frontmatter 中,不是硬编码在 TypeScript 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# scrolling.strategy.md frontmatter
keywords:
- 滑动
- 卡顿
- 掉帧
- jank
- scroll
- fps
-
- frame
- 列表
- 流畅
- fling
- stuttering
- dropped frame
- 不流畅
# ... 共 30+ 关键词

为什么用关键词匹配而不是 LLM?

  1. 成本:场景分类在每次分析的入口处执行,关键词匹配 <1ms + 0 tokens;LLM 调用 ~500ms + ~500 tokens
  2. 确定性:分类错误的代价非常高(会注入错误的 Strategy 文件),关键词匹配的行为完全可预测
  3. 足够准确:在性能分析领域,用户的提问高度格式化——说「滑动卡顿」的就是在问滑动,说「启动慢」的就是在问启动。不需要 LLM 来”理解”

关键词匹配不够的地方怎么办?

关键词匹配的确有边界——当用户说「这个 app 为什么慢」时,关键词无法确定是启动慢还是滑动慢。SmartPerfetto 的处理方式是:匹配不到就 fallback 到 general 场景,让 Agent 在 general 策略的路由决策树中自主选择方向。

1
2
3
4
5
6
# general.strategy.md — 没有任何硬约束,只给路由建议

| 用户关注方向 | 推荐路径 |
| CPU / 调度 / 线程 | invoke_skill("cpu_analysis") |
| 内存 / OOM / 泄漏 | invoke_skill("memory_analysis") |
| 不确定方向 | invoke_skill("scene_reconstruction") 按场景路由 |

这个设计的核心思路是:不要试图在入口处 100% 准确分类,而是让准确的情况走快速路径(关键词 → Strategy),不确定的情况走探索路径(general → Agent 自主路由)。

Layer 2:Trace 数据——架构检测(确定性代码)

场景分类只解决了「用户想分析什么」,但同一个场景(如滑动)在不同渲染架构下的分析路径完全不同:

架构渲染管线分析差异
Standard AndroidUI Thread → RenderThread → SurfaceFlinger主线程 + RenderThread 双线分析
Flutter TextureView1.ui → 1.raster → JNISurfaceTexture → RenderThread updateTexImage双出图管线,需要分析 Flutter engine 线程 + 纹理桥接
Flutter SurfaceView1.ui → 1.raster → BufferQueue → SurfaceFlinger单出图管线,不经过 RenderThread
WebViewCrRendererMain → Viz CompositorChromium 渲染管线,线程名不同
ComposeUI Thread (Composition) → RenderThread和 Standard 类似但有 Composition 阶段

架构检测委托给 YAML skill rendering_pipeline_detection——它在 SQL 层做线程/Slice 信号采集、管线打分、子变体判定,支持 24 种细粒度渲染架构。TypeScript 侧(architectureDetector.ts)只负责调用 skill 和映射结果,不做直接的 if/else 判断:

1
2
3
4
rendering_pipeline_detection skill (SQL)
→ 采集线程信号(1.ui / 1.raster / CrRendererMain / RenderThread ...)
→ 管线打分(FLUTTER_TEXTUREVIEW / WEBVIEW_BLINK / ANDROID_VIEW_STANDARD ...)
→ architectureDetector.ts 映射为 ArchitectureInfo 类型

检测结果被注入到 system prompt 中(通过 arch-flutter.template.md 等模板),Agent 看到架构信息后选择对应的分析工具。

Layer 3:数据完整性——能力寄存器

Trace 采集配置不同,可用的数据维度也不同。有的 trace 没有 GPU frequency 数据,有的没有 thermal zone。SmartPerfetto 在分析开始前探测 18 个数据维度的可用性:

1
2
3
4
5
frame_rendering: ✅ (456 rows)
cpu_scheduling: ✅ (12000 rows)
gpu: ❌ (无 gpu_frequency counter)
thermal_throttling: ✅ (4 zones)
binder_ipc: ✅ (890 transactions)

这个信息同样注入 system prompt,告诉 Agent 哪些维度可以分析、哪些维度数据缺失。避免 Agent 调用没有数据支撑的 Skill 后得到空结果再换方向——这种试错浪费 1-2 个工具调用的 token。

对提问中三条路径的评价

1. 代码匹配(关键词):可以用,但要设计好兜底

提问者说「关键词匹配会导致筛选结果过大或过小」。SmartPerfetto 的经验是:

  • 优先级排序解决「过大」问题:ANR(1) > startup(2) > scrolling(3),同时命中多个关键词时取优先级最高的
  • Compound patterns 解决精度问题:/启动.*慢/ 比单独匹配「启动」更精确
  • general 兜底解决「过小」问题:匹配不到就不猜,交给 Agent 自主探索

2. LLM 理解:不建议用在分类入口,可以用在兜底路径

LLM 的分类在 SmartPerfetto 中不是 Layer 1,而是 general 场景中 Agent 的自主路由——这时 Agent 已经拿到了 trace 数据,可以结合数据做更准确的判断。

3. 日志还原:适合做 Layer 2 的补充信号

日志能告诉你「trace 中有什么」(有无滑动事件、有无 ANR),但不能告诉你「用户关心什么」。SmartPerfetto 的数据完整性探测就是这个角色——它不参与场景分类,但为 Agent 提供数据可用性信息。

一句话总结

场景识别不要试图用一个信号源解决所有问题。用关键词匹配做快速路由(准确的情况),用 general 兜底做 Agent 自主探索(不确定的情况),用 trace 数据做架构和完整性补充。关键词匹配 + 优先级排序 + compound patterns + 兜底策略,46 行代码就够了。


Q6:「确定性步骤 + AI 自主探索」,如何更好地发挥 AI 自主探索能力?

提问背景: 目前线上 Skill 运行的时候发现,AI 自主探索、下钻根因容易跑偏,给出不正确的结果。比如 SmartPerfetto 博客中提到的例子——在前期定位到 RenderThread 被 Binder 阻塞后(基于确定性步骤),后面的多个假设形成和验证是纯 AI 发挥吗,还是说针对 Binder 阻塞我们可能给 AI 一些常见的原因作为引导让它自己排查?

先回答核心问题:不是纯 AI 发挥,也不是写死引导

SmartPerfetto 的做法是结构化推理框架 + 按需知识注入

1
2
3
4
5
6
7
8
9
10
11
确定性步骤产出数据

Agent 形成假设(自主,但有推理框架约束)

Agent 选择验证工具(自主,但有 Strategy 建议)

验证结果反馈

假设成立 → 深入 / 假设不成立 → 回退换方向

Verifier 事后检查

三个关键机制让 AI 自主探索变得更可靠:

机制 1:假设管理工具——给推理过程加结构

SmartPerfetto 提供了 submit_hypothesisresolve_hypothesis 两个 MCP 工具,不是让 Agent 在内心独白中隐式推理,而是强制外显化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Agent 调用:
submit_hypothesis({
description: "system_server Binder 响应慢导致 RenderThread 阻塞",
expected_evidence: "system_server 端对应 Binder 事务的 thread_state 显示长时间 Runnable/Sleeping"
})

Agent 调用:
execute_sql("SELECT ... FROM thread_state WHERE utid = ... AND ts BETWEEN ...")

Agent 调用:
resolve_hypothesis({
id: "h1",
outcome: "rejected",
evidence: "system_server 端 Binder 线程状态正常,响应耗时 <2ms,
RenderThread 阻塞原因是 dequeueBuffer 等待 SurfaceFlinger"
})

为什么有效? 假设管理工具迫使 Agent 在行动之前明确声明「我在验证什么」和「我预期看到什么」。这有两个好处:

  1. 防止目标漂移——Agent 不会在收集数据的过程中忘了自己最初想验证什么
  2. 可审计——每个假设都有完整记录,Verifier 可以检查是否所有假设都被 resolved

机制 2:知识注入——不是写死引导,是按需加载领域知识

“针对 Binder 阻塞我们可能给 AI 一些常见的原因作为引导吗?”

是的,但不是写死在 Skill 中,而是通过 lookup_knowledge MCP 工具按需加载。Agent 发现 Binder 阻塞后,可以调用:

1
invoke lookup_knowledge("binder-ipc")

返回一份 Binder IPC 的知识模板(knowledge-binder-ipc.template.md),包含:

  • Binder 事务的典型阻塞原因分类(server 端忙、进程冻结、CPU 调度延迟、oneway 队列满)
  • 每种原因的排查路径和关键指标
  • 常见误判场景(如 oneway 事务不会阻塞调用方)

关键设计:知识是 Agent 主动拉取的,不是系统强制注入的。 目前共有 8 个知识模板(rendering-pipeline、binder-ipc、gc-dynamics、cpu-scheduler、thermal-throttling、lock-contention、startup-root-causes、data-sources),如果在 system prompt 中预先注入全部模板,会消耗大量 token 且大部分不相关。通过 MCP 工具按需加载,Agent 只在需要时才获取对应领域的背景知识。

SmartPerfetto 还在 Strategy 文件中提供了条件化的深钻建议表,这也是一种引导:

1
2
3
4
5
6
7
8
# scrolling.strategy.md Phase 1.9

| 条件 | 深钻动作 |
| 任何 reason_code Q4>20% | invoke_skill("blocking_chain_analysis", ...) |
| binder_overlap >5ms | invoke_skill("binder_root_cause", ...) |
| cpu_runnable_ratio >30% | invoke_skill("cpu_analysis", ...) |
| thermal_throttle detected | invoke_skill("thermal_throttling", ...) |
| gc_pause_total >10ms | invoke_skill("gc_analysis", ...) |

这个表不是写死的决策树——它是给 Agent 的查找表。Agent 看到数据后,根据数据中的指标值决定走哪一行。如果数据不匹配任何一行,Agent 可以自主探索。

机制 3:ReAct Reasoning Nudge——在工具返回时触发反思

在数据工具(execute_sql / invoke_skill)成功返回的前几次调用中,SmartPerfetto 在结果末尾附加一段推理提示:

1
2
3
4
5
6
7
8
// claudeMcpServer.ts

const REASONING_NUDGE = '\n\n[REFLECT] 在执行下一步之前:' +
'这个数据的关键发现是什么?是否支持/反驳你的假设?' +
'如有重要推断,请用 submit_hypothesis 或 write_analysis_note 记录。';

// 只在前 N 次数据工具调用时附加,之后停止(控制 token 成本)
const REASONING_NUDGE_MAX_CALLS = 4;

成本极低(~20 tokens/次,前 4 次共 ~80 tokens),但效果显著。 之所以不是全程附加,是为了在分析后半段控制 token 开销——前几次 nudge 已经建立了「收到数据 → 先反思 → 再行动」的模式。没有这个 nudge,Agent 倾向于连续调用工具而不停下来思考——收集了 5 次数据但没有形成任何中间结论,最后的总结质量很差。

用图片中的 Binder 例子走一遍完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
1. 先看总览 → 发现 47 帧卡顿,P90 = 23.5ms
[确定性:invoke_skill("scrolling_analysis")]

2. 根据总览决定方向 → 40% 卡在 APP 阶段,优先看 App 侧
[确定性:数据驱动,Strategy 文件建议]

3. 选代表帧深钻 → Frame #234 RenderThread 被 Binder 阻塞 23ms
[确定性:invoke_skill("jank_frame_detail")]

--- 以下进入 AI 自主探索 ---

4. Agent 主动加载知识:lookup_knowledge("binder-ipc")
→ 获取 Binder 阻塞常见原因分类表
[AI 决策 + 知识引导]

5. Agent 形成假设:submit_hypothesis("system_server Binder 响应慢")
预期证据:server 端 thread_state 长时间 Sleeping/Runnable
[AI 推理,假设工具强制外显]

6. Agent 验证:execute_sql("查 Binder 对端 thread_state")
→ 发现 system_server CPU 调度延迟,不是 Binder 响应慢
[AI 自主工具调用]

7. [REFLECT] nudge 触发反思
Agent: "假设 1 不成立,server 端 thread_state 显示 Runnable 排队,
真正原因是 CPU 调度延迟"
→ resolve_hypothesis(outcome: "refined",
evidence: "system_server CPU 调度延迟导致 Binder 线程排队")
[AI 推理 + 知识模板中的排查路径]

8. Agent 深入:execute_sql("查 CPU frequency + thermal zone")
→ 发现热降频,CPU 大核被限频到 50%
[AI 自主选择下一步深钻方向]

9. 综合结论:RenderThread Binder 阻塞 ← system_server CPU 调度延迟 ← 热降频
[AI 归纳,形成 WHY 链]

--- Verifier 事后检查 ---

10. Verifier 启发式检查:
- 文本模式匹配:结论中是否体现深钻分析(而非仅引用概览数据)
- 假设闭环:所有 submit_hypothesis 是否都有对应的 resolve_hypothesis
- 场景完整性:滑动分析是否包含帧/卡顿相关内容
- 因果链启发式:是否有足够的因果连接词和机制性术语
[确定性:启发式规则检查,非精确验证]

注意 Step 4-9 全部是 AI 自主探索,但受到三个约束:

  • 假设工具迫使推理外显化(Step 5, 7)
  • 知识注入提供领域排查路径(Step 4)
  • REFLECT nudge在前几次工具返回后触发反思(Step 7)

如何让 AI 自主探索更可靠:四个实操建议

1. 给数据,不给结论

Skill 应该返回结构化数据(帧耗时、线程状态分布、阻塞函数列表),而不是已经得出的结论(「RenderThread 阻塞是因为 Binder」)。让 AI 自己从数据中推理结论,比让它基于别人给的结论做进一步分析更可靠。

2. 给框架,不给路径

Strategy 文件应该定义「必须做什么」(Phase 1.9 必须深钻),而不是「怎么做」(先查 A 再查 B 再查 C)。Agent 在有框架约束的情况下自主选择路径,比在没有任何约束下自由探索要可靠得多,又比写死路径灵活得多。

3. 给知识,不给答案

知识模板应该包含「可能的原因分类和排查方法」,而不是「如果看到 X 就是 Y」。前者帮助 Agent 建立推理框架,后者把 Agent 变回 Workflow。

4. 验证行为,不验证结论

Verifier 应该用启发式规则检查「分析输出是否体现了关键动作」(结论中是否有深钻分析痕迹、假设是否都已 resolved、因果链是否有足够深度),而不是试图判断「结论是否正确」(这个你在离线评估中用 LLM Judge 做,不应该在运行时做)。注意这是文本模式匹配级别的启发式检查,不是精确的工具调用日志审计。

一句话总结

AI 自主探索的可靠性不是靠「写死引导」来保证的,而是靠三个机制:假设管理工具让推理外显化、按需知识注入提供领域排查框架、ReAct nudge 防止盲目工具调用。关键原则是「给数据不给结论、给框架不给路径、给知识不给答案」。


Q7:每一轮的 Prompt 是怎么拼接的?

提问背景: 大模型 Agent 的输出质量很大程度取决于 system prompt 的设计。SmartPerfetto 在每次分析时是如何构造 prompt 的?不同场景、不同轮次之间 prompt 有什么变化?token 预算怎么控制?

总体设计:四层分层拼接 + 缓存优化

SmartPerfetto 的 system prompt 不是一个静态字符串,而是由 buildSystemPrompt() 函数(claudeSystemPrompt.ts:260)在每次 SDK 查询前动态拼接的。拼接遵循一个核心原则:

按「稳定性」排序——越不容易变化的内容越靠前,越动态的内容越靠后。

这个设计的原因是 Anthropic API 的自动缓存机制:system prompt 超过 1024 tokens 时,API 会自动对 prompt 前缀做缓存。如果把不变的部分放在最前面,多轮对话中大部分 prompt 可以命中缓存,显著降低延迟和成本:

1
2
3
Same trace + same scene:     ~4000 tokens cached (~80% savings)
Same trace + different scene: ~800 tokens cached (~18% savings)
Different trace: ~400 tokens cached (~8% savings)

四层拼接结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
┌───────────────────────────────────────────────────────┐
│ Tier 1: STATIC(进程生命周期内不变) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ prompt-role.template.md (~200 tokens) │ │
│ │ → 角色定义:Android 性能分析专家 │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ prompt-output-format.template.md (~850 tokens) │ │
│ │ → 输出格式:[CRITICAL]/[HIGH]/[MEDIUM]/[LOW] │ │
│ │ → 根因推理链格式、Mermaid 因果链规则 │ │
│ │ → Slice 嵌套规则、CPU 频率估算指南 │ │
│ └─────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────┤
│ Tier 2: PER-TRACE(同一 trace 内稳定) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 架构信息 (~150 tokens) │ │
│ │ → "Flutter TextureView, 置信度 92%" │ │
│ │ → + arch-flutter.template.md 架构指南 │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 焦点应用 (~100 tokens) │ │
│ │ → "com.example.app (主焦点), 帧数 456" │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 数据完整性 (~200 tokens, 可被丢弃) │ │
│ │ → 只报告缺失/不足的维度,可用维度不报告 │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ SQL 知识库参考 (~300 tokens, 可被丢弃) │ │
│ │ → 从 Perfetto stdlib 索引匹配到的表/视图/函数 │ │
│ └─────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────┤
│ Tier 3: PER-QUERY(场景/查询变化时变化) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 方法论 + 场景策略 (~1200 tokens) │ │
│ │ → prompt-methodology.template.md │ │
│ │ → {{sceneStrategy}} = scrolling.strategy.md │ │
│ │ 或 startup/anr/general 等 12 套策略之一 │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 子代理协作指南 (~200 tokens, 可被丢弃) │ │
│ │ → 何时委托 vs 直接调用 │ │
│ │ → 滑动场景专用并行证据收集指南 │ │
│ └─────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────┤
│ Tier 4: PER-INTERACTION(每次查询都可能变化) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 用户选区上下文 (~300 tokens, 不可丢弃) │ │
│ │ → 时间范围选区 (selection-area.template.md) │ │
│ │ → 或 Slice 选区 (selection-slice.template.md) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 比较模式上下文 (条件注入) │ │
│ │ → 双 trace 对比方法论 + 工具说明 │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 对话上下文 (~500 tokens) │ │
│ │ → 分析笔记 (≤10 条, 按优先级排序) │ │
│ │ → 之前的 findings (≤10 条) │ │
│ │ → 已知实体 (用于 drill-down) │ │
│ │ → 对话摘要 (跨轮压缩, ≤2000 tokens) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ SQL 踩坑记录 (≤5 条, 可被丢弃) │ │
│ │ → ERROR → BAD SQL → FIX SQL │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 历史分析经验 (跨会话模式记忆, 可被丢弃) │ │
│ │ 历史踩坑记录 (跨会话负面记忆, 可被丢弃) │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 历史分析计划 (≤3 轮, 可被丢弃) │ │
│ │ → 各阶段 ✓/⊘/○ 状态 + 摘要 │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘

模板加载与变量替换

所有 prompt 内容都在 Markdown 文件中定义(详见 Q1),TypeScript 只做加载和变量替换:

1
2
3
4
5
6
7
8
9
10
11
// strategyLoader.ts — 模板系统

// 1. 加载模板(DEV 模式跳过缓存,刷新浏览器即生效)
loadPromptTemplate('prompt-methodology') // → strategies/prompt-methodology.template.md

// 2. 加载场景策略(从 YAML frontmatter 中提取 keywords,body 作为策略内容)
getStrategyContent('scrolling') // → strategies/scrolling.strategy.md 的 Markdown body

// 3. 变量替换
renderTemplate(methodologyTemplate, { sceneStrategy })
// 将 {{sceneStrategy}} 替换为 scrolling.strategy.md 的内容

模板文件清单:

类别文件用途
静态模板prompt-role.template.md角色定义
prompt-output-format.template.md输出格式规则(91 行)
prompt-quick.template.md快速模式精简 prompt
方法论prompt-methodology.template.md分析方法论(含 {{sceneStrategy}} 占位符)
架构指南arch-standard.template.mdStandard Android 渲染指导
arch-flutter.template.mdFlutter 引擎指导
arch-compose.template.mdJetpack Compose 指导
arch-webview.template.mdWebView 指导
选区模板selection-area.template.md时间范围选区({{startNs}}, {{endNs}}…)
selection-slice.template.mdSlice 选区({{eventId}}, {{ts}}…)
比较模式comparison-methodology.template.md双 trace 对比方法论
场景策略12 个 *.strategy.mdscrolling/startup/anr/memory/…
知识模板8 个 knowledge-*.template.md按需加载的领域知识(非 prompt 注入)
辅助模板prompt-complexity-classifier.template.mdQuick/Full 分流判定(不注入 prompt,但决定走哪条路径)

Token 预算管理

预算上限: 4500 tokens(MAX_PROMPT_TOKENS)。纠错重试时,如果检测到 SDK auto-compact(对话历史被自动压缩),会将预算降到 3000 tokens 以留出空间;否则复用原始 prompt。

Token 估算方法: 混合中英文估算——中文字符按 1.5 tokens/字,ASCII 按 0.3 tokens/字。这是粗略近似,但用于预算管理足够准确。

超预算时的渐进丢弃策略:

当拼接完成后 token 数超出预算,按优先级从低到高依次丢弃整个 section:

1
2
3
4
5
6
7
8
丢弃顺序(最先丢弃 → 最后丢弃):
1. Perfetto SQL 知识库参考 → Agent 可以用 lookup_sql_schema 工具替代
2. Trace 数据完整度 → 有帮助但 Agent 运行时也能发现数据缺失
3. 历史分析经验 → 跨会话模式记忆,非关键
4. 历史踩坑记录 → 跨会话负面记忆,可丢弃
5. SQL 踩坑记录 → 锦上添花
6. 子代理协作 → 只在 sub-agent 启用时有用
7. 历史分析计划 → 补充性上下文

永不丢弃的内容:

  • 角色定义、输出格式(Tier 1 静态)
  • 架构信息、焦点应用(Tier 2 per-trace)
  • 方法论 + 场景策略(Tier 3 核心)
  • 用户选区上下文(用户的显式意图)
  • 对话上下文(之前的 findings 和分析笔记)

上下文构建的完整流程

ClaudeRuntime.analyze() 中,prompt 拼接前需要经过二十多个准备阶段(Phase)来收集所有上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Phase 0:   选区上下文日志
Phase 0.5: 焦点应用检测(3 种方法:battery_stats / oom_adj / frame_timeline)
Phase 1: Skill 执行器初始化
Phase 2: 架构检测(LRU 缓存,同 trace 只检测一次)
Phase 2.5: 厂商检测(OEM 定制化,LRU 缓存)
Phase 2.8: 比较模式上下文(双 trace 模式)
Phase 2.9: 数据完整性探测(18 个维度,~50ms)
Phase 3: 会话上下文 + 对话历史
Phase 4: 实体存储(drill-down 引用)
Phase 5: 场景分类(关键词匹配,<1ms)
Phase 5.5: 跨会话模式记忆匹配
Phase 6: ArtifactStore + 分析笔记
Phase 6.5: 分析计划(当前 + 历史)
Phase 6.6: Watchdog 反馈引用
Phase 6.7: 假设状态
Phase 6.8: 不确定性标记
Phase 7: SQL 错误追踪
Phase 8: MCP Server 创建(注入以上所有状态)
Phase 9: (已移除)
Phase 10: SQL 知识库上下文
Phase 11: Sub-agent 定义
Phase 12: SQL error-fix pairs
Phase 13: → buildSystemPrompt(context) → 最终 prompt

所有 Phase 的结果汇入 ClaudeAnalysisContext 对象,传给 buildSystemPrompt() 做最终拼接。

Quick vs Full 双模式

不是所有查询都需要完整的 4500-token prompt。当用户问事实性问题(如「帧率是多少」)时,SmartPerfetto 使用精简的 quick prompt:

1
2
3
4
// buildQuickSystemPrompt() — ~1500 tokens
// 加载 prompt-quick.template.md
// 只注入 {{architectureContext}} 和 {{focusAppContext}}
// 无方法论、无场景策略、无对话上下文
维度Quick ModeFull Mode
目标 tokens~1500~4500
场景策略12 套之一
方法论prompt-methodology.template.md
对话上下文findings + notes + entity + summary
Planning Gate
Verifier
适用场景「帧率多少」「有没有 ANR」「分析滑动卡顿」「分析启动性能」

多轮对话中 Prompt 的变化

在多轮分析中(用户追问或 drill-down),prompt 的变化取决于 SDK session 是否命中 resume

第 1 轮: 无对话上下文、无历史计划、无分析笔记

第 2 轮起 — SDK session resume 命中(4 小时内):

  • SDK 内部已持有完整对话历史,不重复注入 previousFindingsconversationSummary
  • 但仍注入:分析笔记(≤10 条)、实体上下文(drill-down 引用)、历史计划(≤3 轮)
  • Tier 1-3 保持不变,命中 ~80% 缓存

第 2 轮起 — SDK session 过期或不可用:

  • 手动注入上一轮的 findings 为「之前的分析发现」(≤10 条)
  • 手动注入对话摘要(sessionContext.generatePromptContext(2000),≤2000 tokens)
  • 分析笔记、实体上下文、历史计划同上

纠错重试轮: 如果检测到 SDK auto-compact(对话历史被自动压缩),token 预算从 4500 降到 3000,渐进丢弃会更激进地移除非关键 section。如果没有发生 auto-compact,复用原始 system prompt。

一个具体例子:滑动分析的 Prompt 拼接

用户输入 "分析滑动卡顿",Flutter TextureView 架构,第 1 轮:

1
2
3
4
5
6
7
8
9
10
[Tier 1] prompt-role.template.md"你是 Android 性能分析专家..."
[Tier 1] prompt-output-format.template.md → 输出格式规则
[Tier 2] "架构: Flutter TextureView, 置信度 92%" → + arch-flutter.template.md
[Tier 2] "焦点应用: com.example.app (主焦点)" → 帧数 + 检测方法
[Tier 2] "数据完整性: gpu ❌ 疑似未采集" → 只报缺失/不足,可用的不报
[Tier 2] SQL 知识库: android_frames, slice_self_dur → stdlib 匹配结果
[Tier 3] prompt-methodology + scrolling.strategy.md → Phase 11.51.93 完整策略
[Tier 4] (无选区、无对话上下文、无历史计划)

预估: ~3200 tokens, 在 4500 预算内, 无需丢弃

用户追问 "深入分析第 3 帧",第 2 轮(SDK session resume 命中):

1
2
3
4
5
6
7
8
[Tier 1-3] 与第 1 轮相同(命中 ~80% 缓存)
[Tier 4] 对话上下文 (SDK 已持有对话历史,不重复注入 findings):
- 分析笔记: "⚠️ [假设] 主要 jank 原因是 RenderThread Binder 阻塞"
- 实体上下文: frame#3 的 ID 和时间范围
- 历史计划: "✓ Phase 1 概览, ✓ Phase 1.9 深钻, ○ Phase 3 结论"
(previousFindings 和 conversationSummary 由 SDK session 内部管理,不在 prompt 中重复)

预估: ~3400 tokens, 在预算内

一句话总结

Prompt 按「稳定性」四层排序(Static → Per-Trace → Per-Query → Dynamic),利用 API 前缀缓存实现多轮对话 ~80% token 节省。模板系统让领域专家可以直接编辑分析策略而不碰 TypeScript。超预算时按优先级渐进丢弃,但永远保留角色定义、场景策略和用户选区——这三者决定了分析的方向和范围。


Q8:SmartPerfetto 有哪些 Skill?

提问背景: SmartPerfetto 的分析能力由 YAML Skill 承载。完整的 Skill 清单可以帮助理解系统的分析覆盖范围。

总览

分类数量说明
Atomic87单步检测/统计,一条或几条 SQL 完成
Composite29组合多个 atomic skill,支持 iterator/conditional
Deep2深度剖析(callstack、CPU profiling)
Pipeline28渲染管线检测 + 教学(24+ 种架构)
Module18模块化配置:app/framework/hardware/kernel
合计164

Atomic Skills(87 个)

单步数据提取和检测,是所有高层 Skill 的构建基础。

帧渲染与掉帧:

Skill ID一句话描述
consumer_jank_detection从 SF 消费端角度检测真正的掉帧(per-layer buffer 枯竭)
frame_blocking_calls识别每个掉帧帧期间的阻塞调用(GC、Binder、锁、IO)
frame_production_gap检测帧生产间隙:连续帧之间的 gap 超过 1.5× VSync
frame_pipeline_variance检测帧时长抖动与高方差区间
render_pipeline_latency分解帧渲染全链路各阶段耗时
render_thread_slices分析 RenderThread 的时间片分布
app_frame_production分析应用主线程的帧生产情况
sf_frame_consumption分析 SurfaceFlinger 消费帧的情况
sf_composition_in_range分析 SurfaceFlinger 合成延迟
sf_layer_count_in_range统计时间范围内 SF 活跃图层数量
present_fence_timing分析 Present Fence 时序,检测实际显示延迟
game_fps_analysis针对游戏场景的帧率分析,支持固定帧率模式

VSync 与刷新率:

Skill ID一句话描述
vsync_period_detection检测 VSync 周期,返回刷新率和置信度
vsync_config从 trace 中解析实际的 VSync 周期和刷新率设置
vsync_alignment_in_range分析帧与 VSync 信号的对齐情况
vsync_phase_alignment分析输入事件与 VSync 的相位关系,定位跟手延迟瓶颈
vrr_detection检测设备是否使用可变刷新率(VRR/LTPO/Adaptive Sync)

CPU 与调度:

Skill ID一句话描述
cpu_topology_detection从 cpufreq 动态检测 CPU 大小核拓扑
cpu_topology_view创建可复用 SQL VIEW _cpu_topology
cpu_slice_analysis分析 CPU 时间片分布(动态拓扑检测)
cpu_load_in_range分析指定时间范围内各 CPU 核心的负载情况
cpu_cluster_load_in_range计算大核簇和小核簇的整体 CPU 负载百分比
cpu_freq_timeline分析各 CPU 核心的频率变化时间线
cpu_throttling_in_range检测 CPU 热控限频情况
sched_latency_in_range分析线程调度等待时间分布,检测 CPU 争抢
scheduling_analysis分析线程调度延迟(Runnability)
task_migration_in_range分析线程在大小核之间的迁移频率
thread_affinity_violation检测主线程/RenderThread 的高频迁核行为
thermal_predictor基于 CPU 频率趋势预测热限频风险
cache_miss_impact统计 cache-miss 计数器并评估波动

GPU:

Skill ID一句话描述
gpu_render_in_range分析 GPU 渲染耗时和 Fence 等待
gpu_freq_in_range分析 GPU 频率变化情况
gpu_metrics分析 GPU 频率、利用率和渲染性能
gpu_power_state_analysis分析 GPU 频率状态切换,识别降频压力与抖动

主线程分析:

Skill ID一句话描述
main_thread_states_in_range统计区间内主线程状态、阻塞函数与占比
main_thread_slices_in_range统计区间内主线程切片耗时分布
main_thread_sched_latency_in_range统计主线程 Runnable 等待时间分布
main_thread_file_io_in_range统计区间内主线程文件 IO 相关切片耗时

Binder IPC:

Skill ID一句话描述
binder_in_range分析指定时间范围内的 Binder 事务
binder_blocking_in_range分析同步 Binder 调用中对端进程的响应延迟
binder_root_cause对慢 Binder 事务进行服务端/客户端阻塞原因归因
binder_storm_detection检测 Binder 事务风暴:短时间内过多 IPC 调用

锁与同步:

Skill ID一句话描述
lock_contention_in_range分析指定时间范围内的锁竞争情况
futex_wait_distribution统计 futex/mutex 锁等待分布与耗时

启动专用(19 个):

Skill ID一句话描述
startup_events_in_range查询启动事件及 TTID/TTFD 指标
startup_slow_reasons启动慢原因(Google 官方分类 + 自检)v3.0
startup_critical_tasks自动识别启动区间内所有活跃线程,按 CPU 时间排序
startup_thread_blocking_graph利用 waker_utid 构建线程间的 block/wakeup 关系图
startup_jit_analysis分析 JIT 编译线程对启动速度的影响
startup_cpu_placement_timeline按时间桶分析主线程核类型变化,检测启动初期被困小核
startup_freq_rampup分析冷启动初期 CPU 频率爬升速度,检测升频延迟
startup_binder_pool_analysis分析启动期间 Binder 线程池利用率和饱和度
startup_hot_slice_states分析启动区间内 Top N 热点 Slice 的线程状态分布
startup_main_thread_states_in_range统计启动阶段主线程 Running/Runnable/Blocked 占比
startup_main_thread_slices_in_range统计启动阶段主线程切片热点
startup_binder_in_range统计启动阶段 Binder 调用分布
startup_main_thread_file_io_in_range统计启动阶段主线程文件 IO
startup_sched_latency_in_range统计启动阶段主线程 Runnable 等待时延
startup_main_thread_sync_binder_in_range统计启动阶段主线程同步 Binder 耗时
startup_main_thread_binder_blocking_in_range分析启动阶段主线程同步 Binder 阻塞明细
startup_breakdown_in_range统计启动阶段各归因原因耗时占比
startup_gc_in_range统计启动阶段 GC 切片及主线程占比
startup_class_loading_in_range统计启动阶段类加载切片耗时

内存与 GC:

Skill ID一句话描述
gc_events_in_range查询给定进程的 GC 事件和可选时间范围
memory_pressure_in_range分析指定时间范围内的内存压力指标
page_fault_in_range分析 Page Fault 和内存回收对性能的影响

输入与触摸:

Skill ID一句话描述
input_events_in_range提取区间内的原始输入事件,分析分发延迟
input_to_frame_latency测量每个 MotionEvent 到对应帧 present 的延迟
touch_to_display_latency测量从触摸到帧渲染的端到端延迟
scroll_response_latency测量滚动手势从输入到首帧渲染的响应延迟

系统与设备:

Skill ID一句话描述
system_load_in_range分析系统整体 CPU 利用率和进程活跃度
device_state_snapshot采集 trace 期间的设备环境信息(屏幕、电量、温度等)
device_state_timeline追踪设备状态随时间的变化
wakelock_tracking追踪 Wake Lock 持有情况,检测电池功耗异常

其他:

Skill ID一句话描述
blocking_chain_analysis分析主线程阻塞链:谁阻塞了主线程?唤醒者在做什么?
anr_main_thread_blocking深度分析 ANR 中主线程阻塞原因
anr_context_in_range提取第一个 ANR 事件数据用作时间窗口锚点
app_lifecycle_in_range追踪 Activity/Fragment 生命周期事件
compose_recomposition_hotspot检测 Jetpack Compose 重组热点
webview_v8_analysis分析 WebView V8 引擎:GC、脚本编译、执行时间
rendering_pipeline_detection识别应用渲染管线类型(24 种细粒度检测)
pipeline_key_slices_overlay查询管线关键 Slice 的 ts/dur 用于时间线 overlay

Composite Skills(29 个)

组合多个 atomic skill,支持 iterator(逐帧/逐事件深钻)和 conditional(条件分支)。

Skill ID一句话描述
scrolling_analysis滑动分析主入口:概览 → 帧列表 → 根因分类 → 逐帧诊断
flutter_scrolling_analysisFlutter 特定帧分析,使用 Flutter 线程模型
jank_frame_detail分析特定掉帧的详细原因:深钻 jank 原因和根因分类
startup_analysis启动分析主入口:Iterator 模式、大小核分析、四象限
startup_detail分析单个启动事件:主线程耗时、Binder、CPU 大小核占比
anr_analysisANR v3.0 分析:系统问题 vs 应用问题、分类处理
anr_detail单个 ANR 事件详情:四象限、Binder 依赖、死锁检测
cpu_analysisCPU 分析:时间分布、大小核分析、调度链路
gpu_analysisGPU 分析:频率分布、内存使用、帧渲染关联
memory_analysis内存分析:GC 事件、GC 与帧关联、线程状态
gc_analysisGC 分析:基于 stdlib android_garbage_collection_events
binder_analysisBinder 深度分析:事务基础、线程状态
binder_detail单个 Binder 事务详情:CPU 大小核、四象限、阻塞原因
thermal_throttling温度监控、热节流检测、CPU 频率相关性
lock_contention_analysis锁竞争多维度分析:基于 android.monitor_contention
surfaceflinger_analysisSF 帧合成性能:GPU/HWC 合成比例、慢合成检测
click_response_analysis点击响应分析:基于 stdlib android_input_events
click_response_detail单个慢输入事件详情:延迟分解、四象限、主线程阻塞
scroll_session_analysis单个完整滑动区间:Touch 阶段 vs Fling 阶段 FPS
navigation_analysisActivity/Fragment 跳转性能:生命周期、转场动画
lmk_analysisLMK 分析:原因分布、时间线、频率
dmabuf_analysisDMA Buffer 分析:分配、释放、泄漏检测
block_io_analysisBlock IO 分析:设备级统计、队列深度、长耗时 IO
io_pressureIO 阻塞数据检测、IO Wait 时间、严重度评估
suspend_wakeup_analysis休眠/唤醒分析:时间分布、唤醒源排行
network_analysis网络分析:流量概览、应用流量、协议分布
irq_analysis硬中断和软中断的频率、耗时、嵌套情况
scene_reconstruction通过用户输入和屏幕状态还原用户操作场景
state_timeline四泳道连续状态时间线:设备/用户/应用/系统

Deep Skills(2 个)

深度剖析,通常需要更长的执行时间。

Skill ID一句话描述
cpu_profilingCPU 性能剖析:使用热点和调度效率深度分析
callstack_analysisRunning 状态下的调用栈热点分析

Pipeline Skills(28 个)

渲染管线检测 + 教学。每个 pipeline skill 对应一种渲染架构,包含管线描述、关键线程、性能指标和优化建议。

Skill ID渲染架构
pipeline_android_view_standard_blastAndroid 12+ 标准 HWUI + BLASTBufferQueue
pipeline_android_view_standard_legacyAndroid 12 前标准 HWUI + Legacy BufferQueue
pipeline_android_view_softwareCPU Skia 软件渲染,无 RenderThread
pipeline_android_view_mixedView + SurfaceView 混合渲染
pipeline_android_view_multi_window同进程多窗口(Dialog/PopupWindow)
pipeline_android_pip_freeform画中画和自由窗口模式
pipeline_compose_standardJetpack Compose + HWUI RenderThread
pipeline_flutter_textureviewFlutter PlatformView 降级模式
pipeline_flutter_surfaceview_skiaFlutter + Skia 引擎(JIT Shader)
pipeline_flutter_surfaceview_impellerFlutter + Impeller 引擎(预编译 Shader)
pipeline_webview_gl_functor传统 WebView,App RenderThread 同步等待
pipeline_webview_surface_control现代 WebView + Viz/OOP-R 独立合成
pipeline_webview_textureview_customX5/UC 等定制 WebView 内核
pipeline_webview_surfaceview_wrapperWebView 全屏视频包装模式
pipeline_chrome_browser_vizChrome Viz 合成器,多进程架构
pipeline_opengl_es直接 OpenGL ES / EGL 渲染
pipeline_vulkan_native原生 Vulkan 渲染
pipeline_angle_gles_vulkanANGLE: OpenGL ES → Vulkan 翻译层
pipeline_game_engineUnity/Unreal/Godot 等游戏引擎
pipeline_surfaceview_blast独立 SurfaceView + BLAST 同步
pipeline_textureview_standardSurfaceTexture 纹理采样/合成模式
pipeline_camera_pipelineCamera2/HAL3 多流相机渲染
pipeline_video_overlay_hwcHWC 视频层硬件加速叠加
pipeline_hardware_buffer_rendererAndroid 14+ HBR API 直接 Buffer 渲染
pipeline_surface_control_apiNDK SurfaceControl 直接事务提交
pipeline_variable_refresh_rateVRR/ARR + FrameTimeline 动态刷新率
pipeline_imagereader_pipelineImageReader API:ML 推理、录屏、自定义相机
pipeline_software_compositingSF CPU 软件合成回退(GPU 不可用时)

注:_base.skill.yaml 是 Pipeline Skill 的基础模板文件,不注册为可用 Skill,不计入总数。


Module Skills(18 个)

模块化分析配置,按层级组织。Agent 通过 list_skills 发现并按需调用。

硬件层(5 个):

Skill ID一句话描述
cpu_moduleCPU 频率、热节流和电源状态
gpu_moduleGPU 渲染、频率和显存使用
memory_module内存带宽、LMK、dmabuf、PSI、缺页
thermal_module温度传感器、热节流检测、冷却策略
power_moduleWake Lock、CPU idle、电源模式、休眠/唤醒

框架层(6 个):

Skill ID一句话描述
surfaceflinger_module帧渲染时序、卡顿原因、GPU 合成
choreographer_moduleVSync 信号、doFrame 回调、帧生产管线
ams_module应用生命周期、进程管理、启动时序
wms_module窗口动画、Activity 转场、多窗口
art_moduleGC、JIT 编译和内存分配
input_module触摸延迟、输入派发和点击响应

内核层(4 个):

Skill ID一句话描述
scheduler_module线程调度延迟、CPU 利用率、大小核分配
binder_module跨进程调用、阻塞事务、调用延迟
lock_contention_moduleMutex/Futex、Java monitor、死锁检测
filesystem_moduleBlock IO、文件操作、数据库、SharedPreferences

应用层(3 个):

Skill ID一句话描述
launcher_module主屏性能、应用启动、小部件更新
systemui_module状态栏、通知栏、快速设置、导航栏
third_party_module第三方应用性能、卡顿和资源使用

Skill 之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Module Skills (配置层)
└→ 定义分析范围和关注点

Composite Skills (编排层)
├→ 引用多个 Atomic Skills
├→ Iterator: 逐帧/逐事件遍历深钻
└→ Conditional: 按数据条件分支

Atomic Skills (执行层)
└→ 直接执行 SQL,返回 DataEnvelope

Pipeline Skills (知识层)
└→ 渲染管线教学 + 检测

Deep Skills (剖析层)
└→ Callstack / CPU profiling 深度分析

Agent 的典型调用路径(以滑动分析为例):

1
2
3
4
5
6
invoke_skill("scrolling_analysis")          ← Composite,内部调用多个 Atomic
→ consumer_jank_detection ← Atomic,检测掉帧
→ 逐帧 iterator → jank_frame_detail ← Composite,每帧深钻
→ main_thread_states_in_range ← Atomic
→ binder_blocking_in_range ← Atomic
→ frame_blocking_calls ← Atomic

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android 性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

从 Trace 到洞察:SmartPerfetto AI Agent 的 Harness Engineering 实战

这篇文章记录了 SmartPerfetto 从零到可用过程中的关键技术决策——为什么选这个方案而不是那个,哪些地方踩了坑,踩完之后怎么调整的。

为什么做这个工具

我做了多年 Android 性能优化。日常工作中大量时间花在 Perfetto trace 分析上——Perfetto 是 Google 开源的系统级 trace 工具,采集帧渲染、线程调度、CPU 频率、Binder 通信等数据,几乎是 Android 性能分析的标准工具。它的 trace_processor 引擎把 trace 加载到一个嵌入式 SQLite 数据库中,支持用 SQL 查询。

分析 trace 的过程是高度重复的:找到问题区间、查帧数据、看线程状态、追阻塞链、关联系统指标。每次做的事情类似,但每个 trace 的细节不同。这种「流程固定、细节变化」的工作特点很适合 AI Agent 来处理——把固定流程中的数据收集和初步归因自动化,人来做最后的判断和确认。

SmartPerfetto 就是这个尝试的产物。它在 Perfetto UI 上加了一个 AI 分析面板,用户用自然语言提问(如「分析滑动性能」),背后由 Claude Agent 通过 MCP(Model Context Protocol,Anthropic 提出的工具调用协议)调用 trace_processor 执行 SQL,自主完成多轮数据收集和分析。

写这篇文章的目的,是把构建过程中的工程决策和教训记录下来。从最初的「直接调 API」到现在的最多 20 个 MCP 工具(9 常驻 + 11 条件注入)+ 164 个 YAML Skill + 三层验证体系,中间的每个设计选择都有具体的反例在推动——试过不行才换的方案。这些踩坑记录对做 AI Agent 应用或者做 Android 性能工具的工程师可以直接借鉴。

开篇:同一个 Trace,两条分析路径

一个滑动 trace,120Hz 设备,用户反馈列表滑动偶尔卡顿。打开 Perfetto 看到惯性滑动阶段有 18 帧掉帧,其中 3 帧 Full 级(~60ms,120Hz 设备的单帧预算是 8.33ms)。

掉帧(Jank): Perfetto 的 frame_timeline track 记录每帧渲染耗时。超过 VSync 周期(120Hz 下为 8.33ms)用户会感知卡顿。jank_type 字段区分掉帧类型:App 侧超时、SurfaceFlinger 合成延迟、Buffer Stuffing(BufferQueue 队列背压)等。

路径 A:手动分析

1
2
3
4
5
6
7
8
9
10
1. 打开 Perfetto UI,拖动时间轴找到滑动区间
2. 展开 frame_timeline track,逐帧检查哪些超过 VSync 周期
3. 18 帧掉帧——逐帧点开,展开 thread_state 切片,查看主线程在做什么
4. 帧 1:Sleeping,手动查 waker_utid → system_server(Android 系统核心进程,托管 AMS/WMS 等系统服务)Binder 回来慢
帧 2:Running,但在 Choreographer#doFrame 里卡了 → RecyclerView onBind 过重
帧 3:Sleeping + Running 交替 → dequeueBuffer 等 SurfaceFlinger 合成
... (还有 15 帧需要逐一检查)
5. 关联 CPU 频率 track,确认是否有热降频或 governor 升频延迟
6. 检查是否有 GC pause、Lock contention、Binder 超时
7. 汇总证据,组织结论

thread_state 记录线程的调度状态(Running / Runnable / Sleeping / Uninterruptible Sleep 等)。不同状态指向不同的排查方向——Runnable 通常提示 CPU 调度层面的问题,Sleeping 通常提示等待/阻塞层面的问题。waker_utid 字段记录了唤醒线程的源线程 ID,可以辅助追踪跨进程的阻塞链。

第 3-4 步是主要工作量——18 帧掉帧,每帧都需要展开 thread_state、追踪阻塞原因、关联 CPU 调度。分析过程是逐帧串行的:每帧的下钻路径可能不同(Binder? 锁? GC? IO?),全部看完再汇总。

路径 B:SmartPerfetto Agent

用户输入 "分析滑动性能",以下是 Agent 实际执行的操作(来自 session log session_agent-1774679540422):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
classifyScene("分析滑动性能") → scrolling  (<1ms, 关键词匹配)
buildSystemPrompt() → 4500 tokens (scrolling.strategy.md 注入)

submit_plan({
phases: ["p0: 架构+时间范围", "p1: 滑动概览+掉帧列表",
"p1_9: 根因深钻", "p3: 综合结论"],
successCriteria: "WHY 链 ≥2 级,可操作优化建议"
})

invoke_skill("scrolling_analysis", {process_name: "com.example..."})
18 帧掉帧全部提取,每帧附带:
- 耗时、jank_type、reason_code (App/SF/GPU)
- 主线程状态分布 (Running/Sleeping/IO)
- 阻塞函数 top-3
→ ArtifactStore: art_1 (~440 tokens 引用)

fetch_artifact("art_1", "rows", 0, 18) → 获取全部 18 帧的结构化数据
invoke_skill("frame_blocking_calls", {frame_ids: [3, 7, 12]})
3 帧代表帧的阻塞链: dequeueBuffer / Binder / GC
lookup_knowledge("rendering-pipeline") → 加载渲染管线参考资料

输出结论:
[HIGH] 惯性滑动阶段 18 帧卡顿,3 次 Full 级 ~60ms 掉帧
[MEDIUM] 14 帧 Buffer Stuffing (App 按时完成渲染,但 BufferQueue 满导致呈现延迟)
[MEDIUM] Vulkan Shader 首帧编译 + CPU 冷频 (18.66ms, 超预算 2.2×)

Metrics 快照 (来自 logs/metrics/):16 次工具调用,0 次失败,SQL 平均 652ms。

下图展示了一次完整分析的请求生命周期——从用户输入到最终结论的每一步:

请求生命周期

两条路径的分析步骤相同——查帧数据 → 定位 jank → 追踪阻塞链 → 关联系统状态 → 归纳结论。

差异在于:手动分析逐帧串行,每帧需要手动展开和追踪;Agent 通过 scrolling_analysis Skill 用一条 SQL 批量获取全部 18 帧的结构化数据,再选代表帧深钻阻塞链。

Agent 的分析结果同时落地到 Perfetto UI 上:

  • Auto-Pin:Agent 提到的关键帧和 Slice 自动标记在时间线上
  • 点击跳转:结论中的时间戳和帧 ID 支持点击跳转到 Perfetto 对应位置
  • 数据表格:18 帧的完整性能数据以结构化格式渲染为可排序、可筛选的表格

运行截图:以 SmartPerfetto 前端以 Perfetto 插件的形式存在

SmartPerfetto UI 全貌

运行截图:滑动分析的时候详细分析每一个掉帧的地方, 点击最左边那个剪头可以展开

滑动分析掉帧详情

运行截图:滑动分析结论

运行截图:滑动分析结论,代表帧分析

代表帧分析 1

运行截图:滑动分析结论,代表帧分析

代表帧分析 2

运行截图:每一轮分析都有单独的分析 report,内容与前端显示的一致(更详细一些)

分析报告

分析结论、数据表格和 Perfetto 时间线在同一个界面上。Agent 完成批量数据收集和初步归因后,工程师在 Perfetto UI 上确认关键发现。

需要说明的是,当前 Agent 在复杂边界情况下仍然需要人的判断(后文会具体讨论误诊问题)。这篇文章记录的是构建这个 Agent 背后的工程决策过程。


第一部分:为什么 LLM 不能直接分析 Trace?

在开始讨论架构之前,需要先回答一个根本问题:为什么不能直接把 trace 数据发给 LLM 让它分析?这个问题的答案决定了 SmartPerfetto 整个架构的出发点。

数据规模:trace 文件装不进上下文

一个实际的 Perfetto trace 的数据规模是这样的:

维度典型值
Trace 文件大小50MB - 500MB
事件数量百万 ~ 千万级
序列化为文本后数 GB
Claude 最大 context~200K tokens(约 150KB 文本)

两者差了好几个数量级。即使是一个较小的 50MB trace,里面的 slice(函数调用记录)、counter(CPU 频率采样点)、thread_state(线程调度状态)等数据序列化后也远超 LLM 的上下文容量。

这就意味着 LLM 不可能直接「看到」trace 数据。它必须通过工具按需查询——先用 SQL 找到需要的数据子集(比如某个时间范围内某个线程的状态分布),拿到查询结果后再做分析。这个约束从根本上决定了 SmartPerfetto 必须是一个 工具驱动 的 Agent 架构,而不是把数据喂进 prompt 的简单方案。

精确计算:LLM 不擅长处理数值

性能分析的日常工作围绕精确数值展开:帧耗时的 P50 / P90 / P99 分位数、VSync 周期检测(需要对 VSYNC-sf 间隔取中位数并吸附到标准刷新率)、CPU 利用率的百分比计算、各线程状态的时间占比。

LLM 处理这类数值计算时经常出错。一个实际例子:早期测试中,Claude 把 16.7ms 的帧耗时判断为「正常,未超过 VSync 周期」——它按 60Hz(16.67ms)的帧预算来算了。但这个 trace 采集自一台 120Hz 设备,单帧预算应该是 8.33ms,16.7ms 实际上超预算了一倍。这类错误看起来很小,但在性能分析中会导致完全相反的结论。

数值计算必须由工具完成——SQL 的 AVG()PERCENTILE() 和 YAML Skill 中预定义的统计逻辑,保证每次计算结果一致且精确。

领域知识:LLM 知道但不会用

Android 的渲染管线复杂度是很多开发者没有预期到的。最常见的三种渲染路径是:标准 HWUI 管线(HWUI 是 Android 默认的硬件加速渲染引擎,应用的 View 绘制指令在主线程生成,由 RenderThread 提交给 GPU,最终经 SurfaceFlinger 合成到屏幕)、Flutter 的双线程模型(1.ui → 1.raster,不走 RenderThread)、以及 WebView 的 Chromium 管线(CrRendererMain 线程负责渲染)。除此之外还有 Jetpack Compose、游戏引擎、相机管线等。SmartPerfetto 的架构检测系统目前识别 24+ 种渲染管线,不同管线的 jank 分析需要查看不同的线程和指标——这也是为什么架构检测是分析的第一步。

卡顿的根因可能跨线程(主线程阻塞 → 原因在 RenderThread)、跨进程(App 等待 → system_server 的 WindowManagerService 响应慢)、甚至跨硬件层(CPU 调度到小核 → 算力不足 → 帧超时)。

LLM 的训练数据中包含这些概念——它「知道」什么是 RenderThread,什么是 Binder,什么是 SurfaceFlinger。但面对一个具体的 trace,它缺乏将这些知识按场景分阶段运用的能力。比如分析滑动卡顿时,需要先检查帧级数据(哪些帧掉了、掉帧类型是什么),再针对占比最高的根因类型选择不同的深钻路径(App 侧阻塞走 blocking_chain_analysis,合成端延迟走 SurfaceFlinger 分析)。这种分步骤、有条件分支的分析流程,需要通过策略注入来引导。

可靠性:错误率在实际运行中偏高

即使解决了数据访问问题,直接让 LLM 产出性能分析结论仍然面临可靠性问题。在 SmartPerfetto 的实际运行中,我观察到几类典型的输出问题:

  • 幻觉:生成 trace 中不存在的数据或指标
  • 遗漏:漏掉关键检查项(比如分析启动性能时不检查 JIT 编译和类加载的影响)
  • 浅层归因:停在「主线程忙」的层面,不继续追踪是忙在 futex(锁竞争)、binder_wait(跨进程等待)还是 GC pause
  • 结论不一致:同一份 trace 分析两次,得到不同的严重等级判定

后文第二部分会详细讨论这个问题——agentv3 上线 18 天后的质量审查显示,约 30% 的 Agent 结论包含不同程度的误判。

SmartPerfetto 的分工设计

基于这四个问题,SmartPerfetto 的架构按以下方式分工:

1
2
3
4
5
6
7
8
9
10
11
LLM (Claude) 负责:              工具系统负责:
├─ 理解用户意图 ├─ SQL 精确查询 (trace_processor)
├─ 制定分析计划 ├─ 数值计算与统计 (Skill 内置)
├─ 推理因果关系 ├─ 渲染架构检测 (24+ 管线)
├─ 跨领域关联分析 ├─ 分层数据提取 (L1-L4)
├─ 生成结构化结论 ├─ Perfetto stdlib 查询
└─ 自然语言交互 └─ 数据摘要与压缩 (Artifact Store)

连接层: MCP Protocol — 最多 20 个工具 (9 常驻 + 11 条件)
策略层: 12 套场景策略 (.strategy.md)
质量层: 3 层验证 + SQL 纠错学习

LLM 做推理和表达,工具做查询和计算。连接两者的是 MCP(Model Context Protocol,Anthropic 提出的工具调用协议)——Claude 通过标准 MCP 接口调用 trace_processor 执行 SQL、调用 YAML Skill 做结构化分析、查询 Perfetto stdlib 模块。分析结果通过 SSE(Server-Sent Events)实时推送到 Perfetto UI 前端。

支撑这个分工的工程基础设施包括:场景路由(根据用户问题注入不同的分析策略)、数据压缩(控制返回给 LLM 的数据量)、质量验证(拦截 LLM 的领域误判)。后面几个部分展开讨论每个部分的设计过程。

下图是完整的系统架构,展示了从用户请求到分析结论的 4 个阶段:

系统架构总览


第二部分:从 Workflow 到 Agent

Workflow 和 Agent 的区别

Anthropic 在 2024 年 12 月发表的《Building Effective Agents》(作者 Erik Schluntz、Barry Zhang)中,将 AI 系统分为两类:

  • Workflow(工作流):LLM 和工具通过预定义的代码路径进行编排。每一步做什么、下一步走哪里,都由开发者事先定义好。
  • Agent(智能体):LLM 动态主导自身流程和工具使用,自主决定如何完成任务。

这个区分的实际意义在于灵活性和可控性的权衡。Workflow 提供可预测性,适合步骤固定的任务;Agent 提供灵活性,适合需要根据中间数据调整方向的开放式问题。Andrew Ng 的描述很准确:不需要二元地判断一个系统是不是 Agent,而是把它看作不同程度的 Agent 化。SmartPerfetto 的 agentv2 和 agentv3 分别对应这个光谱的两端。

为什么性能分析需要 Agent 而不是 Pipeline

性能分析不是一个「给输入得输出」的固定流程,它是一个探索性的推理过程。以一个实际的滑动分析为例:

1
2
3
4
5
6
7
1. 先看总览 → 发现 47 帧卡顿,P90 = 23.5ms
2. 根据总览决定方向 → 40% 卡在 APP 阶段,优先看 APP 侧
3. 选代表帧深钻 → Frame #234 的 RenderThread 被 Binder 阻塞 23ms
4. 形成假设 → "可能是 system_server 的 Binder 响应慢"
5. 验证假设 → 查 Binder 对端的 thread_state,发现 system_server CPU 调度延迟
6. 假设如果不成立 → 回退,换方向(比如改查 GPU 或 GC)
7. 综合所有发现,形成结论

每一步决策都依赖前一步的结果——无法在分析开始前就确定所有步骤。Pipeline 无法处理「这个 trace 的问题可能在 GPU,也可能在 GC,需要根据中间数据动态选择下钻方向」这种需求。

SmartPerfetto 的设计是确定性和灵活性的混合:已知场景(滑动、启动、ANR 等)用 Strategy 文件约束必检项,保证不遗漏;但每个阶段内的具体查询和深钻方向由 Claude 自主决定。未匹配的场景则完全交给 Claude 自主探索。

agentv2:一个典型的 Workflow

agentv2 使用 DeepSeek 作为后端,采用 Governance Pipeline 架构——通过 planner / executor / synthesizer 三阶段编排,本质上是预定义的多步骤工作流(历史 commit 6d80aefb: “Replace the 13-step agentv2 governance pipeline with Claude-as-orchestrator”)。

这个架构在标准 Android 应用的滑动分析上工作得不错,但遇到非标准情况就出问题了。比如 Flutter 应用的 trace 里没有标准的 frame_timeline 数据,管线拿到空结果后继续执行后续步骤,最终输出基于空数据的结论。

agentv3:迁移到 Agent 架构

2026 年 3 月 2 日(commit 6d80aefb),我切换到 Claude Agent SDK。Claude 接收工具定义和策略后,自主决定调用什么工具、按什么顺序、查什么数据。

一个 AI Agent 通常具备以下特征,agentv3 的实现对照如下:

特征SmartPerfetto 中的实现代码位置
自主性Agent 自主决定调用哪个工具、按什么顺序claudeRuntime.ts
推理能力每次工具调用后追加 REASONING_NUDGE 触发显式反思claudeMcpServer.ts:84
工具使用最多 20 个 MCP 工具调用 trace_processor9 常驻 + 11 条件
规划能力submit_plan + requirePlan() 门控轻量模式关闭
反思能力3 层 Verifier + Correction Prompt (max 2 轮)claudeVerifier.ts
错误恢复SQL 纠错学习 + 跨会话误判模式学习跨文件
记忆短期: Analysis Notes / Artifact Store;长期: Pattern Memory / SQL Fix Pairs7 层记忆
1
2
agentv2 (Workflow): 固定管线 → 每步预定义 → 意外数据 = 错误结论
agentv3 (Agent): 动态计划 → 自主调用工具 → 意外数据 = 调整计划

迁移后的 9 轮审查

从 3 月 2 日到 3 月 20 日,经历了 9 轮架构审查。其中影响最大的几轮:

轮次日期主要发现
Round 13/2初始 SDK 集成后 12 个修复——SQL 知识库没接入 System Prompt,jank_frame_detail 中 CPU 核数硬编码为 4
Round 33/12架构接线审计——12 处「实现了但没接上」的断连,比如验证管线在 0 findings 时被跳过
Round 73/15Perfetto Stdlib 集成——预加载模块 4→22,Schema Index 708→761
Round 93/2018 天真实 trace 后的生产质量审查——3 P0 + 4 P1 + 5 P2,催生了三层验证系统

冷启动 4 层联动 Bug

2026 年 3 月 19 日(commit d5a1d7b3),发现冷启动被错误分类为热启动。追踪后发现这是一个跨 4 层的联动问题:

1
2
3
4
Layer A (Perfetto Stdlib): bindApplication 的 ts 比 launchingActivity 早 ~98ms → 被过滤器排除
Layer B (Skill 逻辑): startup_events_in_range 的时间过滤与 Layer A 不兼容
Layer C (10 个下游 Skill): 冗余的 startup_type 过滤条件 → 重分类后返回 0
Layer D (质量门禁): startup_analysis 的过滤规则和重分类逻辑不同步

修复规模:重写 10 个下游 Skill,新增 4 个启动分析 Skill。这个问题说明在 Skill 依赖链中,上游的一个字段语义错误会逐层放大。

Ghost MCP Query — 异步生命周期错配

2026 年 4 月 7 日(commit a0ad63ba)抓到的另一类跨层 Bug:分析超时之后,session 已经清理、SSE 流已经收尾,但 trace_processor 的 stderr 仍然在 90 秒后陆续吐出 no such table: cpu_frequency_counters / no such column: ts 这类错误——孤儿日志,没有 owner 可以归因到任何 session,前面错误 4 的两条纠错对就是从这堆 stderr 里反查出来的。

根因在 SDK Query 的异步生命周期:

1
2
3
4
5
6
Layer A (Claude Agent SDK):    SDK 内部的 AsyncIterator 还在生产 message
Layer B (claudeRuntime.ts): timeout 触发,breakfor-await 循环
Layer C (MCP 工具队列): SDK 子进程没收到任何关闭信号,
把队列里剩下的 invoke_skill / execute_sql 继续派发
Layer D (trace_processor): 执行这些"幽灵查询",错误也照常返回——
但此时 session 已经清理,没人在听

AsyncGenerator.return()break 只对消费侧生效,不会反向通知生产侧的外部资源(SDK 子进程 + MCP 工具执行队列)。修法是把 sdkQueryWithRetry 的返回类型从单个 AsyncIterable 改成 { stream, close } 二元组,timeout / 异常 / finally 三个路径都显式调一次 close(),让 SDK 主动 abort 子进程:

1
2
3
4
5
6
7
8
9
// claudeRuntime.ts
const { stream, close } = sdkQueryWithRetry({ ... });
const timer = setTimeout(() => { close(); /* abort SDK 子进程 */ }, AGENT_TIMEOUT);
try {
for await (const message of stream) { ... }
} finally {
clearTimeout(timer);
close(); // 即使正常退出也兜底关一次
}

这个 Bug 之所以隐蔽,是因为表面症状(孤儿 SQL 错误日志)和真实根因(异步资源生命周期错配)相隔很远——错误信息长得像「Agent 写错 SQL」,但其实是「Agent 早就停了,是 SDK 子进程没停」。在 Agent 应用中,”break 一个循环” 经常没有想象中那么干净;任何长生命周期的异步资源都需要显式的 close 通道,而不是依赖 for-await 的自动析构。


第三部分:三个关键的工程决策

决策 1:Scene Classification — 从全量注入到按需加载

一开始我把 12 个场景(scrolling / startup / ANR / interaction / pipeline / game / memory 等)的分析策略全部塞进 System Prompt,总计 15000+ tokens。逻辑是:Claude 应该知道所有场景的分析方法,这样不管用户问什么都能应对。

实际运行后发现 Claude 会混淆不同场景的术语——在分析滑动时引用了启动阶段的指标,把 VSync 间隔(帧间时序)和 bindApplication(进程初始化)搞混。根本原因是不同场景的术语存在大量重叠,「帧」在滑动场景里是渲染帧,在启动场景里是首帧显示,12 套策略同时出现时 LLM 无法区分上下文。

解决方式是做场景分类,每次只注入一套策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
// sceneClassifier.ts — 12 场景, <1ms 执行
export function classifyScene(query: string): SceneType {
const scenes = getRegisteredScenes(); // 从 .strategy.md frontmatter 加载
const sorted = scenes
.filter(s => s.scene !== 'general')
.sort((a, b) => a.priority - b.priority); // ANR(1) → startup(2) → scrolling(3)

for (const scene of sorted) {
if (scene.compound_patterns.some(p => p.test(query))) return scene.scene;
if (scene.keywords.some(k => lower.includes(k))) return scene.scene;
}
return 'general';
}

关键词和优先级声明在每个 .strategy.md 的 YAML frontmatter 中,不硬编码在代码里:

1
2
3
4
5
6
7
8
# scrolling.strategy.md
---
scene: scrolling
priority: 3
keywords: [滑动, 掉帧, jank, scroll, fps, 帧率, 卡顿]
compound_patterns:
- "(?:分析|看看|检查).*(?:滑动|滚动|列表)"
---

添加新场景只需新建一个 .strategy.md 文件。DEV 模式下支持热加载,修改后刷新浏览器即可生效。

调整之后 System Prompt 从 ~15000 tokens 降到 ~4500 tokens,策略混淆的问题没有再出现。新增场景也从改代码变成了新建一个 .md 文件。

当多轮对话积累了较多上下文(分析笔记、历史计划、模式记忆等),System Prompt 可能重新超过 4500 token 预算。这时按优先级逐个丢弃低价值段落:SQL 知识库参考(Claude 可以用 lookup_sql_schema 工具按需查询)→ 历史分析经验 → 历史踩坑记录 → SQL 纠错对 → 子代理协作指引 → 历史分析计划。核心段落(角色、方法论、场景策略、输出格式)不会被丢弃。

决策 2:Artifact Store — 控制返回给 LLM 的数据量

决策 1 解决了 System Prompt 的膨胀问题。但即使场景策略只注入了一套,Agent 在执行过程中每次调用 Skill 仍然会产生大量数据(200+ 行帧数据),这些数据全部放进上下文带来新的问题。

早期版本把 Skill 执行结果(比如 200 行帧数据、487 行阻塞分析)完整返回给 Claude。每个 Skill 结果约 3000 tokens,一次分析调用 5-8 个 Skill,仅 Skill 数据就占 15000-24000 tokens。

token 成本是一方面,更意外的发现是:数据越多,Claude 的输出质量反而越差。面对 200 行帧数据时,它倾向于逐行描述(「帧 1 耗时 12.3ms,帧 2 耗时 15.7ms…」)而不是做模式归纳。我猜测原因是上下文中充斥大量数字后,LLM 的注意力被分散了。

解决方式是把 Skill 结果存入 ArtifactStore,返回给 Claude 的只有紧凑引用(~440 tokens)——行数、列名和摘要信息。需要详情时,Claude 通过 fetch_artifact 按需分页获取。完整数据通过独立的 SSE(Server-Sent Events)通道发送给前端渲染,不经过 LLM。

1
2
3
4
5
6
7
invoke_skill("scrolling_analysis") 执行结果:
├── 前端: 全量 DataEnvelope (200 行) → SSE → UI 表格渲染
│ (DataEnvelope: 自描述的数据合约,包含列名、类型、交互动作,
│ 前端根据 schema 自动渲染表格/图表,不需要针对每个 Skill 写代码)
└── Claude: 紧凑引用 (~440 tokens)
"scrolling_analysis 完成. 概要: 347 帧, jank 率 10.6%
art_1 (详情: fetch_artifact('art_1', 'rows', 0, 20))"

fetch_artifact 的三个粒度:

级别返回内容约 tokens
summary行数 + 列名 + 首行样本~50
rows分页数据 (offset/limit)~200-500
full完整原始数据~3000

调整后每个 Skill 的 token 成本从 ~3000 降到 ~440,8 个 Skill 从 ~24000 降到 ~3520 tokens。Claude 的输出从逐行描述变成了模式归纳,前端仍然能拿到完整数据做表格渲染。

决策 3:三层验证 — 从真实误判中学到的

agentv3 上线 18 天后,我做了一次系统性的质量审查(2026 年 3 月 20 日,commit da63eaf9)。统计结果让我意外:约 30% 的 Agent 结论包含不同程度的误判。

以下是实际遇到的误判案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[案例 1] AgentVSync 对齐偏移标记为 CRITICAL
实际情况: 现代高刷设备(90Hz/120Hz/144Hz)的 VSync 间隔本身就不是完全固定的,
存在正常的微小偏移(±0.5ms 量级)。Agent 把这种正常偏移当成了异常

[案例 2] AgentBuffer Stuffing 帧计入掉帧统计
实际情况: Buffer Stuffing 表示 App 按时完成了渲染,但 BufferQueue 队列满导致
生产侧背压。这不是 App 逻辑问题,不应直接算作 App 侧掉帧。
SmartPerfetto 通过双信号检测处理:默认排除,但如果实际呈现间隔 > 1.5x VSync
则仍计入感知掉帧

[案例 3] Agent 将单帧耗时异常标记为 CRITICAL
实际情况: 孤立的单帧异常不构成模式,需要确认是否重复出现

[案例 4] Agent 将主线程 Sleeping35% (469ms) 标记为 MEDIUM
实际情况: 在启动总时长中,469ms 的主线程睡眠占比已经很高,应标记为 HIGH

这些误判有一个共同特点:它们不是逻辑错误,而是 领域经验的缺失。高刷设备上 VSync 微小偏移是正常的、Buffer Stuffing 的延迟发生在管线队列层面而非 App 逻辑、单帧异常不构成模式——这些判断依赖对 Android 图形栈的深入理解,Claude 的训练数据对这些细节覆盖不足。

认识到这一点后,我建立了三层递进验证:

1
2
3
4
5
6
7
8
Layer 1: 启发式检查 (无 LLM 调用)
— 正则匹配已知误判模式(VSync 偏移标 CRITICAL、Buffer Stuffing 算掉帧、单帧标 CRITICAL)

Layer 2: Plan 遵从检查 (无 LLM 调用)
— 对照 submit_plan 的步骤,检查结论是否覆盖了所有计划阶段

Layer 3: 独立模型审查 (使用 Haiku)
— 用不同模型检查每个发现是否有数据证据支持,因果链是否完整

验证发现严重问题时,生成 Correction Prompt 让 Claude 修正结论(最多 2 轮)。

跨会话学习: 确认的误判模式被持久化到 logs/learned_misdiagnosis_patterns.json,下次分析时自动注入 System Prompt。例如系统学到了:

1
2
3
4
5
6
{
"keywords": ["R008", "TTID", "超出", "LOW"],
"message": "TTID 超出标记为 LOW,但 TTID(1912ms) 超出 dur_ms(1338ms) 43%,
应标记为 MEDIUM 或更高",
"occurrences": 1
}

注:学习到的误判模式不会立即生效。代码中要求 occurrences >= 2 才会进入有效模式集——首次记录只是标记,同一模式第二次出现时才会注入到后续分析的 System Prompt 中,避免孤立事件造成过度矫正。


第四部分:为什么不用标准的 Skill 系统?

从 SOP 到 YAML Skill 的设计选择

做性能分析的团队一般都有自己的 SOP(标准操作流程):滑动卡顿怎么查、启动慢怎么分析、ANR 怎么定位。SOP 通常是一份文档或检查清单,有经验的工程师照着做,新人跟着学。

Anthropic 的 Claude Code 有一套 Skills 系统,本质上是参数化的 Prompt 模板——注入上下文后提交给 Agent 执行。一个自然的想法是把性能分析 SOP 写成这种 Prompt 模板,让 Claude 按 SOP 执行。

我一开始也走了这条路。给 Claude 的 Prompt 是:「查询 frame_timeline 表,找出 jank 帧,分析主线程在 jank 帧期间的状态分布。」

Claude 理解意图没问题,但每次生成的 SQL 不一样。有时候 JOIN 路径写对了(slice → thread_track → thread),有时候直接写 slice.utid——这个列不存在。查出来的结果格式也不固定,有时候 3 列有时候 5 列,前端渲染没法做。

原因很简单:SOP 是给人看的,工程师看到「查 frame_timeline」知道具体该写什么 SQL。LLM 对 Perfetto 的 SQL schema 理解不完整(这些 schema 在训练数据中覆盖有限),每次从 SOP 文本到 SQL 的翻译过程都会引入方差。

SmartPerfetto 的 YAML Skill 采用了不同的思路——不是 Prompt 模板,而是声明式的 SQL 执行单元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# YAML Skill: SQL 预定义,结果格式固定
steps:
- id: thread_state_distribution
type: atomic
sql: |
SELECT state, SUM(dur) as total_dur,
ROUND(SUM(dur) * 100.0 / SUM(SUM(dur)) OVER(), 2) as pct
FROM thread_state ts
JOIN thread_track tt ON ts.track_id = tt.id
WHERE tt.utid = ${main_thread_utid}
AND ts.ts BETWEEN ${frame_start} AND ${frame_end}
GROUP BY state ORDER BY total_dur DESC
display:
level: detail
columns:
- { name: state, type: string }
- { name: total_dur, type: duration }
- { name: pct, type: percentage }

两种方式的核心区别在于「谁来写 SQL」。Prompt 模板让 LLM 每次动态生成 SQL,结果格式不可预测,无法做回归测试;YAML Skill 预定义了 SQL 和输出 schema,参数替换后执行,结果格式固定,可以稳定地回归测试和前端渲染。

维度Prompt 模板 (SOP 式)YAML Skill (声明式执行)
SQL 来源LLM 每次动态生成YAML 预定义,参数替换
结果格式每次可能不同固定的列名和类型
可回归测试不支持6 条 trace 回归测试全通过
前端渲染需要解析自由文本Schema-driven 表格/图表
可组合不支持composite skill 调用 atomic skill
厂商适配需要写不同 Prompt.override.yaml 覆写 SQL

最终的分工是:Claude 负责理解意图、选择 Skill、推理归因;YAML Skill 负责精确的 SQL 查询和结构化输出。Claude 通过 invoke_skill 调用 Skill,Skill 返回结构化数据,Claude 基于数据做判断。

为什么不把每个 Skill 暴露为独立的 MCP Tool?

一个自然的问题是:为什么不直接把 87 个 atomic 分析能力注册为 87 个 MCP Tool,让 Claude 直接调用?

实际试过会发现一个问题:MCP 的 tool list 会随着工具数量线性增长。87 个工具意味着每次 API 调用都要在请求中附带 87 个工具的描述(名称、参数 schema、使用说明),这个固定开销会占据大量 token。更重要的是,当 Claude 面对 87 个工具时,它的选择准确率会下降——工具太多,它不知道该用哪个。

SmartPerfetto 的设计是 Claude 只看到 2 个和 Skill 相关的 MCP Tool:

  • invoke_skill(skillId, params) — 执行指定的 Skill
  • list_skills(category?) — 按场景类别查询可用的 Skill 列表

通过 list_skills(category="scrolling") 按需发现能力,再用 invoke_skill 调用。2 个 MCP Tool 封装了 160+ 个分析能力,工具列表的 token 开销是固定的。

另一个好处是 YAML 格式降低了贡献门槛。性能分析专家如果对某个分析场景有经验,可以直接写 YAML Skill 定义 SQL 查询和输出格式,不需要懂 TypeScript 或修改后端代码。修改后在开发模式下刷新浏览器即可生效(热加载),迭代周期在秒级。

Skill 系统的结构

Skill 数量从项目初期的十几个增长到现在的 164 个,增长的驱动力不是「尽可能多」,而是分析实践中不断遇到新的场景需要覆盖——比如最初只有标准 HWUI 的帧分析,后来遇到 Flutter 应用需要专门的 Skill,再遇到厂商差异需要 override,再遇到启动分析中 JIT、class loading、Binder pool 各自需要独立的检测逻辑。

当前的 Skill 按类型分布如下:

类型数量位置说明
Atomic87skills/atomic/单一检测能力(VSync 周期、CPU 拓扑、GPU 频率、GC 事件等)
Composite29skills/composite/多步组合分析(如 scrolling_analysis 编排多个 atomic Skill)
Pipeline28skills/pipelines/渲染管线检测 + 教学(24+ 种 Android 渲染架构识别)
Module18skills/modules/按模块分类的分析(app / framework / hardware / kernel)
Deep2skills/deep/深度分析(CPU profiling、callstack 分析)

另有 skills/vendors/ 下 8 个厂商的 .override.yaml(Pixel / Samsung / Xiaomi / Honor / OPPO / Vivo / Qualcomm / MTK),覆盖通用 Skill 中的厂商特定 SQL。

分层结果

早期 Skill 的输出是平铺的——一个 Skill 返回一张大表,200 行帧数据混在一起,用户打开就看到全量数据,没有层次感。实际使用中发现工程师的阅读习惯是:先看概要(掉帧率多少、P90 多少),再决定要不要展开看详情,再针对具体帧深钻。

现在 Skill 的输出按层级组织,前端渐进式渲染:

1
2
3
4
5
6
7
8
9
10
11
summary"47 帧卡顿, P90=23.5ms, SEVERE 占 12%"
│ 聚合指标,快速了解全貌

key — 关键数据(最重要的指标和发现)
│ 高亮展示

detail — 完整的数据列表 (frame_id, duration, jank_type)
│ 可展开的数据表格

hidden — 辅助数据(中间计算结果,默认折叠)
按需展开查看

Skill 的每个 step 通过 display.level 声明自己的展示层级(实际使用最多的是 detail — 240 处、key — 170 处、summary — 81 处)。前端根据 DataEnvelope 中的列类型(timestampdurationpercentagebytes 等)和交互动作(navigate_timeline 跳转到 trace 位置、navigate_range 选中时间范围、copy 复制数据)自动渲染表格和图表——新增一个 Skill,前端不需要写额外的代码。这是 164 个 Skill 而前端代码量仍然可控的关键。

Step 类型

最初所有 Skill 都只有一种 step:执行一条 SQL。后来遇到需要组合多个 Skill 的场景(比如 scrolling_analysis 需要先查帧数据,再对每个 jank 帧做阻塞分析),以及需要遍历数据行的场景(逐帧诊断),逐步扩展了 step 类型:

Step 类型说明使用频次
atomic单条 SQL 查询,最基础的 step 类型最常用
skill引用另一个 Skill 的结果,用于组合分析中复用已有能力56 处
iterator遍历数据行,对每行执行子查询5 个 composite Skill 中使用
diagnostic诊断步骤,生成结构化的诊断结论38 处
parallel并行执行多个 step(代码已支持,尚未在 Skill 中使用)0
conditional根据条件选择分支(代码已支持,尚未在 Skill 中使用)0

iterator 是逐帧分析的核心——比如对 18 个 jank 帧中最严重的 8 个,逐一执行 blocking_chain_analysis,每帧独立分析阻塞原因。parallelconditional 在类型系统中已定义,目前还没有 Skill 使用——这是因为当前的分析场景用 skill 引用 + iterator 遍历已经能覆盖,后续引入更复杂的场景(如多路并行数据采集)时会用到。

领域 Skill 举例

以下几个例子说明为什么需要这么多专用 Skill——每个 Skill 背后都有一个「通用方案处理不了」的具体问题。

Consumer Jank Detection — 框架标记 ≠ 用户感知

框架的 jank_type 标记不等于用户感知的掉帧。存在 Hidden Jank——框架标记 jank_type='None' 但用户感知到卡。原因是框架的判定口径和用户的实际感知之间存在差异。

SmartPerfetto 用独立的 consumer_jank_detection Skill 做掉帧判定:通过 VSYNC-sf 间隔的中位数估算实际 VSync 周期,再用 1.5 倍 VSync 周期作为阈值,基于相邻帧的 present_ts 差值(帧实际显示到屏幕的时间戳)判断是否掉帧。不依赖框架标记。

阻塞链分析 — 跨线程、跨进程的根因追踪

一帧掉帧的根因可能涉及多层因果链:

1
2
3
4
5
42 耗时 62ms (预算 8.33ms)
└→ 主线程被阻塞 35ms
└→ 阻塞在 futex_wait (锁竞争)
└→ 锁持有者是 Binder 线程
└→ Binder 线程在等 system_server 响应

blocking_chain_analysis Skill 用 3 步 SQL 提供这条链的关键线索:主线程状态分布(Running / Sleeping / IO 各占多少)→ 唤醒者追踪(通过 waker_utid 找到是谁唤醒了主线程)→ 阻塞函数汇总(futex / binder_wait / io_schedule 各累计多少时间)。这种跨层分析用通用 Prompt 让 Claude 自己写 SQL 很难稳定实现。

Flutter 架构分支 — 不同渲染模式需要不同分析逻辑

Flutter 的两种渲染模式涉及不同的线程,分析时需要看不同的目标:

模式Jank 分析目标线程是否经过宿主 RenderThread
TextureView (双管线)1.ui + 1.raster + RenderThread
SurfaceView (单管线)1.ui + 1.raster

如果用标准 HWUI 的分析逻辑去分析 Flutter SurfaceView 应用,会把 1.raster 线程的耗时错误归因到 RenderThread。SmartPerfetto 通过架构检测(24+ 种渲染管线)自动识别 Flutter 应用并切换到专用的 flutter_scrolling_analysis Skill。

但「自动识别 Flutter」本身也踩了坑(commit 355df8ee,4/6)。早期的 pipeline 检测器是给每种架构单独打分,分数最高的胜出——结果 Flutter TextureView 的 trace 经常被误判为 STANDARD。原因是 Flutter TextureView 的宿主侧仍然走 HWUI 管线(Choreographer#doFrame / DrawFrame / RenderThread),这些信号同时被 STANDARD 和 TEXTUREVIEW 两个分类吸收。STANDARD 的信号覆盖面更广(trace 里几乎一定有 Choreographer 帧),总分常常压过专属的 TEXTUREVIEW,把 Flutter 应用误分到 STANDARD。同样的问题也出在 WeChat Skyline(被 WEBVIEW 吸收)和游戏引擎(被 STANDARD/MIXED 吸收)上。

修法不是调权重,而是给特化 pipeline 加 exclude_if:TEXTUREVIEW 一旦看到 Flutter 1.ui / 1.raster 信号就直接屏蔽 STANDARD 分类;STANDARD_LEGACY/MIXED/SURFACEVIEW_BLAST 看到 Game Engine 信号就互斥;OPENGL_ES 看到 WebView/Game 信号就互斥。24+ 种 pipeline 不能各打各的分,需要一个「特化 → 通用」的优先级链。 这是「pipeline 多了之后必须做相互排斥」的典型例子——也是为什么 Skill 数量增长到 160+ 之后,光「正确路由到哪个 Skill」本身就成了独立的工程问题。

厂商覆写 — 同一指标在不同平台上的字段名不同

高通、联发科、Google Tensor 的 trace 中,相同指标的字段名不同(比如 GPU 频率在高通叫 gpufreq,联发科可能叫 gpu_freq_khz)。.override.yaml 让同一个 Skill 在不同平台上自动适配 SQL,不需要为每个厂商写独立 Skill。


第五部分:SQL 工程

前面讨论的 Skill 系统最终都落到 SQL 查询上——每个 Skill 的 step 执行的是预定义的 SQL。SQL 是 SmartPerfetto 的核心——所有性能数据的获取最终都通过 SQL 查询 trace_processor 完成。这部分展开讨论 SQL 层面的几个工程问题:查询模式设计、官方 stdlib 复用、Schema 索引、结果压缩和纠错学习。

SQL 查询模式:时间区间 JOIN 和递归分桶

Perfetto trace 的数据本质上是带时间戳和持续时长的事件流。性能分析中最常见的操作是判断两个事件在时间上是否重叠——比如某帧渲染期间,主线程有没有被 Binder 调用阻塞。

YAML Skill 中大量使用的核心 SQL 模式是时间区间 JOIN——判断两个事件是否在时间上重叠。下面这条 SQL 的业务含义是:对于每个掉帧,找出在这帧渲染期间同时发生的阻塞调用(如 GC、Binder、锁),并计算它们重叠了多少毫秒:

1
2
3
4
5
6
7
8
9
10
11
-- 业务含义:掉帧帧和阻塞调用的时间重叠分析
SELECT
jf.frame_id,
b.name as blocking_call,
-- 计算精确的重叠时长(纳秒级)
ROUND((MIN(b.ts + b.dur, jf.ts + jf.dur) - MAX(b.ts, jf.ts)) / 1e6, 2) as overlap_ms
FROM jank_frames jf
JOIN blocking_calls b
ON b.ts < jf.ts + jf.dur -- 阻塞调用的开始 < 帧的结束
AND b.ts + b.dur > jf.ts -- 阻塞调用的结束 > 帧的开始
HAVING overlap_ms > 0.5 -- 过滤掉不足 0.5ms 的微小重叠

这里的 MIN(end1, end2) - MAX(start1, start2) 是计算两个区间重叠长度的标准公式。在 Perfetto trace 中,时间戳精度到纳秒,这种区间 JOIN 能精确到 0.001ms 的粒度。

另一个常用模式是递归 CTE 做时间分桶。比如分析启动过程中 CPU 大核/小核的使用分布变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 递归生成时间桶(最多 30 个,防止递归失控)
WITH RECURSIVE buckets AS (
SELECT 0 as idx, ${start_ts} as bucket_start,
MIN(${start_ts} + bucket_ns, ${end_ts}) as bucket_end
UNION ALL
SELECT idx + 1, bucket_end, MIN(bucket_end + bucket_ns, ${end_ts})
FROM buckets WHERE bucket_end < ${end_ts} AND idx < 29
)
-- 每个时间桶内,统计大核 vs 小核的调度时间
SELECT
ROUND(SUM(CASE WHEN core_type IN ('prime','big','medium')
THEN overlap_dur ELSE 0 END) / 1e6, 2) as big_core_ms,
ROUND(SUM(CASE WHEN core_type = 'little'
THEN overlap_dur ELSE 0 END) / 1e6, 2) as little_core_ms
FROM buckets b
LEFT JOIN main_thread_sched ms ON ms.ts < b.bucket_end AND ms.ts + ms.dur > b.bucket_start
GROUP BY b.idx

_cpu_topology 是 Perfetto stdlib 提供的视图,把 CPU 核心分类为 prime / big / medium / little。递归 CTE 限制最多 30 个桶,防止在极长 trace 上递归失控。

这些 SQL 模式被封装在 YAML Skill 中,通过 ${param|default} 语法接受参数。Claude 不需要自己写这些复杂的时间区间 JOIN——它调用 invoke_skill 传入时间范围和进程名,Skill 负责执行预定义的 SQL 并返回结构化结果。

Perfetto Stdlib 复用

Perfetto 官方维护了一套 SQL 标准库(stdlib),提供了大量预定义的视图和函数。比如 android_frames 视图封装了帧渲染数据的多表关联逻辑,_android_critical_blocking_calls 内部表汇总了关键阻塞调用。直接使用这些官方抽象,比手写 SQL 从底层表关联要稳定得多。

SmartPerfetto 对 stdlib 的集成经历了几轮迭代——其中一次回退还把「优化的方向」整个翻了过来:

  • 初始阶段: 只预加载了 4 个 stdlib 模块(android.frames.timeline、android.binder、android.startup.startups、android.input),大部分 Skill 的 SQL 直接查底层表。优点是启动快,缺点是 Skill 里到处自己手写多表 JOIN

  • Round 7 (3/15): 把预加载集扩展到 22 个模块,包括 linux.cpu.utilizationandroid.garbage_collectionandroid.oom_adjusterslices.with_context,覆盖 CPU/GC/OOM/slice 等常用维度。当时的逻辑是:trace 加载时一次性把所有常用 stdlib 模块批量 INCLUDE,后续 Skill 查询零开销

  • 回退到 lazy 加载 (4/1, commit 0afeb60f): 22 模块的 eager preload 在生产中翻车了——200MB+ 的大 trace 上,启动时并发 INCLUDE 22 个模块会同时占用 trace_processor_shell 的 RPC 连接,触发 socket hang up根因是 trace_processor_shell 是单线程的 SQLite 引擎,最不擅长并发 INCLUDE 这种「批量 schema mutation」负载。 最终的修法是把 eager preload 收回,只保留 3 个 Tier-0 模块,且改成首次 query 时 lazy + 串行加载 + 最多 3 次重试:

    1
    2
    3
    4
    5
    6
    7
    // workingTraceProcessor.ts
    // Tier 0: absolute minimum stdlib modules needed for any analysis to start.
    const CRITICAL_STDLIB_MODULES = [
    'android.frames.timeline', // 19 个 skill 引用 — frame/jank 基础
    'android.startup.startups', // 16 个 skill 引用 — startup 基础
    'android.binder', // 22 个 skill 引用 — IPC/blocking 基础
    ];

    这 3 个是按「skill 引用次数」筛出来的最高频依赖。其余 stdlib 模块改由 Skill YAML 的 prerequisites 段或 SQL 里显式的 INCLUDE PERFETTO MODULE 在第一次用到时按需声明

  • 按需发现: perfettoStdlibScanner.ts 扫描 Perfetto 源码目录自动发现所有可用模块,通过 list_stdlib_modules MCP 工具让 Claude 按需 INCLUDE 非预加载的模块

1
2
3
4
5
// perfettoStdlibScanner.ts — 扫描 perfetto/src/trace_processor/perfetto_sql/stdlib/
function scanDirectory(dir: string, prefix: string): string[] {
// 递归扫描 .sql 文件,转换为模块路径格式 (如 "android.frames")
// 排除 prelude 目录(这些是自动加载的,不需要手动 INCLUDE)
}

这次回退的教训和文章前面的「数据越多 Claude 输出反而越差」是同一类的——「在系统启动时把所有可能用到的资源都准备好」是直觉上最优、实际上最差的策略。无论是给 LLM 的上下文还是给 trace_processor 的 stdlib,先 lazy + 按需加载,等真正出现性能瓶颈再考虑预热,几乎总是更稳的选择。

另一个独立的教训是:使用 stdlib 的 android_garbage_collection_events 视图比自己 JOIN slice + thread + process 表查 GC 事件要稳定得多——因为 GC 事件的 slice name 在不同 Android 版本中有变化(concurrent mark sweep vs young concurrent copying vs HeapTaskDaemon),stdlib 已经处理了这些兼容性问题。但 stdlib 视图自己也有坑(列名前缀、模块未自动加载),后面 SQL 纠错那一节会展开讲。

SQL Schema Index:让 Claude 知道有什么表可以查

Perfetto trace_processor 包含数百个表和视图,加上 stdlib 的模块,Claude 不可能全部记住。lookup_sql_schema MCP 工具提供了一个搜索接口,让 Claude 按关键词查找相关的表、视图和函数定义。

底层是一个从 Perfetto 源码自动生成的索引文件(perfettoSqlIndex.light.json),包含 761 个模板,每个模板记录了名称、类别、类型(table/view/function)、列定义和参数。

查询时使用分词匹配 + 评分排序:

  • 名称/类别/描述中包含完整搜索词 → 高分
  • 多词查询按 token 分别匹配 → 匹配 ≥50% 的 token 才算相关
  • 表名的下划线分段支持前缀匹配(”frame_time” 匹配 “frame_timeline_slice”)
  • 返回 top 30 结果

配合 sqlKnowledgeBase.ts 的意图映射,还支持双语查询:用户输入「卡顿」会映射到 ['jank', 'frame', 'dropped'] 等搜索词,输入「启动」会映射到 ['android_startups', 'launch', 'time_to_display']。多个意图同时命中时,分数叠加——比如查询「启动帧卡顿」同时触发 startup 和 jank 两个意图,匹配到两者交集的模板分数最高。

SQL 结果压缩

当 Claude 使用 execute_sql 直接查询时,可以传入 summary=true 参数触发结果压缩。压缩逻辑在 sqlSummarizer.ts 中实现:

数值列: 计算 min、max、avg 和分位数(P50 / P90 / P95 / P99),让 Claude 了解数据分布,不需要看原始行。

字符串列: 统计 top 5 值及其出现次数,了解数据的类别分布。

样本行选择: 从完整结果中选 10 行有代表性的样本。选择策略是:如果数据中有 durlatencyjankcount 等和性能相关的列,按该列降序排列取 top 10(最严重的数据通常最有分析价值);如果没有明确的性能指标列,等间距采样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 200 行原始结果 (~3000 tokens) 压缩为:
{
"totalRows": 200,
"columnStats": [
{ "column": "dur_ms", "type": "numeric",
"min": 2.1, "max": 67.3, "avg": 12.8,
"p50": 9.2, "p90": 23.5, "p95": 35.1, "p99": 62.0 },
{ "column": "jank_type", "type": "string",
"topValues": [
{ "value": "App Deadline Missed", "count": 87 },
{ "value": "Buffer Stuffing", "count": 45 },
{ "value": "None", "count": 68 }
] }
],
"sampleRows": [ /* 10 行最严重的帧数据 */ ]
}
// ~500 tokens,压缩率 ~85%

这和前面提到的 Artifact Store 配合使用——Artifact Store 压缩的是 Skill 结果(invoke_skill 返回的数据),SQL Summarizer 压缩的是 Claude 直接执行 SQL 时的结果。两层压缩覆盖了 Agent 获取数据的两条路径。

SQL 纠错学习

Claude 对 Perfetto 的 SQL schema 不完全熟悉,会写出有错误的查询。以下是实际记录的典型错误(来自 logs/sql_learning/error_fix_pairs.json):

错误 1:JOIN 了不存在的列

Perfetto 的 slice 表没有直接的 utid 列。要关联 slice 和 thread,需要经过 thread_track 中间表:slice.track_id → thread_track.id → thread_track.utid → thread.utid

1
2
3
4
5
6
7
8
-- 错误: no such column: s.utid
SELECT s.ts, s.name FROM slice s
JOIN thread t ON s.utid = t.utid

-- 修正: 通过 thread_track 中间表
SELECT s.ts, s.name FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t ON tt.utid = t.utid

错误 2:列名歧义

1
2
3
4
5
-- 错误: ambiguous column name: name (slice 和 process 都有 name 列)
SELECT name, ts FROM slice s JOIN process p ON ...

-- 修正: 加表名前缀
SELECT s.name, s.ts FROM slice s JOIN process p ON ...

错误 3:对 counter 表的数据模型理解有误

Perfetto 的 counter 表存储的是采样点(时间戳 + 值),不是区间数据,没有 dur 列。

1
2
3
4
5
-- 错误: no such column: c.dur
SELECT SUM(c.value * c.dur) FROM counter c

-- 修正: 使用简单平均值或 LEAD 窗口函数
SELECT AVG(c.value) FROM counter c WHERE ...

错误 4:stdlib 表名 / 列名陷阱

这两条都是 commit 05922e67 加进去的——发现源是「无主孤儿 stderr」:分析早就结束了,trace_processor 还在喷错误日志,反查回去才定位到 Agent 在 dynamic SQL 里反复踩同样的坑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 错误 4a: no such table: cpu_frequency_counters
-- 这是 stdlib 表,不在 lazy 预加载的 3 个 Tier-0 模块里
SELECT ts, cpu, freq FROM cpu_frequency_counters WHERE ...

-- 修正: 先 INCLUDE 模块再查
INCLUDE PERFETTO MODULE linux.cpu.frequency;
SELECT ts, cpu, freq FROM cpu_frequency_counters WHERE ...

-- 错误 4b: no such column: ts
-- android_garbage_collection_events 用 gc_ 前缀的列名,不是常规的 ts/dur
SELECT ts/1e6, dur/1e6 FROM android_garbage_collection_events

-- 修正: 用 gc_ts / gc_dur / gc_running_dur / gc_runnable_dur
SELECT gc_ts/1e6, gc_dur/1e6, gc_running_dur/1e6
FROM android_garbage_collection_events

stdlib 视图作者经常会用领域前缀的列名来避开多表 JOIN 时的 ambiguous column 问题,但 Claude 默认假设的是 ts/dur 这种通用约定。这种「stdlib 自己的命名习惯 vs 通用 SQL 习惯」的冲突没法靠 schema introspection 完全自动解决——lookup_sql_schema 工具能告诉 Claude 表存在和有哪些列,但不能预测「这次 Claude 一定会想当然地写 ts」。只能靠纠错对累积来兜底。

这些错误的检测和学习机制是这样的:当 SQL 执行失败时,错误信息和 SQL 被暂存;当后续有 SQL 执行成功时,系统通过 Jaccard 相似度匹配(排除 SQL 结构关键词如 SELECT/FROM/WHERE,以及 Perfetto 通用 token 如 utid/dur/slice)判断是否是同一查询的修正版本。匹配阈值 >30%,时间窗口 60 秒。匹配成功则生成 error→fix 对并持久化到磁盘。

新分析开始时,最近 10 条纠错对加载到 System Prompt,Claude 在写 SQL 之前就能看到这些已知的坑。纠错对设置 30 天 TTL,过期自动清理——Perfetto 的 SQL schema 会随版本更新变化。


第六部分:开发过程本身的 Harness 演进

最后一个部分稍微跳出产品本身,聊一下开发过程。SmartPerfetto 是用 AI 辅助开发的——从第一行代码到现在,Claude Code 是主要的编程工具。回顾这三个月,我使用 AI 辅助开发的方式本身也经历了几次迭代,和 SmartPerfetto 从 agentv2 到 agentv3 的演进有相似的逻辑。

AI 辅助开发的几个阶段

先简要说明涉及的工具和概念:

  • Claude Code:Anthropic 的 CLI 工具,可以在终端中与 Claude 对话,Claude 能直接读写文件、执行命令。我在开发中一直开启 --dangerously-skip-permissions(危险模式)和 bypass permissions,让 Claude 无需逐次确认即可自主执行文件编辑、命令运行、Git 操作等。这大幅提升了迭代速度——Claude 可以连续执行「改代码 → 跑测试 → 看结果 → 修复 → 再跑」的完整循环而不被权限弹窗打断,代价是需要开发者对 Claude 的操作有足够信任和事后审查
  • Claude Agent SDK:Anthropic 提供的 Agent 开发框架,SmartPerfetto 的 agentv3 后端基于它构建。SDK 封装了多轮对话管理、MCP 工具调用循环、上下文自动压缩(auto-compact)等能力,开发者定义工具集和 System Prompt,SDK 驱动 Claude 自主完成多轮分析
  • Plan Mode:Claude Code 的规划模式,AI 先输出结构化实施方案(要改哪些文件、改什么、依赖关系),人审查确认后再执行代码修改
  • SuperPower:Claude Code 的第三方插件生态,通过 MCP Server 为 Claude Code 注入额外能力。SmartPerfetto 开发中使用了 Chrome DevTools Protocol 插件(直接操控浏览器截图、调试前端)、Playwright 插件(自动化 UI 测试和截图)等。这些插件让 Claude Code 的能力从代码编辑扩展到了浏览器交互和可视化验证
  • Codex + Codex MCP:Codex 是 OpenAI 的代码推理模型。通过 Codex MCP Server 集成到 Claude Code 中后,Claude 可以在对话过程中直接调用 Codex 做独立审查——把实施方案发给 Codex,Codex 以只读方式访问代码库,从架构合理性、边界情况、遗漏风险三个角度给出反馈,整个过程不需要离开 Claude Code 的工作流
  • Agent Team:Claude Code 支持启动多个子 Agent 并行工作,每个 Agent 可以有独立的工具集和角色定义
  • Skills / Hooks:Claude Code 的扩展机制,Skills 是可复用的任务模板(如 /commit/simplify),Hooks 是在特定事件(如工具调用前后)自动执行的脚本

我的实际演进过程

阶段 1:直接对话

最早期的开发方式是在 Claude Code 中直接描述需求,让 AI 修改代码。类似于结对编程中一个人说、一个人写。这个阶段人需要逐行审查每次修改,因为 AI 对项目上下文的理解有限,经常做出不符合整体架构的局部修改。

阶段 2:Plan Mode(SuperPower)

开始使用 Plan Mode 后,工作流变成:我描述需求 → AI 输出结构化的实施方案(要改哪些文件、每个文件改什么、改动顺序和依赖关系)→ 我审查方案 → 确认后 AI 执行。这把 review 的重心从「逐行看代码」转移到了「审查架构方案」,效率明显提升。

阶段 3:Plan Mode(SuperPower) + 引入同行 Review(Codex)

单靠一个 AI 生成方案,容易出现盲区。我开始在 Plan Mode 的方案确定后,把方案发给 Codex 做独立审查。Codex 以只读方式访问代码库,从架构合理性、边界情况、遗漏风险三个角度给反馈。这相当于在 AI 开发流程中引入了 code review 环节。

文章前面提到的 9 轮架构审查,大部分都经过了这个流程。以 Perfetto Stdlib 集成为例(Round 7,3 月 15 日),Codex 审查了 3 轮,累计提出 36 条反馈,其中涉及 stdlib 模块预加载策略、Schema Index 的缓存失效机制等我在方案中遗漏的问题。

阶段 4:Harness 化的工程流水线

到后期,开发流程变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. 我确定需求和架构方向
2. Claude Code 在 Plan Mode 下输出实施方案
3. Codex 以只读模式审查方案,提出反馈
4. 我评估 Codex 的反馈(不盲从,约 20% 的建议会被驳回并说明理由)
5. Claude Code 按修正后的方案执行代码修改
6. 自动运行完整测试:
- npx tsc --noEmit (类型检查)
- npm run test:scene-trace-regression (6 条 trace 回归,验证 Skill 数据产出)
- npm run validate:skills + validate:strategies (Skill/策略合约校验)
- 对于启动/滑动/Flutter 相关的改动,还会跑真实 Trace 的 E2E Agent 分析:
用 verifyAgentSseScrolling.ts 脚本加载真实 trace 文件,
发起完整的 Agent 分析会话,检查 SSE 事件流、工具调用序列、
最终结论是否覆盖策略中定义的必检项。
比如滑动场景会检查 Agent 是否执行了 Phase 1.9 根因深钻,
Flutter 场景会检查 Agent 是否正确识别了 TextureView/SurfaceView 架构
并调用了 flutter_scrolling_analysis 而不是标准的 scrolling_analysis。
这一步验证的不是 Skill 能不能跑通,而是 Agent 在面对真实数据时
的推理路径和结论质量是否符合预期。
<!-- TODO: 贴真实截图 -->
<!-- 截图 6: E2E 测试输出 — verifyAgentSseScrolling.ts 的终端输出,展示 SSE 事件统计和通过/失败状态 -->
7. 测试不通过 → 分析失败原因(读 session log + metrics)→ 修复 → 重新测试
8. 测试通过 → /simplify (代码整理)
9. 最终 Codex review 确认

这个流程中,人的介入集中在第 1 步(需求和架构决策)和第 4 步(评估 review 反馈)。代码细节、测试执行、格式整理由工程流水线完成。

和 SmartPerfetto 架构的对应关系

回过头看,我的 AI 辅助开发流程和 SmartPerfetto 的 Agent 分析流程在结构上是相似的:

维度SmartPerfetto Agent 分析我的 AI 辅助开发
意图理解Scene Classifier 识别场景我确定需求方向
策略注入.strategy.md 注入分析方法论Plan Mode 输出实施方案
执行MCP 工具调用 SQL/SkillClaude Code 执行代码修改
质量验证三层 Verifier (启发式+Plan+Haiku)回归测试 + Codex review
纠正循环Correction Prompt 让 Claude 修正测试失败 → 分析 → 修复 → 重跑
跨会话学习Pattern Memory + SQL 纠错CLAUDE.md 规则积累 + memory 系统

两个系统的演进方向也一致:人的介入从执行层逐步上移到决策层。 SmartPerfetto 从固定管线(人定义每一步)到自主推理(人定义目标和约束);我的开发方式从逐行 review 到审查架构方案。

这不是偶然——Harness Engineering 的核心就是构建足够的工程基础设施(测试、验证、review),使得人可以信任 AI 的执行结果,把注意力放在更高层的决策上。

下图汇总了 SmartPerfetto 的 Harness Engineering 全景——从输入路由到跨会话学习:

Harness Engineering 全景

结语

回顾这三个月的迭代,从 agentv2 的 13 步固定管线到 agentv3 的自主推理,从约 30% 误判率到三层验证,从 15000 tokens 的 System Prompt 到 4500 tokens 的按需加载——每一步变化都有具体的失败经历在推动。

做完这个项目之后,我对 AI Agent 应用开发有两个体会。

第一个是:主要工作量不在 LLM API 调用本身,而在围绕 LLM 的工程基础设施:

  • System Prompt 怎么组织,才能让 LLM 不混淆上下文?→ 场景分类 + 按需加载 + Token 预算
  • 怎么控制 LLM 的执行顺序,让它先想再做?→ Planning Gate + Hypothesis 提交
  • 返回多少数据给 LLM 合适?→ Artifact Store,给摘要而不是全量
  • 怎么发现和拦截 LLM 的领域误判?→ 三层验证 + Correction 循环
  • 怎么保证数据查询的精度?→ YAML Skill (声明式 SQL) + SQL 纠错学习
  • 怎么适配不同渲染架构和芯片平台?→ 架构检测 + 厂商覆写

第二个体会是:Agent 的「环境」比 prompt 的措辞重要得多。 agentv3 初期我花了不少时间调整 System Prompt 的用词和格式,后来发现真正影响 Agent 输出质量的不是 prompt 怎么写,而是给它什么工具、返回什么数据、施加什么约束。三个具体的例子:

  • 加了 submit_plan 门控后,Claude 不再没有方向地查 SQL(之前会出现连续 SELECT * FROM sliceSELECT * FROM thread 的无目的查询),分析路径变得有组织
  • 加了 ArtifactStore 后,Claude 接收到的数据从 200 行降到摘要引用,推理的聚焦度明显提升
  • 加了 lookup_knowledge 工具后,根因分析的深度从「主线程阻塞」推进到「Binder 对端 system_server 因 CPU 被调度到小核导致响应延迟」

这些改进都不是通过调整 prompt 文字实现的,而是通过改变 Agent 的工具集和数据环境实现的。如果我要给做 AI Agent 应用的工程师一个建议,就是把精力放在工具设计和数据控制上,而不是 prompt engineering 上。

后续方向

当前的 SmartPerfetto 是一个交互式分析工具,还远远没有达到可以发布的程度,所以目前还是闭源的,由我个人在负责开发。后续的工程方向包括:

  • 厂商深度接入 — 当前 8 个厂商的 .override.yaml 只覆盖了核心 Skill。更多厂商专属指标(高通 Snapdragon Profiler 数据、联发科 MAGT 信号、三星 GameOptimizing 服务)需要逐一对接
  • CI 集成 + 批处理 — 从交互式分析到 CI Pipeline 中自动分析每次构建的性能回归。包括无人值守模式、结果对比基线、自动标记 regression
  • E2E 验证框架 — 当前的 6 条 trace 回归测试验证 Skill 产出数据的正确性,但不验证 Agent 的结论质量。需要建立 E2E 验证:给定 trace + 已知根因 → 检查 Agent 是否正确定位
  • 代码库接入 — 将 trace 中的 slice/function 映射回源码位置,结合 git blame 定位变更引入点

在合适的时候本工具会开源处理(因为各个大厂内部都在做了,所以开源出来大家集思广益,共同开发),对进度感兴的同学可以加我微信进群聊或者私聊。


关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android 性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

OpenClaw 常见问题解答:Token 消耗、能干什么、本地模型、隐私安全、使用体验

昨天那篇 OpenClaw 实战复盘发出去之后,后台留言和群里讨论最多的就是这几个方向的问题:Token 消耗、能干什么、本地模型、隐私安全、使用体验。今天挑出被问频率最高的五个,一个一个说清楚。

OpenClaw 常见问题解答

1. Token 消耗到底多少?贵不贵?

先说结论:没你想的那么贵,但也不是零成本。我前后用了两套方案,一套云端大厂套餐,一套国产性价比路线,感受差异还是挺明显的。

Token 消耗与成本

前期:OpenAI Pro 套餐(200 刀/月)

一开始接的是 OpenAI Pro,量大管饱,用着确实省心。不过这个套餐不是只给 OpenClaw 用的——我日常写代码也走这个额度,实际消耗大概一半一半。跑了段时间下来,5 小时速率上限从来没碰过,Weekly 上限也没撞到,说明我这种”重度使用但不是 7×24 不停歇”的用法,200 刀的额度是够用的。

后期:智谱 GLM5 Coding Plan(年费 ¥1608)

Pro 到期后,我切到了智谱 GLM5 Coding Plan,走的是国产模型路线。我买的时候赶上了首购优惠价 ¥980/年,但 **2026 年 2 月智谱调整了套餐价格,取消了首购优惠,整体涨幅约 30%**。

2026 年 3 月最新套餐价格

套餐季费年费(季费×4)月均适用场景
GLM Coding Lite¥132¥528¥44轻度用户
GLM Coding Pro¥402¥1608¥134重度用户(推荐)
GLM Coding Max¥1266¥5064¥422团队/企业

我用的相当于 Pro 档位,涨价前 ¥980/年,现在 ¥1608/年。实际跑下来体验还行,定时任务、巡检、日报周报、知识库整理这些高频场景都能稳稳撑住,没出现过限额不够用的情况。

GLM-5 API 按量计费(如果不用套餐)

如果你不想买套餐,也可以按量付费。GLM-5 的 API 价格:

模型输入价格输出价格
GLM-5(0-32K)¥4 / 百万 Tokens¥18 / 百万 Tokens
GLM-5(32K+)¥6 / 百万 Tokens¥22 / 百万 Tokens
GLM-5-Code(0-32K)¥6 / 百万 Tokens¥28 / 百万 Tokens

算笔账:我 7 天烧了 2.4 亿 Token,如果按量付费光这 7 天就要花大约 ¥2400,而套餐年费才 ¥1608——也就是说一整年的套餐价还不到 7 天按量费用的零头。所以我的建议很直接:重度用户直接上套餐,轻度用户可以先按量试试水深

国内 Coding Plan 对比(2026 年 3 月)

这三家是对 OpenClaw 支持最好的国内模型:

套餐智谱 GLMMiniMaxKimi
入门档Lite ¥44/月(¥528/年)Starter ¥70/月(¥700/年)Moderato ¥133/月
进阶档Pro ¥134/月(¥1608/年)Plus ¥140/月(¥1400/年)Allegretto ¥99/月
高级档Max ¥422/月(¥5064/年)Max ¥350/月(¥3500/年)Vivace ¥199/月
主力模型GLM-5MiniMax M2.5Kimi K2.5
特点国产最强,OpenClaw 默认支持性价比高,API 稳定长上下文强,2026 年转 token 计费
OpenClaw 支持⭐⭐⭐ 原生支持⭐⭐⭐ 原生支持⭐⭐ 需配置

我的选择是 GLM Coding Pro(¥1608/年),原因是 OpenClaw 对智谱的支持最成熟,API 稳定,中文能力强。性价比之选是 MiniMax Plus(¥1400/年),比 GLM Pro 便宜 200 块,能力接近。

⚠️ 以上价格为 2026 年 3 月查询结果,各家会不定期调整,购买前请以官网为准。

真实数据:7 天烧了 2.4 亿 Token

我查了下智谱后台:过去 7 天,GLM5 消耗了约 2.4 亿 Token。数字看起来挺吓人的,但要注意几点:这 2.4 亿是纯云端 GLM5 的消耗,本地模型处理的那部分并不算在里面;Coding Plan 年费 1608 元,2.4 亿 Token 分摊到 7 天的话平均每天大约 3400 万 Token;如果按量付费 GLM5 大约 0.01 元/千 Token,光这 7 天就要花 2400 元——而 Coding Plan 全年才 1608 元,套餐的性价比不言而喻。所以真实的费用结构是这样:云端干高质量任务,本地干高频脏活,搭配下来成本远没有想象中夸张。 完全不想花钱的话也可以只用本地模型起步,但能力上限和响应质量确实会差一截。如果愿意每月花一杯咖啡的钱接个云端模型,整体体验会好很多,尤其是长文写作和复杂推理的场景。

Token 消耗统计

心态:别太功利

说到费用,我想多聊两句心态。养了这么久 OpenClaw(我家小朋友叫它”虾哥”),我最大的感触是:别一开始就想着变现、ROI 这些功利性的东西。AI 时代才刚刚开始,前期投资是必须的。你现在投入的时间和金钱可能暂时看不到直接收益,但会慢慢转化成你对 AI 能力边界的理解、对 Agent 协作的体感、对”什么场景能用 AI 解决”的直觉判断力。这些东西短期内没法量化,但长期来看价值巨大。

我的态度一直是:养虾就当给自己买了个大玩具,千金难买我开心。每天看它自己跑任务、写文章、整理知识库,这件事本身就已经很有成就感了。以后能变现当然好,但不能用”现在能不能变现”来决定要不要开始。先养起来,比什么都重要。

2. OpenClaw 到底能干什么?给几个实际的例子

上一篇实战复盘写得比较详细,但我知道很多人没耐心从头看到尾。这里直接捞几个我觉得最有价值、最能说明问题的场景出来。

OpenClaw 能干什么

我正在用的

场景说明价值
每日论文精读自动抓 Android/AI 论文 → 翻译 → 精读笔记,落盘到 Obsidian跟踪前沿,零人工
GitHub 仓库监控巡检 Issue/PR 变化,重要更新主动通知我监控的是 Perfetto,不用每天刷
知识库建设公众号/X/博客自动归档、结构化1760 篇 Markdown 持续增长
内容回顾每天推送值得重看的内容解决”收藏了再也没看过”
Android 动态汇总自动汇总领域动态,提取关键话题不用刷信息流

核心价值在于,OpenClaw 最值钱的不是”帮你回答一个问题”,而是把那些你明知很重要、但凭意志力很难长期坚持的事情变成系统默认帮你执行的后台任务。


进阶案例 1:批量导入 NotebookLM 进行深度分析

NotebookLM 是 Google 推出的 AI 研究助手,可以把你的各种资料(PDF、网页、文档)变成一个可对话的知识库。它最大的亮点是能自动生成播客式音频概述——两个 AI 主持人用轻松聊天的形式帮你总结资料要点,通勤路上戴着耳机听非常香。但 NotebookLM 有个明显的痛点:手动添加资料很麻烦,需要一个一个上传文件、一条一条粘贴链接,资料一多就让人抓狂。OpenClaw 刚好可以帮你把这个流程自动化掉。

实际操作

1
2
3
4
5
我:帮我研究一下 Android 15 的性能优化特性。
1. 搜索相关资料(官方文档、博客、论文)
2. 整理成 10-15 个高质量来源
3. 批量导入到 NotebookLM
4. 生成播客概述

OpenClaw 会自动搜集 Android 15 性能优化相关的官方文档、技术博客和论文,筛选掉水文和营销号只保留真正有价值的内容,然后通过 API 或浏览器自动化把资料一键导入 NotebookLM,最后触发播客生成。

为什么这个组合很香?

单独用 NotebookLMOpenClaw + NotebookLM
手动找资料、手动上传自动搜集、批量导入
资料零散、质量参差不齐筛选过的高质量来源
一个项目搞完就结束可以定期更新、持续追踪

本质上就是把”信息搜集和筛选”这种费时费力的脏活外包给 OpenClaw,让 NotebookLM 专注于它最擅长的”深度分析和结构化输出”。


进阶案例 2:扔一个链接,自动抓取所有关联内容并落盘

这是我日常用得最多的场景之一,几乎每天都会触发。刷推或者看公众号的时候看到一篇好文章,里面提到了几篇论文、几个 GitHub 仓库、几个相关链接、几本推荐的书籍。以前的做法你一定不陌生:收藏 → 告诉自己回头看 → 忘记 → 再也没看过。现在有了 OpenClaw,做法变了:

1
2
3
4
5
6
7
我:https://example.com/awesome-article
把这篇文章里提到的所有内容都抓下来:
- 论文 → 下载 PDF
- GitHub 仓库 → star + 克隆到本地
- 相关链接 → 抓取正文
- 书籍 → 记录到待读清单
最后整理成一份 Markdown 落盘到 Obsidian

OpenClaw 会读取原文、识别所有链接和引用,然后分类处理:arXiv/论文链接下载 PDF 到 论文/ 目录,GitHub 链接判断是否值得 star、重要的克隆到本地,普通网页抓取正文转成 Markdown,书籍链接提取书名加入待读清单。最后生成一份包含原文摘要、所有关联内容链接和简介、落盘位置的索引 Markdown,自动保存到 Personal-Knowledge/source/ 目录。一个链接,一条指令,全套内容自动归位。 以后想回顾的时候打开 Obsidian 搜一下关键词就行,所有关联资料都已经按类型归好位了。

如果你是 Android 开发者

这是我目前还没做但正在规划中的方向,列出来看看有没有跟你产生共鸣的场景:

痛点OpenClaw 能做什么适用
Trace 分析门槛高上传 Perfetto/Systrace → AI 辅助解读 → 生成”人话版”报告App 性能优化、系统 UX 性能
版本适配清单自动抓 Android Release Notes → 生成适配 checklist → 对比代码标记检查点App 开发者(每年必做)
Issue 堆积难筛选自动分类(bug/feature/question)→ 优先级判断 → 提取关键信息开源维护者、团队协作
Code Review 耗时检测常见问题模式(空指针、资源泄漏、主线程 IO)→ 生成 Review checklist所有团队
技术债务遗忘定期扫描 TODO/FIXME → 按优先级排序 → 定期提醒”该还债了”所有项目
Benchmark 没人看定期跑 benchmark → 自动对比历史 → 异常告警(启动 +500ms)性能优化团队
内部文档找不到基于知识库回答”之前遇到过吗?””设计文档在哪?”所有团队
上游变更追踪监控 AOSP/厂商代码变更 → 提醒需要合并的点系统开发者

这些能力并不是开箱即用的,需要根据你自己的业务场景做定制开发。但 OpenClaw 的架构天然支持这些方向的扩展——关键是先把基础设施养起来,再根据实际需求慢慢迭代

一句话总结

论文每天读、仓库每天盯、知识每天理、日报每天写。它真正做到的是,把”需要长期坚持”这件反人性的事情变成了”系统在后台默默帮你执行”。

3. 本地模型可以干什么?

本地模型的定位并不是替代云端大模型,而是专门负责那些脏活、累活和高频重复的任务。一句话概括分工:云端”想得深”,本地”跑得勤”。

云端 + 本地分工

本地模型承担了多少?

我的实际运行数据:

任务类型模型占比
巡检、监控、批处理本地 Qwen3.5-27B30%
状态检查、轻量分类本地 Qwen3.5-4B20%
极轻任务本地 Qwen3.5-2B10%
云端失败降级本地 Qwen3.5-9B5%
长文、精修、复盘云端 GLM515%
日报周报、审计云端 GLM510%
论文精读、ClawFeed云端 GLM510%

按任务数量算,本地承担了 60-70%;按 Token 消耗算,本地承担了 80%+。

我的配置

硬件是一台 Mac Studio(M 系列 Apple Silicon),上面部署了多个规格的本地模型:Qwen3.5-2B/4B/9B 使用 MLX 8bit 量化,负责轻量任务和云端故障时的容灾降级;Qwen3.5-27B 通过 Ollama 部署,承担高频结构化任务,是本地的主力干活模型。顺便提一句,Apple Silicon 上用 MLX 框架做推理的效率明显比 Ollama 高,尤其是跑小模型的时候差距更明显。

分工原则

本地适合云端更强
高频结构化任务(RSS、队列处理)长文写作与精修
巡检与监控复杂推理与判断
数据预处理(不离开本机)高质量翻译与解读
云端降级接管论文精读、深度分析

至于成本,硬件本来就是已经购入的设备,电费增加也不多,所以本地模型的边际成本接近零

烧 Token 的速度

即使有本地模型分担,云端 Token 还是烧得飞快。我的配置是主模型 GPT-5.4 + 干活主力 GLM5 + 本地苦力 Qwen3.5 系列(27B/9B/4B/2B)。所以我的建议是直接上量大管饱的 Coding Plan 套餐,预算充裕的话可以选 GPT Pro,追求性价比的话 MiniMax 或者 Kimi 都是不错的选择。按量付费的话,以这个消耗速度你一定会心疼的。

备用通道

虾哥前期运行偶尔会出现不稳定的情况,这时候我会用 Claude Code(Remote Control)远程连过去手动排查和救火。这种情况出现的频率不高,但有备用通道在手里心里踏实很多。

分工逻辑

打个比方,本地模型是”勤奋的蓝领工人”,云端模型是”聪明的高级顾问”。OpenClaw 做的事情就是帮你把这套分工协作的逻辑彻底自动化了。

4. 隐私和安全怎么保护?

这个问题被问得非常多,也确实是每个人在决定养虾之前必须认真想清楚的事情。

隐私与安全

核心原则

敏感数据不离开本机。 这是我给自己划的底线。具体的做法也很简单直接:

  1. 敏感文件不上云端:公司代码、内部文档、个人隐私相关的内容,一律只走本地模型处理
  2. 本地优先:能在本地解决的任务,绝不走云端 API
  3. 最小权限原则:OpenClaw 只能访问我明确授权过的目录和工具,其他一概不给权限

技术层面的防护

命令执行白名单方面,safeBins 配置严格限制了它能执行的命令范围,只允许 lscatgrepfindgit 这些基础只读命令,而像 rm -rf 这样的危险操作会被 deniedFlags 直接拦截。也就是说即使有人尝试通过提示注入诱导它执行恶意操作,命令层面也根本跑不通。

来源校验方面,commands.ownerAllowFromchannels.allowFrom 做了来源白名单,只允许我自己的账号发送系统级指令,即便在群聊里有其他人@它也不会响应任何敏感操作。

另外还有配置文件哈希校验,每天自动对 openclaw.json 生成 SHA256 哈希值并与基线值对比,一旦检测到配置文件被篡改系统会立刻触发告警通知。加上定期巡检,每天自动扫描 workspace 目录下的所有文件,检测是否存在私钥、密码、助记词等敏感信息,一旦发现就立即告警通知绝不静默忽略。

运行层面的隔离

  • OpenClaw 跑在独立的 Mac Studio 上,不和我日常开发的机器混用
  • 敏感账号(公司邮箱、内部系统)不接入 OpenClaw
  • 微信公众号等对外发布渠道,只开放”草稿保存”,不开放”直接发布”

信任边界

我现在的策略是:OpenClaw 能看到的东西,不会比我主动发给云端 AI 的内容更敏感。 你想想看,日常用 ChatGPT、Claude 的时候也会上传文件、发截图,那些数据的暴露面其实更大、更不可控。OpenClaw 反而是相对受控的——它的权限范围、它的行为日志、它的所有操作记录全部都在我本地机器上,随时可以审计。

安全的本质

安全从来不是 AI Agent 自带的默认行为,而是需要你在部署之前主动设计和配置出来的。养虾之前先把笼子扎紧,这个顺序千万不能反。

5. 使用体验:手机随时操控,数据不会丢

这一条其实被问得不算多,但我个人觉得它是 OpenClaw 用起来真正”顺手”的关键所在,值得单独拎出来说说。

使用体验

Telegram:一个主 bot + 若干群聊

我的用法是这样:主 bot 用私聊做日常对话、发指令、问问题;不同的 daily task 转发到不同的群——Android 群放技术简报、论文精读、GitHub 动态,Daily 群放日报、周报、系统通知,其他群按需分流。为什么要分群?原因有三个:

  1. 信息不混杂:技术内容和日常运维日志分开管理,想看哪个类别就进哪个群
  2. 通知可控:重要的群打开通知提醒,不重要的直接静音,避免信息轰炸
  3. 协作方便:有些群可以拉同事一起加入,共享信息源

数据落盘到 Obsidian

Telegram 在这个体系里只是”前端展示层”,真正重要的内容最终都会落盘到 Obsidian。这种架构设计意味着三件事:

  • 不会丢:Telegram 的消息会随着时间被刷掉,但 Obsidian 里的 Markdown 文件会一直留存
  • 可检索:Obsidian 的全文搜索能力远比 Telegram 的聊天记录搜索好用
  • 可整理:后期可以随时对内容重新组织结构、打标签、建立双向链接

我现在 Obsidian 里的结构:

  • OpenClaw定时任务/:所有定时任务的输出
  • 论文/:论文精读三件套
  • Personal-Knowledge/source/:知识库(1760 篇)
  • X 文章/:归档的高价值 X 内容

手机随时访问

这一点的重要性比我最初想象的要大得多。出去玩的时候掏出手机打开 Telegram 就能直接给虾哥发指令,跟在电脑前操作没有区别;想看今天的日报打开 Obsidian 手机端文件已经通过 iCloud 同步过来了;遇到紧急情况需要调整任务也不用专门开电脑,手机上就能完成操作。我现在的日常习惯是早上醒来先刷一下 Telegram,看看昨晚虾哥跑了哪些任务、有没有异常,有问题的直接在手机上回复处理。外出的时候也能随时看一眼运行状态,心里有数。

OpenClaw 状态卡片

架构总结

前端是 Telegram,后端是 Obsidian。 Telegram 解决的是”随时随地都能触达”的问题,Obsidian 解决的是”数据永远不会丢”的问题。两者结合在一起,才构成了一个真正好用的 24/7 全天候助手体验。

相关文章

  1. 我把 OpenClaw 跑在本地三周后,发现它根本不是聊天机器人

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android 性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

我把 OpenClaw 跑在本地三周后,发现它根本不是聊天机器人

最近这段时间,我一直在本地重度使用 OpenClaw。最开始我也把它当成一个 AI 工具,但真正把它接进 Telegram、Obsidian、定时任务、本地模型和内容工作流之后,我发现自己完全想错了——它最厉害的地方,不是回答问题,而是开始替你持续工作。 它能接消息、调工具、跑定时任务、调用不同模型、沉淀长期记忆、把结果回写 Obsidian,还能把复杂任务分发给别的 Agent。你如果只把它当聊天机器人,那基本等于只用了它 20% 的能力。

对我这种既做 Android 系统性能优化、又要做内容、运营社群、维护知识库、跟进项目、写代码的人来说,OpenClaw 最值钱的地方只有一句话:它让我第一次有了”AI 不只是回答我,而是真的在替我持续推进工作”的感觉。

OpenClaw 工作流总览

1. 我是什么时候开始真正用 OpenClaw 的?

按本地环境的初始化痕迹看,我是在 2026 年 2 月 16 日晚上开始把 OpenClaw 真正落到自己机器上的。这不是”装来看看”的那种体验,而是一路接进自己的日常工作流:消息入口、定时任务、Obsidian 知识库、日报周报、GitHub 监控、论文精读、内容素材整理、创作与复盘。所以我现在已经不把它当”一个工具”,而是把它当成一个本地 AI 调度层、一个定时执行系统、一个长期记忆系统、一个内容与知识的自动整理器,以及一个能和 Claude Code 配合工作的 AI 中枢。

2. 对一个 Android 系统工程师来说,OpenClaw 到底有什么用?

如果你本职是 Android 工程、系统优化、性能分析、稳定性治理,OpenClaw 最有价值的,不是”替你答一道题”,而是帮你处理那些持续重复、跨工具、跨时间、跨上下文的工作。我现在主要把它用在四类事情上。

第一类:信息流自动化

每日技术简报、每日信息简报、RSS 抓取、高价值内容筛选、GitHub Issue / PR 监控、每日论文精读——这些事情以前不是不会做,而是太碎了,做着做着就断。OpenClaw 的价值就是把这些动作从”靠意志力”变成”靠系统默认执行”。

第二类:知识库持续整理

我平时会收很多内容:技术文章、公众号、X 长文、RSS、项目资料、自己的思考。以前最大的问题是:收藏了但没整理,整理了但找不到,找到了也很难复用。OpenClaw 接进来之后,会持续做增量整理、结构修复、高价值归档、内容回顾和记忆维护。这件事对工程师特别重要,因为成长靠的不是”看过很多”,而是以后还能拿出来继续用

第三类:日报、复盘和周检

这是很多人最容易忽略,但最有复利的一部分。我现在让 OpenClaw 长期跑每日早间提醒、今天干了啥日报、日终复盘、周沉淀、项目周审、三层记忆维护、冥想 / evolution log。说白了,OpenClaw 最让我上瘾的一点就是:它把那些”我知道很重要,但人很难长期坚持”的事情,变成了系统自动帮我做。

第四类:工程协作

它也能参与工程工作,比如跟踪 GitHub 仓库变化、巡检 issue / PR、整理代码上下文、调用其他 coding agent 干活。但这里我要先说结论:OpenClaw 不是为了替代 Claude Code,而是为了编排 Claude Code。 Claude Code 更像一个”坐在终端里的顶级程序员”;OpenClaw 更像一个”AI 运营系统”。

3. 为什么我旁边常备 Claude Code?

因为 OpenClaw 早期真的会挂。我说得更直白一点:升级后会报错、模型配置会出问题、定时任务会异常、某些工具链会失灵、某些路径写入会失败、旧会话和新配置不一定同步。这时候如果你只有 OpenClaw 自己,很多问题会非常烦,但如果旁边有个 Claude Code,事情就简单很多。

我的实际分工很清楚:OpenClaw 负责调度、记忆、定时、归档、消息、工作流;Claude Code 负责修 bug、查日志、看报错、做复杂代码改动、升级后救火。简而言之,Claude Code 像顶级程序员,OpenClaw 像 AI 运营系统。

Claude Code 与 OpenClaw 协作

4. 我的本地配置是什么?

我现在这套机器是 Mac Studio M1 Ultra,64GB 内存,48 核 GPU。这套配置对本地 Agent 工作流非常舒服,因为它允许我同时跑 OpenClaw 常驻服务、Ollama 本地模型、Obsidian、浏览器自动化、以及各种脚本和知识库任务。我现在不是单模型打法,而是云端 + 本地混合分工

模型搭配思路很简单

高质量任务上云(长文、周报、复杂总结、关键判断),高频脏活本地跑(结构化任务、巡检、批处理、日常 worker)。我本地主要是这套分层:2B 做状态检查,4B 做轻量巡检,9B 是高频主力,27B / 35B 负责更重的本地任务。对我这台机器来说,9B 是最划算的日常工作马

5. OpenClaw 最重要的,不只是模型,而是那几份核心文件

很多人第一次接触 OpenClaw,会把注意力放在模型、工具、命令行上。但我现在越来越觉得,它真正强大的地方反而是那几份被文件化的规则。我自己最看重的是这几类文件:

  • SOUL.md:定义它的性格、风格、做事姿态
  • USER.md:定义它到底在帮谁、目标是什么
  • AGENTS.md:定义启动流程、记忆规则、安全边界、群聊行为
  • MEMORY.md + memory/YYYY-MM-DD.md:一个存长期记忆,一个存每天发生了什么
  • TOOLS.md:记录这台机器独有的本地环境信息

所以我现在对 OpenClaw 的理解是:它不是靠一大段系统提示词活着,而是靠一套”文件化人格 + 文件化记忆 + 文件化规则”活着。

OpenClaw 核心文件框架

6. Skill 机制为什么重要?

很多人把 Skill 理解成”插件”,但我觉得更准确的理解是能力包。一个 Skill 往往不只是多一个按钮,而是把适合什么场景、怎么调用工具、是否依赖脚本、有哪些边界和注意事项、在什么情况下该用或不该用这些东西一起打包了。比如 Obsidian、coding-agent、RSS、技能审计这些能力,一旦装对,OpenClaw 就不再只是聊天,而是真的开始干活。但我的建议也很明确:不要为了炫技乱装 Skill。 先装核心能力,真正跑两天确认稳定再继续扩;涉及高权限或外联的 Skill,先审计再决定要不要上。

7. Telegram 这块,单 Agent 多群聊能用,但后期最好按职责拆

如果你只在私聊里用 OpenClaw,其实还没完全体会到它的架构价值。它真正好玩的地方之一,是同一套系统可以接 Telegram,而且不一定只能对应一种工作方式。

方案 A:单 Agent 多群聊

优点是简单:配置快、起步成本低、前期验证最省事。但缺点也很明显:上下文容易串味,不同群的语气和任务容易混在一起,写作、日报、技术答疑很容易互相污染。

方案 B:按职责拆成多个 Agent

这才是我现在更认可的方式。比如 main 负责私聊主会话,daily 负责定时任务、日报、巡检、知识库类工作,writer 负责写作和内容精修类任务,专题 Agent 负责某个群、某条业务线或某个项目。这样做的好处是上下文隔离更清楚、角色边界更明确、不同群不会互相污染、后期扩展更轻松。我的建议是:前期可以单 Agent 多群聊,后期一定要按职责拆。

Telegram 多 Agent 架构

8. 安全不是附录,而是前置条件

这件事我非常建议一开始就讲透。因为 OpenClaw 一旦开始接本地文件系统、浏览器、命令执行、外部消息入口、本地和云端模型混合调用,它就不再是一个”无害聊天框”了,而是一个真正有行动能力的系统。这个时候,安全边界一定要先立住。

我现在比较认同的几条原则是:

  • 长期记忆要分层:像 MEMORY.md 这种更私人、更稳定的信息,最好只在主会话里使用,不要在群聊上下文里乱读乱用
  • 外部发送要谨慎:邮件、公开发帖、社交平台发布,最好默认需要确认,不要自动对外表达半成品
  • 群聊不要越权:在群里它是参与者,不是代言人,更不是你本人
  • Skill 不要乱装:尤其是涉及联网、执行脚本、读本地路径的能力,先审计再说
  • 本地小模型别无脑给大权限:尤其是接网页、执行命令、读写路径时,要清楚工作区边界和工具边界

如果你后面还要接更多外部能力、浏览器自动化或者海外模型,通常还需要稳定的科学上网环境,但能力越多越要先把边界设计好。

9. 还有一个特别关键的问题:它到底留下了什么成果?

如果文章只写”它能做什么”,读者还是容易觉得这是一套概念系统。真正有说服力的,其实是这套系统已经在 Obsidian 里沉淀出了什么成果物。截至我写这篇文章时,我的 Obsidian 里已经能直接看到这些成果:

Obsidian 成果墙

  • OpenClaw定时任务/ 下有 214 篇 Markdown,说明日报、周检、归档、巡检已经不是口号,而是持续产出
  • Personal-Knowlodge/source/ 下有 1760 篇 Markdown,知识库已经不是空目录,而是真在增长
  • X 文章/ 下有 115 篇 Markdown,高价值外部内容已经沉淀成长期素材库
  • 论文/ 下有 4 个标准化论文目录,每个目录都按”原 PDF + 翻译 + 精读”来组织
  • 小说工坊/夜航之上(分章)/ 下有 58 个章节相关文件,它已经进入长期创作工作流,而不只是技术任务
  • Ebook/ 里已经有实际 EPUB 成果,不只是笔记,还有可交付格式

如果你看几个具体例子,会更有感觉:OpenClaw定时任务/每日论文精读(Android+AI)/2026-03-08-每日论文精读(Android+AI).mdPersonal-Knowlodge/source/2026-03-08_wechat_Android_JNI原理分析.md论文/AI-2026-03-08-Agentic-Reasoning-Framework/01-paper.pdf + 02-翻译.md + 03-精读.md小说工坊/夜航之上(分章)/第018章-光标闪烁.md。这也是我现在最看重 OpenClaw 的地方:它不是做完就没了,而是在不断把工作变成资产。

10. 我踩过哪些坑?

这部分非常重要,因为它决定你会不会半路放弃。

坑 1:它初期真的没那么稳

你要接受一个现实:OpenClaw 很强,但并不等于”零维护”。我踩过的坑包括升级后报错、模型 allowlist 没配对、模型切换后旧会话不生效、定时任务能跑但落盘失败、外部路径权限问题、浏览器策略问题等等。

坑 2:Node 环境不要混

我后面把运行环境统一切到了 Homebrew Node,不再混用 nvm。这个动作非常有必要,不然后面升级、重启、路径都容易乱。

坑 3:Obsidian 外部路径别乱写

这是我踩得很深的一个坑。后来我收敛成非常明确的规则:

  • 必须绝对路径
  • 最稳是 exec + python/pathlib 落盘
  • 不要想当然地直接 write/edit
  • 同日内容尽量追加,不要覆盖

坑 4:自动化不是越多越好

一开始很容易上头,这个也想自动化、那个也想自动化。但最后你会发现任务太多就会带来重复、冲突、噪音、token 成本上升,结果反而没人真正看。所以更好的策略不是”全自动化”,而是先抓最有复利的几条主线。

11. 什么样的人适合用 OpenClaw?

我觉得最适合的是三类人。第一类是有持续输入和输出需求的人,比如工程师、独立开发者、技术博主、社群运营者、研究型创作者。第二类是愿意折腾工作流的人,如果你对”自动化、系统化、结构化”天然感兴趣,OpenClaw 会越用越顺手。第三类是想把 AI 真正接进工作流的人,如果你不是只想问几个问题,而是真的想做持续监控、自动整理、主动提醒、定时产出、长期记忆、跨平台内容协同,那 OpenClaw 会很有用。

12. 什么样的人不太适合?

也要说实话。只想把它当 ChatGPT 替代品的人、不想维护环境不想看日志的人、没有持续工作流的人,这三类人不一定用不好,但大概率感受不到它真正的价值。

13. 硬件怎么推荐?

如果你问我推荐什么机器,我会分三档说:

  • 入门档:16GB 内存,主要跑云端模型,本地只做少量辅助
  • 实用档:32GB 内存,能跑一部分本地模型,可以承接中轻量任务
  • 舒服档:64GB 及以上,Mac Studio 或高配工作站级机器,云端 + 本地混合长期常驻

如果你真想把 OpenClaw 跑成”长期常驻、云端+本地混合、多任务并行”的系统,64GB 这一档体验会明显更稳

14. 我现在对 OpenClaw 的最终判断

如果只让我用一句话总结,我会这么说:OpenClaw 最迷人的地方,不是它更会回答问题,而是它开始替你持续推进工作。 对 Android 系统工程师来说,它最有价值的地方不是取代你写代码,而是帮你把输入、整理、归档、跟进、输出、复盘、记忆这些本来散落在各处的事情串起来。它不是零门槛工具,它会挂、会报错、会踩坑,但一旦你把它跑顺,你会很明显地感受到:以前是你在推着工作流走,后来是系统在推着你往前走。 这就是我觉得它最值得折腾的地方。

结尾

如果你本身就是工程师、创作者,或者正在尝试把 AI 接进真实工作流,而不是只把它当聊天窗口,那 OpenClaw 值得你认真折腾一次。它不一定适合所有人,但一旦跑顺,带来的不是”对话效率提升”,而是工作流层面的复利

相关文章

  1. OpenClaw 常见问题解答:Token 消耗、能干什么、本地模型、隐私安全、使用体验

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android 性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

Android Perfetto 系列 10 - Binder 调度与锁竞争

本文是 Perfetto 系列的第十篇文章,聚焦 Binder 这一 Android 跨进程通信的核心机制。Binder 承载着大部分系统服务与应用的交互,也常常是性能瓶颈的源头。本文站在系统开发与性能调优的视角,结合 linux.ftrace(binder tracepoints + sched)、thread_state 轨道,以及 ART 的 Java Monitor Contention(通过 atrace 的 dalvik 类别采集)等信号,给出一套可直接落地的诊断流程,帮助初学者和进阶开发者定位耗时、线程池压力与锁竞争等问题。

本系列的目标,就是通过 Perfetto 这个工具,从一个全新的图形化视角,来审视 Android 系统的整体运行,同时也提供一个学习 Framework 的新途径。或许你已经读过很多源码分析的文章,但总是对繁杂的调用链感到困惑,或者记不住具体的执行流程。那么通过 Perfetto,将这些流程可视化,你可能会对系统有更深入、更直观的理解。

本文目录

Perfetto 系列文章

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

Binder 基础与案例

对于首次接触 Binder 的读者,理解它的角色和参与者至关重要。可以先把 Binder 粗暴地理解成“跨进程的函数调用”:你在一个进程里像调用本地接口一样写代码,真正的调用和数据传输则由 Binder 帮你完成。整体上它是 Android 的主力跨进程通信(IPC)机制,核心包含四个组件:

  1. Client:应用线程通过 IBinder.transact() 发起调用,将 Parcel 序列化的数据写入内核。
  2. Service(Server):通常运行在 SystemServer 或其他进程中,通过 Binder.onTransact() 读取 Parcel 并执行业务逻辑。
  3. Binder Driver:内核模块 /dev/binder 负责线程池调度、缓冲区管理、优先级继承等,是连接双方的“信使”。
  4. Thread Pool:服务端通常维护一组 Binder 线程。需要注意的是,线程池并不是一开始就创建满的,而是按需创建。Java 层默认最大线程数约为 15 个 Binder 工作线程(不含主线程),Native 层通过 ProcessState 也可以配置最大线程数(默认值通常也是 15)。当所有 Binder 线程都忙碌时,新的请求就会在驱动层排队等待空闲线程。

为什么需要 Binder?

Android 采用多进程架构来隔离应用、提升安全性与稳定性。每个 APK 运行在独立的用户空间,当需要访问系统能力(相机、位置、通知等)时,必须跨进程调用 Framework 或 SystemServer。

传统 IPC 方案的局限:

IPC 方式问题
Socket开销大,缺少身份校验
Pipe仅支持父子进程,单向通信
共享内存需要额外的同步机制,缺少访问控制

Binder 在内核层解决了这些问题,提供了三个关键能力:一是身份与权限(基于 UID/PID 校验,确保调用方合法);二是同步与异步调用(同步模式下 Client 等待 Server 返回,这是最常见的模式,而异步模式下 Client 发送后立即返回,适用于通知、状态上报等场景);三是优先级继承(当高优先级 Client 调用低优先级 Server 时,Server 会临时提升优先级,避免优先级反转问题)。

因此,当应用进程在启动阶段通过 IActivityManager#attachApplication() 把自己“挂到” SystemServer 时,底层必然借助 Binder 把调用安全、可靠地传递给 system_server

从 App 开发者视角的案例

假设我们在 Trace 里关注到 AIDL::java::IActivityManager::attachApplication::server。它对应的是应用进程通过 IActivityManager#attachApplication(...) 发起的一次同步 Binder 调用,服务端实现位于 system_serverActivityManagerService。调用路径可以概括为:首先,在 Proxy 侧,应用进程通过 ActivityManager.getService() 拿到一个 IActivityManager 的代理对象(BinderProxy);然后进行序列化,调用 attachApplication(...) 时,代理会把参数写入 Parcel,执行 transact();接着是内核传输,Binder 驱动将该事务排入 system_server 的 Binder 线程队列,并唤醒一个空闲线程(例如 Binder:1460_5);随后在 Stub 侧ActivityManagerService(Stub)所在的线程被唤醒,读取参数并进入 attachApplication 的处理流程;最后是返回阶段,Service 处理完毕,将结果写入 Parcel,驱动唤醒原 App 线程,App 线程从 waitForResponse() 返回继续执行。

在 Perfetto 中,这条链路会显示为:Android Binder / Transactions 轨道上的一次事务(如果 trace 中能解析到 AIDL 信息,Slice 名称会类似 AIDL::java::IActivityManager::attachApplication::client/server,或在 SQL 中体现为 aidl_name=IActivityManagermethod_name=attachApplication);App 线程在 thread_state 里处于 S (Sleeping) 状态(同步调用时常见),且 blocked_function 通常涉及 binder_thread_read / epoll_wait / ioctl(BINDER_WRITE_READ);SystemServer 的 Binder 线程出现 Running 切片;以及 Flow 箭头(Perfetto 会用箭头把 Client 的 transact 和 Server 的处理线程连接起来)。

image-20260207113358540

Perfetto 观测准备

要在 Perfetto 中诊断 Binder,需要提前准备好数据源与 Trace 配置。

数据源与轨道总览

Binder 分析需要把「事务事件」和「线程调度/阻塞/锁」串起来。录制侧主要依赖 linux.ftrace(包含 Binder tracepoints、调度事件以及可选的 atrace 类别),再配合少量元数据源(进程/线程命名映射)。

linux.ftrace(内核层 + atrace) 是最通用、最基础的数据源,兼容所有 Android 版本。它直接读取内核的 ftrace 事件,包括 binder_transaction(事务开始)、binder_transaction_received(服务端收到事务)、binder_transaction_alloc_buf(缓冲区分配,诊断 TransactionTooLarge)等;再配合调度相关事件(sched_switchsched_waking)即可还原出 “Client 发起调用 → 内核唤醒 Server 线程 → Server 处理 → 返回” 的链路。
另外,linux.ftrace 里还可以开启 atrace 类别来补充用户态 Slice:binder_driver/am/wm 等有助于解释系统服务语义;dalvik 则用于采集 ART 的 monitor contention(Java synchronized 竞争),从而在 UI 里出现 Thread / Lock contention 相关轨道。

linux.process_stats(元数据) 用于把 PID/TID 映射成进程名/线程名,方便在 UI 和 SQL 中阅读与过滤。开销极低,建议常开。

说明:Perfetto UI 中的 Android Binder / TransactionsAndroid Binder / Oneway Calls 轨道,以及 PerfettoSQL 标准库中的 android.binder / android.monitor_contention 模块,都是在 trace processor 侧基于上述原始事件解析/聚合出来的,并不是需要额外开启的“录制数据源”。

Trace Config 推荐

以下配置兼顾了兼容性与新特性,建议作为标准的 Binder 分析模板。将配置保存为 binder_config.pbtx 即可使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# ============================================================
# Binder 分析专用 Perfetto 配置
# 适用范围:Android 10+(Android 12+ 在路径/权限上更省心)
# ============================================================

# --- 缓冲区与时长设置 ---
buffers {
size_kb: 65536 # 64MB 缓冲区,适合中等复杂度场景
fill_policy: RING_BUFFER
}
duration_ms: 15000 # 15 秒抓取时长,可根据需要调整

# --- 数据源 1:linux.ftrace (内核 + atrace) ---
# 最通用的数据源:Binder tracepoints + sched 事件 +(可选)atrace 类别
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
# Binder 核心事件
ftrace_events: "binder/binder_transaction" # 事务开始
ftrace_events: "binder/binder_transaction_received" # 服务端收到事务
ftrace_events: "binder/binder_transaction_alloc_buf" # 缓冲区分配(诊断 TransactionTooLarge)
ftrace_events: "binder/binder_set_priority" # 优先级继承
ftrace_events: "binder/binder_lock" # 内核锁(通常可省略)
ftrace_events: "binder/binder_locked"
ftrace_events: "binder/binder_unlock"

# 调度事件(串联 Client/Server 线程)
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_blocked_reason" # 阻塞原因

# 可选:应用层 Trace 点(需要 atrace)
atrace_categories: "binder_driver" # Binder 驱动层
atrace_categories: "sched" # 调度
atrace_categories: "am" # ActivityManager
atrace_categories: "wm" # WindowManager
atrace_categories: "dalvik" # Java Monitor Contention(锁竞争)
# atrace_categories: "view" # 如需分析 UI 可开启

# 如需抓取应用侧 atrace Slice(例如 doFrame / 自定义 Trace),可指定:
# atrace_apps: "你的应用包名"
# 或者抓全部(Trace 体积会变大):
# atrace_apps: "*"

# 符号化内核调用栈
symbolize_ksyms: true

# 优化调度事件存储,减少 Trace 体积
compact_sched {
enabled: true
}
}
}
}

# --- 数据源 2:linux.process_stats (进程信息) ---
# 提供进程名、PID 等基础信息
data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
}
}
}

配置项说明

数据源作用Android 版本要求开销
linux.ftrace (binder/*)内核层 Binder 事件所有版本
linux.ftrace (sched/*)调度事件,串联线程唤醒所有版本
linux.ftrace (atrace: dalvik/…)Framework Slice + Java Monitor Contention所有版本(字段随版本演进)低-中
linux.process_stats进程名称和 PID 映射所有版本极低

提示:本文的 Binder 分析工作流只依赖 linux.ftrace(binder tracepoints + sched + dalvik),因此 Android 12/13/14+ 的抓取思路基本一致。不同版本的 UI 字段名可能略有差异,遇到差异时推荐用 Perfetto SQL(stdlib)做校验。

快速上手:3 步抓取与查看 Binder Trace

  1. 抓取 Trace

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 推送配置
    adb push binder_config.pbtx /data/local/tmp/

    # 开始抓取
    adb shell perfetto --txt -c /data/local/tmp/binder_config.pbtx \
    -o /data/misc/perfetto-traces/trace.pftrace

    # ... 操作手机复现卡顿 ...

    # 取出文件
    adb pull /data/misc/perfetto-traces/trace.pftrace .
  2. 打开 Trace:访问 ui.perfetto.dev,拖入 trace 文件。

  3. 添加关键视图

    • 左侧点击 TracksAdd new track
    • 搜索 “Binder”,添加 Android Binder / TransactionsAndroid Binder / Oneway Calls
    • 搜索 “Lock”,添加 Thread / Lock contention(如果有数据)

其他 Binder 分析工具

除了 Perfetto,还可以用两个工具辅助定位:am trace-ipc(系统自带)和 binder-trace(开源,能力更强但门槛更高)。

am trace-ipc:Java 层 Binder 调用追踪

am trace-ipc 用于追踪 Java 层 Binder 调用堆栈。系统会在目标进程开启 Binder stack tracking(BinderProxy.transact() 路径),在停止时导出文本统计。优点是零配置、无需 root。

基本用法很简单,就是”开始 → 操作 → 停止导出”三步:

1
2
3
4
5
6
7
8
9
10
# 1. 开始追踪(系统会记录符合条件进程的 Binder 调用,通常以可调试进程为主)
adb shell am trace-ipc start

# 2. 在手机上执行你要分析的操作(比如启动某个应用、触发卡顿场景等)

# 3. 停止追踪并导出结果到文件
adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt

# 4. 把结果文件拉到电脑上查看
adb pull /data/local/tmp/ipc-trace.txt

导出结果是纯文本,示例如下:

1
2
3
4
5
6
7
Traces for process: com.example.app
Count: 15
java.lang.Throwable
at android.os.BinderProxy.transact(BinderProxy.java:xxx)
at android.app.IActivityManager$Stub$Proxy.startActivity(...)
at android.app.Instrumentation.execStartActivity(...)
...

它会按进程分组,列出调用堆栈和次数(Count),适合快速回答“调了哪些服务、调了多少次”。

与 Perfetto 配合使用:Perfetto 看时间线与线程关系,trace-ipc 补“具体是哪个 Java 调用点发起调用”。

适用场景:怀疑卡顿/ANR 与频繁 IPC 有关,或需要定位具体 Java 发起点。

binder-trace:实时 Binder 消息解析

binder-trace 可以实时拦截并解析 Binder 消息,常被称为“Binder 的 Wireshark”,能看到接口、方法及部分参数。

它基于 Frida 动态注入,通常需要 root(或模拟器)和 frida-server,本地需 Python 3.9+。示例:

1
2
# 追踪指定应用的 Binder 通信(-d 指定设备,-n 指定进程名,-a 指定 Android 版本)
binder-trace -d emulator-5554 -n com.example.app -a 11

它支持按接口/方法/事务类型过滤,适合安全研究和逆向分析这类“看消息内容”的场景。日常性能排查通常仍以 Perfetto + am trace-ipc 为主。

Binder 分析工作流

拿到 Trace 后,不要直接在大海捞针。推荐按照“找目标 → 看耗时 → 查线程 → 找锁”的顺序进行。

步骤一:定位事务耗时

分析的第一步是找到你关心的那次 Binder 调用。在 Perfetto 中有几种常用的定位方式:如果你已经知道是哪个进程发起的调用,可以直接在 Transactions 轨道里找到你的 App 进程作为 Client 的区域;如果你知道调用的接口名或方法名,可以按 / 键打开搜索框,输入 AIDL 接口名(如 IActivityManager)、方法名(如 attachApplication),或者直接输入完整的 Slice 名(如 AIDL::java::IActivityManager::attachApplication::server)来快速定位;如果你是在排查 UI 卡顿问题,最直接的方式是先看 UI 线程的 thread_state 轨道,找到处于 S(Sleeping)状态且时长较长的片段——如果这段时间主线程几乎没有在执行代码,那很可能就是在等待 Binder 调用返回,这里就是分析的起点。

选中一个 Transaction Slice 后,右侧的 Details 面板会显示这次事务的详细信息(Client/Server 线程、时间戳、耗时等)。不同版本的 Perfetto UI 字段名可能略有差异,但你可以用 Perfetto SQL 的 android_binder_txns 来统一理解几个关键耗时:

  • client_dur:客户端端到端耗时(同步调用时基本等同于“我在等这次 Binder 返回”的时间)
  • server_dur:服务端从开始处理到(同步时)发出 reply 的 wall clock 时长
  • dispatch_dur = server_ts - client_ts:从客户端发起到服务端真正开始处理的延迟(常包含排队/线程可用性/调度影响)

下面这段 SQL 可以直接在 Perfetto UI 的 SQL 页面运行(用于快速找出最慢的同步事务,并拆出派发延迟与服务端耗时):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INCLUDE PERFETTO MODULE android.binder;

SELECT
aidl_name,
method_name,
client_process,
client_thread,
client_dur / 1e6 AS client_ms,
server_process,
server_thread,
server_dur / 1e6 AS server_ms,
(server_ts - client_ts) / 1e6 AS dispatch_ms
FROM android_binder_txns
WHERE is_sync
ORDER BY client_dur DESC
LIMIT 20;

image-20260207110047680

理解这些耗时之间的关系非常重要,因为它直接决定了你下一步应该往哪个方向深挖。如果 client_dur 很长但 server_dur 很短,通常说明慢主要不在服务端处理,而是在派发/排队(dispatch_dur 会很大),这时应该优先检查服务端线程池与调度情况(步骤二)。如果 server_dur 本身就很长,说明服务端处理慢,这时你需要跳转到服务端的 Binder 线程,看它在这段时间里到底在干什么——是在跑业务代码、等锁、还是等 IO。

步骤二:评估线程池与 Oneway 队列

如果步骤一的分析发现耗时主要不在服务端处理,而是在”排队”上,那就需要进一步检查 Binder 线程池的状态了。在深入分析之前,先回答一个经常被问到的问题:**”每个进程大概会有多少个 Binder 线程?system_server 的 Binder 线程池规模大致是什么量级?什么情况下会’耗尽’?”**

system_server 的 Binder 线程池规模

在上游 AOSP(Android 14/15)中,Binder 线程池的设计思路是:按需增长、可配置、没有单一固定数字

  • 线程池是按需增长的:每个服务端进程在 Binder 驱动中维护一个线程池,实际线程数会根据负载按需增减,上限由内核中的 max_threads 字段和用户态 ProcessState#setThreadPoolMaxThreadCount() 等配置共同决定。
  • 典型上限取决于进程角色:普通应用进程的 Binder 工作线程上限通常在 15 左右(libbinder 默认值);但 system_server 会在启动时显式把上限调高,AOSP 当前代码中设置为 31。因此,system_server 并不等同于“默认十几条线程”。
    某些厂商 ROM 或定制内核会根据自身负载模型,把上限调大或调小(例如调到几十条线程),因此你在不同设备上通过 ps -T system_servertop -H 或 Perfetto 数 Binder: 线程时,看到的具体数字可能会有差异。
  • 以实际观测为准,而不是死记一个数字:在 Perfetto 里,更推荐的做法是直接展开某个进程,看有多少个 Binder:xxx_y 线程轨道,以及它们在抓 Trace 期间的活跃程度,以此来评估线程池的“规模”和“繁忙度”。

Binder 线程数、缓冲区与“Binder 耗尽”

在性能分析中,大家提到“Binder 个数”时,往往会混在一起谈三类不同的资源限制:

Binder 线程池耗尽是指某个进程内所有 Binder 工作线程都处于 Running / D / S 等忙碌状态,没有空闲线程可以被驱动唤醒处理新事务。其现象包括 Client 线程在 thread_state 轨道里长时间停留在 S 状态(调用栈停在 ioctl(BINDER_WRITE_READ) / epoll_wait),并且在 SQL 里可以观察到大量事务的 dispatch_durserver_ts - client_ts)显著偏大——说明请求在服务端真正开始处理之前就已经卡在“等线程/等调度”上了。对于 system_server 这类关键进程,线程池被打满意味着系统服务响应能力下降,很容易放大为全局卡顿或 ANR

Binder 事务缓冲区耗尽涉及每个进程在 Binder 驱动里的一块有限大小的共享缓冲区(典型值约 1MB 量级),用于承载正在传输的 Parcel 数据。典型场景包括一次事务传输过大的对象(如大 Bitmap、超长字符串、大数组等),以及大量并发事务尚未被消费完,导致缓冲区中堆积了太多尚未释放的 Parcel。可能的结果包括内核日志中出现 binder_transaction_alloc_buf 失败、Java 层抛出 TransactionTooLargeException,以及后续事务在驱动层长时间排队甚至失败(看起来像是“Binder 被用光了”)。解决这类问题的思路不是通过“多开线程”,而是控制单次传输的数据量(拆包、分页、流式协议),并对大块数据优先使用 SharedMemory / 文件 / ParcelFileDescriptor 等机制。

Binder 引用表 / 对象数量方面,Binder 驱动会为每个进程维护引用表和节点对象,这些也有上限,但在大多数实际场景中,很少首先撞到这里。常见风险是长时间持有大量 Binder 引用却不释放,更多体现为内存/稳定性问题,而不是 UI 卡顿。

在 Perfetto 里分析时,可以带着一个判断框架:
“现在的慢,是因为线程池被打满,还是事务过大/缓冲区被用光?”
前者主要看 **Binder 线程数与它们的 thread_state**,以及事务的 dispatch_durserver_ts - client_ts,可近似理解为派发/排队延迟);后者则关注 单次事务的大小、并发事务数量和是否伴随 TransactionTooLargeException / binder_transaction_alloc_buf 相关日志


现在回到我们的分析场景:

Binder 线程池的繁忙程度直接决定了服务的并发处理能力。对于同步事务来说,如果服务端 Binder 线程长期处于 RunningUninterruptible Sleep (D) 状态,新的请求就会在内核里排队,客户端线程会长时间阻塞在 ioctl(BINDER_WRITE_READ) / epoll_wait,主线程在 thread_state 上通常表现为长段 S(Sleeping)。

在 Perfetto 中诊断线程池问题,优先看两个信号:Binder 线程是否长期满载,以及事务的 **dispatch_dur 是否显著大于 server_dur**(判读方式与步骤一一致)。

关于 Oneway 调用在 Perfetto 中的识别:同步调用(Two-way)和异步调用(Oneway)在 Perfetto 中的表现有明显区别,学会区分它们对分析很有帮助。同步调用时,客户端会阻塞等待(thread_state 显示 S),Perfetto 通常会画出 transaction → reply 的 Flow;而 Oneway 调用客户端发完就返回、几乎无阻塞,Flow 只有单向的 transaction,没有 reply 回来。另外,Oneway 调用的 Slice 名称后面可能会带 [oneway] 标记;在 SQL 里也可以通过 android_binder_txns.is_sync = 0 来过滤 Oneway。

在分析 Oneway 相关问题时,重点关注两件事:一是服务端的队列深度(如果同一 IBinder 对象上的 Oneway 请求堆积,后续请求的实际执行时机会被不断延后);二是是否存在批量发送的模式(短时间内大量 Oneway 调用会形成”尖峰”,在 Perfetto 中表现为服务端 Binder 线程上密集排列的短 Slice)。

image-20260207130758344

值得一提的是,SystemServer 的 Binder 线程不仅要处理来自各个 App 的请求,还要处理系统内部的调用(比如 AMS 调 WMS、WMS 调 SurfaceFlinger 等)。如果某个”行为不端”的 App 在短时间内疯狂发送 Oneway 请求,可能会把某个系统服务的 Oneway 队列塞满,进而影响到其他 App 的异步回调时延,造成全局性的卡顿感。

步骤三:排查锁竞争

如果你跳转到服务端的 Binder 线程,发现它在处理你的请求期间长时间处于 S(Sleeping)或 D(Disk Sleep / Uninterruptible Sleep)状态,那通常意味着它在等待某个资源——要么是在等锁,要么是在等 IO。锁竞争是 SystemServer 中非常常见的性能瓶颈来源,因为 SystemServer 里运行着大量服务,它们之间共享很多全局状态,而这些状态往往通过 synchronized 锁来保护。

Java 锁(Monitor Contention) 是最常见的情况。SystemServer 中有不少全局锁,比如 WindowManagerService 的 mGlobalLock、ActivityManagerService 的一些内部锁等。当多个线程同时需要访问被这些锁保护的资源时,就会产生竞争。在 Perfetto 中,如果你看到某个 Binder 线程状态为 S,并且 blocked_function 字段包含 futex 相关的符号(如 futex_wait),那基本可以确定是在等 Java 锁。要进一步确认是在等哪个锁、被谁持有,可以查看 Lock contention 轨道。Perfetto 会把锁竞争的关系可视化出来:用连接线标出 Owner(持有锁的线程,比如 android.display 线程)和 Waiter(等待锁的线程,比如处理你请求的 Binder:123_1)。点击 Contention Slice,还可以在 Details 面板里看到锁对象的类名(比如 com.android.server.wm.WindowManagerGlobalLock),这对于理解问题的根源非常有帮助。

image-20260207130953951

Native 锁(Mutex / RwLock) 的情况相对少见一些,但在某些场景下也会遇到。表现形式类似:线程状态为 DS,但调用栈里出现的是 __mutex_lockpthread_mutex_lockrwsem 等 Native 层的符号,而不是 Java 的 futex_wait。分析这类问题通常需要结合 sched_blocked_reason 事件来看线程具体在等什么,属于比较进阶的内容,这里就不展开了。

使用 SQL 统计 system_server 中的 Java Monitor Contention(可选)

PerfettoSQL 标准库已经提供了解析后的 android_monitor_contention 表(由 ART 的 monitor contention 相关 Slice 解析而来),建议优先使用它来做统计,而不是手工解析 slice 名称字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INCLUDE PERFETTO MODULE android.monitor_contention;

SELECT
process_name,
blocked_thread_name AS waiter_thread,
blocking_thread_name AS owner_thread,
(dur / 1e6) AS dur_ms,
(waiter_count + 1) AS waiter_threads,
short_blocked_method,
short_blocking_method,
blocked_src,
blocking_src
FROM android_monitor_contention
WHERE process_name = 'system_server'
ORDER BY dur DESC
LIMIT 50;

提示:如果查不到数据,请确认抓取时 atrace_categories 包含 dalvik,并且问题场景中确实发生了 monitor contention。

image-20260207105941270

最新平台特性与优化建议

随着 Android 版本演进,Binder 在性能与稳定性上也持续增强。理解这些机制有助于解释 Perfetto 现象并指导优化。

Binder Freeze(Android 12+):Cached 进程被冻结后几乎不获得 CPU。对其发起同步 Binder 调用会被拒绝,并可能触发目标进程终止;异步(oneway)事务通常先缓冲,待解冻后处理。

Frozen-callee 回调策略(Android 14+ 常见):可用 RemoteCallbackList 的 policy(DROPENQUEUE_MOST_RECENTENQUEUE_ALL)控制冻结期间回调堆积,降低解冻后的抖动与压力。

Binder Heavy Hitter Watcher:用于识别短时间内占比异常高的 Binder 调用热点。启用方式、阈值和输出渠道依版本与设备配置而定。

给开发者的一些建议

关于 Oneway:只在确实不需要返回值和完成时机时使用(如日志、状态通知)。把同步调用硬改成 Oneway 往往只会把等待转移到服务端队列,并引入时序问题。

关于 大数据传输:避免直接走 Binder(尤其是 Bitmap)。单进程 Binder 缓冲区约 1MB,容易触发 TransactionTooLargeException;应改用 SharedMemory、文件或 ParcelFileDescriptor

关于 主线程调用:不要在 UI 线程调用耗时不可控的 Binder 服务;若必须调用,请放到后台线程,完成后再回主线程更新 UI。

总结

Perfetto 是分析 Binder 问题的高效工具。核心方法是:用 linux.ftrace 抓取 binder/sched/dalvik 信号,在 UI 中沿 Flow 串联 Client 与 Server,再结合 client_dur / server_dur / dispatch_dur、线程状态和锁竞争,区分“排队慢”“处理慢”“等锁”。

遇到难解释的 UI 卡顿或 ANR 时,可按“主线程是否在等 Binder → 服务端是否排队/处理慢/等锁”的顺序排查。再结合 CPU、调度、渲染等信号,通常能更快定位根因。

参考

  1. 理解Android Binder机制1/3:驱动篇
  2. PerfettoSQL stdlib - android.binder
  3. Perfetto Documentation - Ftrace
  4. Android Source - Binder
  5. Android Developers - Parcel and Bundle
  6. binder-trace - Wireshark for Binder
  7. am trace-ipc 源码分析

*

关于我 && 博客

  1. 博主个人介绍
  2. 本博客内容导航
  3. Android性能优化知识星球

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

Android Perfetto 系列 9 - CPU 信息解读

本文是 Perfetto 系列的第九篇文章,主题是 Perfetto 中的 CPU 信息分析。Perfetto 提供了远超 Systrace 的数据可视化与分析能力,理解 CPU 相关信息是定位性能瓶颈、分析功耗问题的基础。

本系列的目标,就是通过 Perfetto 这个工具,从一个全新的图形化视角,来审视 Android 系统的整体运行,同时也提供一个学习 Framework 的新途径。或许你已经读过很多源码分析的文章,但总是对繁杂的调用链感到困惑,或者记不住具体的执行流程。那么通过 Perfetto,将这些流程可视化,你可能会对系统有更深入、更直观的理解。

本文目录

Perfetto 系列文章

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

Perfetto 中的 CPU 信息概览

在 Perfetto UI 中,CPU 相关的信息通常分组置于顶部,是性能分析的起点。主要包含以下三个核心轨道:

  • CPU Scheduling (CPU 调度): 显示在每个时间点,各个 CPU 核心上正在执行的线程。
  • CPU Frequency (CPU 频率): 显示每个 CPU 核心或核心簇的频率变化情况。
  • CPU Idle (CPU 空闲状态): 显示每个 CPU 核心进入的低功耗状态 (C-States)。

image-20251112214415666

通过分析 CPU 相关的信息,可以解答以下关键性能问题, 或者进行竞品分析:

  • 应用主线程为何没有执行?是否被其他线程抢占?
  • 某个任务执行缓慢的原因是什么?是否被调度到了低性能核心?
  • 在特定场景下,CPU 频率是否受限?
  • 应用在后台时,CPU 是否有效进入了深度睡眠状态?

抓取 CPU 信息所需要的 Trace Config

为了采集到本文分析所需的所有 CPU 数据,你需要一个正确的 TraceConfig。不正确的配置会导致某些轨道(如 CPU 频率)或某些内核事件(如唤醒事件)丢失。以下是 Perfetto 官方文档推荐的、用于通用 CPU 分析的配置。你可以将其添加到你的 Perfetto 的 Config 中,抓 Trace 的时候使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_process_exit"
ftrace_events: "sched/sched_process_free"
ftrace_events: "task/task_newtask"
ftrace_events: "task/task_rename"
ftrace_events: "sched/sched_switch"
ftrace_events: "power/suspend_resume"
ftrace_events: "sched/sched_blocked_reason"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_wakeup_new"
ftrace_events: "sched/sched_waking"
ftrace_events: "sched/sched_process_exit"
ftrace_events: "sched/sched_process_free"
ftrace_events: "task/task_newtask"
ftrace_events: "task/task_rename"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
ftrace_events: "power/suspend_resume"
symbolize_ksyms: true
disable_generic_events: true
}
}
}
data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
}
}
}
data_sources {
config {
name: "linux.sys_stats"
sys_stats_config {
cpufreq_period_ms: 250
}
}
}

这个配置启用了包括 sched(调度)、power(频率和空闲)、task(任务生命周期)在内的关键 ftrace 事件,是进行深度 CPU 分析的基础。

CPU 核心架构:big.LITTLE

在深入分析前,必须了解现代手机 SoC 的 CPU 核心架构。目前主流的移动处理器普遍采用 big.LITTLE 异构多核架构,或其变种,如 big.Medium.LITTLE(大中小核)。

  • 小核 (LITTLE cores): 针对低功耗设计,频率较低,用于处理后台任务、轻量级计算,以保证续航。
  • 大核 (big cores): 针对高性能设计,频率更高,功耗也更大,用于处理用户交互、游戏、应用启动等重负载场景。
  • 超大核 (Prime Core): 部分旗舰芯片会有一个频率极高的超大核,用于应对最严苛的单核性能挑战。

在 Perfetto 的 CPU 轨道中,核心通常从 0 开始编号。例如,在一个典型的八核处理器中,CPU 0-3 可能为小核,CPU 4-6 为大核,CPU 7 为超大核。识别核心类型对于性能分析至关重要:一个计算密集型任务如果长时间运行在小核上,其耗时必然远超预期。分析时,需要将线程的运行核心与其任务属性进行匹配,以判断调度器(Scheduler)的行为是否符合预期。

下面是一个典型的 4+4 的 CPU :

image-20251112214626810

下面是一个典型的 4+3+1 的 CPU ( MTK 的天玑 9500、9400 ,以及高通骁龙高端系列):

image-20251112215105550

下面是一个 5+2 的 CPU (高通 8Elite 1 阉割版,8Elite 1 的标准版 0-5 是小核,6-7 是大核,这里就不放图了)

image-20251112214833158

一般通过查阅 CPU 的 spec 就可以知道他的大中小核心的架构,或者 cat 对应的 CPU 节点也可以,这里就不再赘述了。

CPU 调度

CPU Scheduling 轨道是最常用且最重要的部分,它可视化了 Linux 内核调度器的决策过程。其数据来源于内核 ftrace 中的 sched/sched_switch 事件。

CPU 调度区域

每个 CPU 核心对应一行独立的轨迹。轨道上的不同色块,代表了在该时间片上,特定线程正在该 CPU 核心上运行。

  • UI 细节:点击 CPU 切片,详情面板会显示该次调度的 cpuend_statepriority、所属 process/thread 等;向下展开进程还能看到每个线程的独立轨道,便于跟踪单个线程的状态演化(参考 官方文档)。

image-20251112215540626

线程状态深度解析

理解 Linux 的线程状态是进行性能优化的前提。在 Perfetto 中,选中一个线程,下方的 Current State 面板会显示其当前状态。这些状态信息来源于 Perfetto 解析得到的 thread_state/thread_state_slice 表。

Running (绿色)

状态定义:绿色代表线程正在 CPU 上执行代码。这是唯一真正在消耗 CPU 资源进行计算的状态。

image-20251112215705017

分析要点

  • 执行时长:过长的 Running 状态,尤其是在关键线程上,通常意味着密集的计算任务,例如复杂的算法或循环。这会增加任务耗时,并可能阻塞其他线程的执行。
  • 运行核心:结合 CPU 的核心架构(例如 big.LITTLE)进行分析。一个计算密集型任务是否被调度到了预期的性能核心(大核)上,是评估调度策略是否合理的重要依据。
  • 运行频率:线程的实际执行速度也受 CPU 频率影响。即使线程运行在大核上,如果因为温控等原因导致降频,其性能表现也会下降。因此需要结合 CPU Frequency 轨道进行综合分析。

R (Runnable / 可运行)

状态定义:线程已具备所有运行条件,正在等待调度器分配 CPU 核心。在 Perfetto 的线程私有轨道中,Runnable 状态通常以浅绿色或白色条显示。

image-20251112220248837

分析要点

  • Runnable 与卡顿:对于 UI 线程这类对响应时间敏感的线程,长时间处于 Runnable 状态是造成卡顿的直接原因。它意味着线程无法及时获得 CPU 时间来处理任务(如 UI 绘制),从而导致掉帧。

Runnable 的三种类型:
仔细观察会发现,线程进入 Runnable 状态的“前身”各不相同。根据内核的调度时机,我们可以将其分为三种情况:

  1. 从睡眠中唤醒-Wake-up:这是最常见的一种。线程因等待的资源(如锁、I/O、Binder 回复)已经就绪,从 SD 状态被唤醒,进入 Runnable 状态,等待被调度器选中执行。

  2. 用户抢占-User Preemption:指线程的运行时间片用完,或出现更高优先级的任务,导致调度器在从内核态返回用户态时(如系统调用、中断返回后)决定换下当前线程。此时,被换下的线程从 Running 变为 Runnable。在底层的 sched_switch trace 中,它的 prev_state 标记为 R

  3. 内核抢占-Kernel Preemption:指一个更高优先级的任务或中断,在当前线程正在执行内核态代码期间,就“强行”将其打断,使其让出 CPU。这种情况通常意味着一次更紧急的调度。此时,被换下的线程从 Running 变为 Runnable (Preempted)。在底层 trace 中,它的 prev_state 标记为 R+,Perfetto 据此进行了解析和展示。

理解这三种类型的区别,有助于更精细地判断调度延迟的原因。例如,大量的 Runnable (Preempted) 可能暗示着系统中存在频繁的、高优先级的唤醒源,导致关键线程在内核态中被频繁打断,或者当时的 CPU 已经满载了(比较常见的情况),这时候优先级比较低的 Task 就很容易被其他优先级高的 Task 抢占,被迫让出 CPU 。如果你的关键 Task 总是被抢占,那就需要调整优先级。

image-20251112220534899

S (Sleep / 可中断睡眠)

状态定义:线程因等待某个事件而进入睡眠,可以被信号中断。这是最常见的睡眠状态,通常情况下是良性的,因为它在等待期间不消耗 CPU 资源。

image-20251112221122170

分析要点

  • 等待的资源:如果关键线程(如 UI 线程)睡眠时间过长,同样会引发性能问题。常见的等待原因包括:
    • 锁竞争:等待获取一个 mutex (Java 锁或 native futex)。
    • Binder 通信:等待另一个进程通过 Binder 调用返回结果。
    • I/O 操作:等待网络 socket 数据 (epoll_wait)。
    • 显式休眠:代码中调用了 Thread.sleep()Object.wait()
  • 依赖分析:在 Perfetto 中,选中 CPU 区域被唤醒正在 Running 的 Task,就会有 UI 标识他是被哪个 CPU 上的哪个 Task 唤醒的,这有助于快速定位线程间的依赖关系。结合函数调用栈,可以进一步定位导致睡眠的具体代码。
    image-20251112221703122

D (Uninterruptible Sleep / 不可中断睡眠)

状态定义:线程在等待硬件 I/O 操作完成,期间不能被任何信号中断。此状态旨在保护进程与设备交互过程中的数据一致性。在 Perfetto 中,该状态通常显示为橙色或红色,是需要重点关注的信号。

image-20251112220738143

分析要点

  • 严重的性能瓶颈:长时间的 D 状态意味着线程被完全阻塞,无法响应任何事件。如果发生在 UI 线程,极易导致 ANR。
  • 常见原因
    1. 磁盘 I/O:频繁或单次大量的文件读写操作。
    2. 内存压力:系统物理内存不足,导致频繁的页面换入/换出 (swap),其本质是高频的磁盘 I/O。
    3. 内核驱动问题:部分内核驱动的实现缺陷也可能导致线程陷入 D 状态。
  • 排查方向:在 Current State 面板中,如果 D 状态伴有 (iowait) 标记,则明确表示在等待 I/O。需要检查应用的 I/O 模式,评估其合理性,例如是否将耗时 I/O 操作置于主线程,或是否存在可优化的空间(如减少 I/O 次数和数据量)。

唤醒关系分析

线程间的依赖关系是性能分析中的一个难点。一个线程长时间 Sleep,关键在于找出它在“等谁”。Perfetto 提供了强大的唤醒关系可视化功能。

  • UI 操作:在 Perfetto 的 CPU 区域中,用鼠标左键单击选中一个处于 Running 状态的线程 Task 。Perfetto 会自动绘制一条从“唤醒者”到“被唤醒者”的依赖箭头,并高亮显示唤醒源所在的线程切片。
    image-20251112221703122
  • 底层原理:该功能依赖于内核的 sched_wakeup ftrace 事件。当线程 T1 释放了某个资源(如解锁、完成 Binder 调用),而线程 T2 正在等待该资源时,内核会将 T2 标记为 Runnable 状态,并记录下 T1 -> T2 的这次唤醒事件。Perfetto 解析这些事件,从而构建出线程间的依赖链。

通过唤醒分析,可以清晰地追踪复杂的调用链,例如:UI 线程等待一个 Binder 调用 -> Binder 线程执行任务 -> Binder 线程等待另一个锁 -> 持锁线程释放锁并唤醒 Binder 线程 -> Binder 线程完成任务并唤醒 UI 线程。整个过程中的瓶颈点将一目了然。

调度唤醒与延迟分析

当线程 A 在 wait() 上挂起时,它进入 S(Sleeping)并从 CPU 运行队列移除。线程 B 调用 notify() 后,内核会把线程 A 转换为 R(Runnable)。此时线程 A“有资格”被放回某个 CPU 的运行队列,但这并不意味着“立刻”运行。

image-20251112222240991

常见等待原因包括:

  • 所有 CPU 都在忙:线程 A 需要等待空出队列槽位(或当前运行的线程优先级更高)。
  • 存在空闲 CPU,但迁移需时:负载均衡器将线程迁移到其他 CPU 需要一定时间窗口。

除非使用实时优先级,大多数 Linux 调度器配置并非严格的“work‑conserving”。调度器有时会“等待”当前 CPU 线程自然空闲,以避免跨 CPU 迁移带来的额外开销与功耗。这会在 R 状态下形成可观测的“排队延迟”。结合 sched_waking/sched_wakeup_new 可更精确地刻画“被唤醒”到“真正入队/运行”之间的时间段(参考官方文档:sched_waking / sched_wakeup 的差异与适用场景)。

  • 在目标线程的 thread_state 轨道中筛选 state=R 的切片,用作“调度延迟”的直接证据。
  • 同步对照同一 CPU 的其它重负载线程与 IRQ/SoftIRQ 轨迹,验证是否存在时间重叠的抢占或高优先级压制。
  • 若频繁在 R 状态排队并以 end_state=R+ 收尾,视为非自愿抢占严重,需评估优先级、放置与负载均衡策略。

sched_waking 与 sched_wakeup 的差异与非严格 work-conserving

  • sched_waking 在线程被标记为可运行(R)时就会发出,sched_wakeup 与跨 CPU 唤醒有关,可能记录在源或目的 CPU 上;对大多数延迟分析而言,仅 sched_waking 已足够(参见官方说明)。
  • 多数 Linux 调度配置在通用优先级下并非严格“work-conserving”。调度器有时会“等待当前 CPU 空闲”以避免跨核迁移带来的额外开销与功耗,这会造成 R 状态下的等待时间(排队延迟)并非异常,而是权衡后的结果(参见 Perfetto CPU Scheduling)。

用户态与内核态:sys_* 切片定位空火焰图

  • Running 的绿色切片未必都是应用代码在忙。若线程陷入单个长系统调用如 sys_readsys_futex,用户态采样火焰图可能几乎为空。
  • 若 UI 线程某段 sched_slice 很长,而 CPU 火焰图热点很少或几乎没有:
    1. 打开该线程的 slice 视图,查找是否存在长时间 sys_* 切片;
    2. 若存在:瓶颈多在 I/O 或同步原语,优先检查 I/O 路径、锁粒度与访问模式;
    3. 若不存在:回到火焰图,继续剖析用户态热点函数。
  • 建议:合并 I/O、切换异步、优化锁竞争、降低系统调用频率与单次数据量。

CPU 时间与墙上时间

image-20251112223105996

  • Wall 是切片从开始到结束的真实世界时间,CPU 是真正在 CPU 上运行的时间。
  • 二者关系:Wall = CPU + Runnable + Sleep
    1. 选中目标切片(如 Choreographer#doFrame),比较 WallCPU
    2. Wall ≈ CPU,则为计算过重,使用火焰图定位热点;\n 3) 若 Wall >> CPU,则为调度或依赖等待,查看 thread_stateR/S/D 分布与唤醒链。

CPU Frequency (CPU 频率) 深度解析

CPU 频率直接影响代码执行速度,同时也与功耗正相关。CPU Frequency 轨道显示了每个核心在特定时间的运行频率(scaling_cur_freq)。但更重要的是理解其背后的限制因素:

影响 CPU 频率的核心因素

  1. 任务负载 (Task Utilization): 这是最主要的驱动因素。现代 Android 系统多采用 schedutil 作为 cpufreq 调频策略。它直接关联调度器(Scheduler),根据线程的“繁忙”程度(utilization)来决定频率。一个高负载的线程(util 很高)会促使 schedutil 请求更高的频率,反之亦然。

  2. 场景策略 (Power HAL): Android Framework 通过 Power HAL 向底层传递当前的系统状态,即“场景”。例如,在应用启动、游戏、或触摸屏幕时,Power HAL 会向内核请求更高的性能,这通常通过抬高 CPU 的地板频 (Floor / scaling_min_freq) 和/或 天花板频 (Ceiling / scaling_max_freq) 来实现,确保 CPU 能快速响应。

  3. 温度控制 (Thermal Throttling): 这是拥有最高优先级的限制。当设备温度(来自电池、CPU、NPU 等传感器)超过预设阈值时,温控系统会强制降低 CPU 的天花板频,以减少发热,保护硬件。此时,即使有高负载任务,CPU 频率也无法提升,是导致游戏掉帧、应用卡顿的常见外部原因。

  4. 功耗限制与省电模式: 在低电量或开启省电模式时,系统同样会通过降低天花板频、限制最高性能输出来延长续航。

因此,当发现一个重任务在运行时 CPU 频率上不去,不仅要看当前频率,更要关注 scaling_max_freq 是否被限制了。这通常意味着性能瓶颈的根源不在于应用代码本身,而在于系统的温控或功耗策略。

image-20251112223237018

上图是 CPU 频率区域的标识图,鼠标放上去就可以看到当前的频率,CPU 频率变化很快。图中 CPU 有带颜色和不带颜色的部分,带颜色标识当前 CPU 有 Task 在跑,不带颜色说明当前 CPU 是空的,无 Task。

频率数据采集与平台差异

  • 两种获取方式:
    • 事件驱动:启用 power/cpu_frequency,在内核 cpufreq 驱动变更频率时记录事件。并非所有平台支持,在多数 ARM SoC 上可靠,在大量现代 Intel 平台上常无数据。
    • 轮询采样:启用 linux.sys_stats 并设置 cpufreq_period_ms > 0,定期读取 /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq,ARM/Intel 均可用。建议与事件驱动组合使用以补齐“初始频率快照”。
  • Android 设备常以“簇”为单位调频,常见现象是同簇内多个 CPU 同步变频。
  • 已知问题:
    • 事件仅在频率变化时产生,短 Trace 或稳定场景可能出现左侧“空白”。此时需依赖轮询补足。
    • 某些 UI 版本在未捕获 Idle 状态时不渲染 cpufreq 轨道,但数据仍可通过查询得到。
    • 参考:CPU frequency and idle states 官方说明。

big.LITTLE:相同频率不等于相同性能或能耗

  • 在异构 CPU 上,同为 2.0GHz,小核与大核的实际算力和能耗并不等价。频率必须结合核心类型理解。
  • 核心容量(capacity)与 IPC:大核通常具备更宽的乱序执行、更多执行端口、更大的缓存与更激进的预取/分支预测,单位周期完成的指令数(IPC)更高;同频下,大核完成同样工作所需时间更短、能量更少。
  • 簇级 DVFS(cluster-level DVFS):移动 SoC 多以“簇”为单位调频。小核簇的高频并不等同于大核簇的中频性能输出;不同簇的电压-频率-能量曲线不同,同一频点无法横向类比“每瓦性能”。
  • 内存/缓存/互联瓶颈:热点若受制于内存带宽、LLC 命中率或系统互联(NoC),单纯提升小核频率收益有限;大核更大的缓存/更强预取可显著降低同任务的访存等待,体现“同频不同效”。
  • 能效曲线非线性:小核在接近最高频时常进入能效陡降区(电压抬升使边际能耗激增),而大核在中等频点可能达到“更高单位能效”。同频比较忽略了“电压-频率-能量”的三维权衡。

P-States 与 governor

  • CPU 频率并非连续值,而是离散的性能状态 P-States。常用 governor 为 schedutil,依据任务利用率等信号选择合适 P-State,并联动电压调节。
  • UI 上的频率变化,实质是 governor 在不同 P-State 之间切换。高频不必然更快或更省电,需要结合核簇类型与当时的上限/下限约束理解。
  • 核验步骤:
    1. CPU Frequency 观察频率的上下限是否被拉高/压低(如场景策略/温控限制 scaling_max_freq)。
    2. CPU Scheduling 查看关键线程运行在哪类核心(结合设备的核编号划分)。
    3. 若“高频 + 小核 + 仍慢”,优先考虑选核/访存瓶颈,而非简单“频率不够”。
  • 建议结合 Power HAL 场景、热管理与任务画像(CPU 密集或访存密集)综合优化,避免单纯以频率作为调参目标。参考官方文档: Perfetto CPU Scheduling

Linux 内核调度策略:选核与迁移

理解了线程状态和频率后,我们还需要深入了解 Linux 内核调度器(在 Android 中主要是 EAS - Energy Aware Scheduling)的两个核心行为:如何为任务挑选一个 CPU 核心,以及为何要将任务从一个核心迁移到另一个。

选核逻辑 (Task Placement)

当一个线程从 Sleep 状态被唤醒,或一个新线程被创建时,EAS 调度器需要为它选择一个最合适的 CPU 核心。其核心目标是在满足任务性能需求的前提下,尽可能地降低系统功耗。决策流程大致如下:

  1. 评估任务负载 (Task Utilization): 调度器会评估该线程的 util,即它需要多少计算资源。这是一个动态调整的值,反映了线程历史上的繁忙程度。
  2. 寻找“足够”的核心: 调度器会遍历所有可用的 CPU 核心,比较任务的 util 和每个核心的 capacity(容量/最大计算能力)。大核的 capacity 远高于小核。调度器会寻找 capacity > util 的核心,即能“装下”这个任务的核心。
  3. 寻找“最节能”的核心: 在所有满足 capacity 要求的核心中,调度器会利用预置在内核中的“能量模型”(Energy Model)进行计算。这个模型知道每个核心在每个频率下运行的功耗。调度器会选择一个能让整个系统(包括任务本身和其他正在运行的任务)总功耗最低的 CPU 核心作为最终选择。

简而言之:EAS 的目标不是为任务找一个最快的核,而是找一个“刚刚好够用”且“最省电”的核。

核心迁移逻辑 (Task Migration)

将任务从一个 CPU 核心移动到另一个,是调度器进行动态调优的关键手段。主要发生在以下情况:

  1. 负载均衡 (Load Balancing): 这是最常见的迁移原因。调度器会周期性地检查系统是否处于“负载不均衡”状态。例如,CPU-1(小核)上挤满了高负载任务导致其利用率饱和,而 CPU-7(大核)却很空闲。此时,调度器会判定系统失衡,并将 CPU-1 上的某个高负载任务“拉到”(pull)CPU-7 上运行,以恢复平衡,提升性能。

  2. 唤醒时迁移 (Wake-up Migration): 当一个线程睡醒时,调度器会重新评估它的最佳核心。如果这个线程在睡眠期间 util 发生了变化(例如,一个后台下载线程突然收到了一个大文件下载任务,util 飙升),或者原先运行的核心现在非常繁忙,调度器就可能在唤醒时直接为它选择一个更合适的新核心,而不是让它在原来的地方排队。

理解选核与迁移逻辑,有助于我们判断调度器的行为是否“异常”。例如,一个明显的前台 UI 线程被不合理地长时间限制在小核上,或者在大小核之间过于频繁地“反复横跳”,都可能暗示着系统调度策略或任务优先级设置存在问题。

当然目前 Android 厂商针对调度器有做非常多的客制化,关键 Task(Critical Task)通常会占用更多的 CPU ,甚至更容易上大核。另外还有很多绑核策略、签核策略,导致大家看到的每个厂商的现象都不一样。这也是各个厂商的核心竞争力(比如 Oppo 的蜂鸟引擎)。

实战与 SQL

使用 SQL 进行量化分析

Perfetto 内置的 SQL 查询引擎是其强大功能之一,允许开发者对 Trace 数据进行精确的聚合、筛选和分析。以下是一些常用的 CPU 分析查询。

1. 计算各进程的 CPU 总时长

此查询统计每个进程在所有 CPU 上运行的总时长,按降序排列,用于快速定位消耗 CPU 资源最多的进程。

1
2
3
4
5
6
7
8
SELECT
process.name,
sum(dur) / 1e9 AS total_cpu_time_s
FROM sched
JOIN thread ON sched.utid = thread.utid
JOIN process ON thread.upid = process.upid
GROUP BY process.name
ORDER BY total_cpu_time_s DESC;

2. 分析单个线程的时间状态分布

此查询基于 thread_state 表,可用于分析特定线程(示例为 surfaceflinger)在各种状态下的时间分布,从而判断其主要瓶颈。

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT
CASE state
WHEN 'Running' THEN 'Running'
WHEN 'R' THEN 'Runnable'
WHEN 'S' THEN 'Interruptible Sleep'
WHEN 'D' THEN 'Uninterruptible Sleep'
ELSE state
END AS human_state,
sum(dur) / 1e6 AS total_time_ms
FROM thread_state
WHERE utid = (SELECT utid FROM thread WHERE name = 'surfaceflinger' LIMIT 1)
GROUP BY human_state
ORDER BY total_time_ms DESC;

3. 查找特定时间段内 CPU 消耗最高的线程

此查询用于分析特定场景(如应用启动的 2-5s 时间段)中,消耗 CPU 时间最长的线程。

1
2
3
4
5
6
7
8
9
SELECT
thread.name,
sum(dur) / 1e9 AS cpu_time_s
FROM sched
JOIN thread ON sched.utid = thread.utid
-- 时间戳单位为纳秒,可以自己加上 WHERE ts > 2e9 AND ts < 5e9 来取某一段时间
GROUP BY thread.name
ORDER BY cpu_time_s DESC
LIMIT 20;

4. 查看线程在各 CPU 核心上的运行时间分布

此查询有助于了解一个线程的 CPU 亲和性,以及它是否在预期的大小核上运行。

1
2
3
4
5
6
7
SELECT
cpu,
sum(dur) / 1e6 AS time_on_cpu_ms
FROM sched
WHERE utid = (SELECT utid FROM thread WHERE name = 'system_server' LIMIT 1)
GROUP BY cpu
ORDER BY cpu;

5. 计算各进程的 CPU 利用率

此查询计算了每个进程在整个 Trace 时间范围内的 CPU 利用率。

1
2
3
4
5
6
7
8
SELECT
process.name AS process_name,
100 * sum(dur) / CAST(TRACE_END() - TRACE_START() AS REAL) AS cpu_utilization_percent
FROM sched
JOIN thread ON thread.utid = sched.utid
JOIN process ON process.upid = thread.upid
GROUP BY process.name
ORDER BY cpu_utilization_percent DESC;

总结

对 Perfetto 中 CPU 信息的熟练分析,是 Android 性能优化的关键技能。通过深度理解 核心架构线程状态唤醒关系频率限制C-State,并结合强大的 SQL 查询进行量化分析,开发者可以精准地定位并解决各类性能与功耗问题。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析

本篇是 Perfetto 系列文章的第八篇,主要深入介绍 Android 中的 Vsync 机制及其在 Perfetto 中的表现形式。文章将从 Perfetto 的角度来分析 Android 系统如何基于 Vsync 信号进行帧渲染和合成,涵盖 Vsync、Vsync-app、Vsync-sf、VsyncWorkDuration 等核心概念。

随着高刷新率屏幕的普及,理解 Vsync 机制变得更加重要。本文将以 120Hz 刷新率为主要叙事线,帮助开发者理解现代 Android 设备中 Vsync 的工作原理,以及如何在 Perfetto 中观察和分析 Vsync 相关的性能问题。

注:本文内容基于 Android 13~16 的公开实现与演进;文中代码以 AOSP main 的“签名对齐精简摘录”为主,少量位置使用 ... 省略非主线逻辑,请以当前分支源码为准。

本文目录

系列文章目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

什么是 Vsync

Vsync(Vertical Synchronization,垂直同步)是 Android 图形系统的核心机制,它的存在是为了解决一个根本性的问题:如何让软件的渲染节奏与硬件的显示节奏保持同步。

在没有 Vsync 机制之前,常见问题是屏幕撕裂(Screen Tearing)。当显示器读取 framebuffer 的同时,GPU 写入了下一帧,就会在同一次刷新中出现上下两部分不一致的画面。

Vsync 解决什么问题?

Vsync 机制的核心思想非常简单:让所有的渲染工作都按照显示器的刷新节拍来进行。具体来说:

  1. 同步信号:显示器每次开始新的刷新周期时,都会发出一个 Vsync 信号。
  2. 帧节拍与生产:应用侧在 Vsync 到来时由 Choreographer 驱动开始一帧的生产(Input/Animation/Traversal);CPU 提交渲染命令后,GPU 异步流水执行。SurfaceFlinger 侧在 Vsync 到来时进行 Buffer 的合成操作。
  3. 缓冲机制:使用双缓冲或三缓冲技术,确保显示器总是读取完整的帧数据。

这样,帧的生产与显示以 Vsync 为节拍对齐。以 120Hz 为例,每 8.333ms 会有一个显示机会;应用需要在该窗口前把可合成的 Buffer 提交给 SurfaceFlinger。关键约束是 queueBuffer/acquire_fence/present_fence 的时序;若未赶上本周期,会顺延到下一个周期显示。

Android 中 Vsync 的基本工作原理

Android 系统的 Vsync 实现比基本概念复杂得多,需要考虑多个不同的渲染组件,以及它们之间的协调工作。

Vsync 信号的分层架构

在 Android 系统中,并不是只有一个简单的 Vsync 信号。实际上,系统维护着多个不同用途的 Vsync 信号:

硬件 Vsync(HW Vsync)
这是最底层的 Vsync 信号,由显示硬件(HWC,Hardware Composer)产生。它的频率严格对应显示器的刷新率,比如 60Hz 的显示器会每 16.67ms 产生一次 HW Vsync,120Hz 的显示器会每 8.333ms 产生一次。(硬件 Vsync 回调由 HWC/SurfaceFlinger 管理,详见 frameworks/native/services/surfaceflinger 相关实现)

但是,HW Vsync 并不是一直开启的。由于频繁的硬件中断会消耗较多的电量,Android 系统采用了一种智能的策略:只有在需要精确同步的时候才开启 HW Vsync,大部分时间使用软件预测的方式生成 Vsync 信号。

Vsync-app(应用 Vsync)
这是专门用于驱动应用层渲染的 Vsync 信号。当应用需要进行 UI 更新时(比如用户触摸、动画运行、界面滚动等),应用会向系统申请接收 Vsync-app 信号。

1
2
3
4
5
6
7
8
9
10
// frameworks/base/core/java/android/view/Choreographer.java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
// 向系统申请下一个 Vsync 信号
mDisplayEventReceiver.scheduleVsync();
}
}
}

Vsync-app 是按需申请的。如果应用界面是静态的,没有任何动画或用户交互,那么应用不会申请 Vsync-app 信号,系统也就不会为这个应用生成 Vsync 事件。

Vsync-sf(SurfaceFlinger Vsync)
这是专门用于驱动 SurfaceFlinger 进行图层合成的 Vsync 信号。SurfaceFlinger 是 Android 系统中负责将所有应用的图层合成为最终画面的服务。

Vsync-appSf(应用-SurfaceFlinger Vsync)
Android 13 引入的新信号类型。为消除旧设计中 sf EventThread 既唤醒 SurfaceFlinger 又服务部分 Choreographer 客户端带来的时序歧义,系统将两类职责分离:vsync-sf 专注唤醒 SurfaceFlinger,vsync-appSf 面向需要与 SurfaceFlinger 同步的客户端。

在 Perfetto 中观察 Vsync

Perfetto trace 中包含多个与 Vsync 相关的 Track,理解这些 Track 的含义有助于分析性能问题。

在 SurfaceFlinger 进程中

  1. vsync-app
    显示应用 Vsync 信号状态,数值在 0 和 1 之间变化。每次数值变化代表一个 Vsync 信号。
    image-20250811221826847

  2. **vsync-sf **
    显示 SurfaceFlinger Vsync 信号状态。无 Vsync Offset 时与 vsync-app 同步变化。
    image-20250811221902646

  3. vsync-appSf
    Android 13+ 新增,服务于需要与 SurfaceFlinger 同步的特殊 Choreographer 客户端。
    image-20250811222036489

  4. HW_VSYNC
    显示硬件 Vsync 开启状态。值为 1 表示开启,值为 0 表示关闭。为节省电量,硬件 Vsync 仅在需要精确同步时开启。
    image-20250811222159253

在应用进程中

FrameDisplayEventReceiver.onVsync Slice Track:
显示应用接收 Vsync 信号的时间点。该事件连接通过 Binder 建链、通过 BitTube/Looper 通道分发事件,时间可能略晚于 SurfaceFlinger 中的 vsync-app

image-20250918220632473

UI Thread Slice Track:
包含 Choreographer#doFrame 及相关的 Input、Animation、Traversal 等 Slice。每个 doFrame 对应一帧的处理工作。

image-20250918220709655

RenderThread Slice Track:
包含 DrawFramesyncAndDrawFramequeueBuffer 等 Slice,对应渲染线程工作。

image-20250918220730872

Android App 每一帧是如何基于 Vsync 工作的

Android 应用的每一帧基于 Vsync 机制完成从渲染到显示的完整过程涉及多个关键步骤。

image-20250918221821265

流程总览(按顺序)

  1. 触发重绘/输入:View.invalidate()、动画、数据变化或输入事件触发 → ViewRootImpl.scheduleTraversals()Choreographer.postCallback(TRAVERSAL)
  2. 申请 Vsync:Choreographer 通过 DisplayEventReceiver.scheduleVsync() 申请下一次 Vsync(app 相位)
  3. 接收 Vsync:DisplayEventReceiver.onVsync() 收到 Vsync 后,向主线程消息队列投递异步消息
  4. 主线程帧处理:Choreographer.doFrame() 按顺序执行五类回调:INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT
  5. 渲染提交:RenderThread 执行 syncAndDrawFrame/DrawFrame,CPU 记录 GPU 命令,queueBuffer 提交到 BufferQueue
  6. 合成显示:SurfaceFlingervsync-sf 到来时合成(GPU/或HWC),生成 present_fence,输出到显示
  7. 帧完成度量:通过 FrameTimeline(PresentType/JankType)与 acquire/present_fence 判定是否按期显示

下面分别展开每一步的关键实现与 Perfetto 观测点。

App 什么时候会申请 Vsync 信号

应用并不是时刻都在申请 Vsync 信号的。Vsync 信号是按需申请的,只有在以下情况下,应用才会向系统申请下一个 Vsync:

触发申请 Vsync 的场景

  1. UI 更新需求:当 View 调用 invalidate()
  2. 动画执行:ValueAnimator、ObjectAnimator 等动画开始时
  3. 用户交互:触摸事件、按键事件等需要 UI 响应时
  4. 数据变化:RecyclerView 数据更新、TextView 文本改变等

App 申请 Vsync 的完整流程

当应用需要更新 UI 时,会通过以下流程申请 Vsync 信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 1. UI 组件请求重绘
// frameworks/base/core/java/android/view/View.java
public void invalidate() {
// 标记为需要重绘,但不立即执行
mPrivateFlags |= PFLAG_DIRTY;

if (mParent != null && mAttachInfo != null) {
// 向父容器请求重绘
mParent.invalidateChild(this, null);
}
}

// 2. ViewRootImpl 调度遍历
// frameworks/base/core/java/android/view/ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 关键:向 Choreographer 注册回调,等待下一个 Vsync
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

// 3. Choreographer 申请 Vsync
// frameworks/base/core/java/android/view/Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

// 重点:不只 TRAVERSAL,任意“到期回调”都可能触发下一帧调度
if (dueTime <= now) {
scheduleFrameLocked(now);
}
}

private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}

TRAVERSAL 仍然是最常见触发源,但从 AOSP main 实现看,并非“只有 TRAVERSAL 才申请 Vsync”。

主线程如何监听 Vsync 信号

应用主线程通过 DisplayEventReceiver 来监听 Vsync 信号。这个过程涉及几个关键步骤:

1. 建立连接

1
2
3
4
5
6
7
8
9
// frameworks/base/core/java/android/view/Choreographer.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {

public FrameDisplayEventReceiver(Looper looper, int vsyncSource, long layerHandle) {
super(looper, vsyncSource, /* eventRegistration */ 0, layerHandle);
// 在构造时建立与 SurfaceFlinger 的连接
}
}

2. 接收 Vsync 信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
// 接收到 Vsync 信号,但注意:这里并不直接执行 doFrame
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData.copyFrom(vsyncEventData);

// 关键:将工作 post 到主线程的 MessageQueue 中
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true); // 设为异步消息,优先处理
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
// 这里才真正开始执行一帧的工作
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}

几个遗留问题

Q1:为什么不在 onVsync() 中直接执行 doFrame()
image-20250918221936675

  • 线程边界:在 Choreographer 场景下,onVsync() 回调运行在其绑定的 Looper(通常就是主线程);通过消息队列再进入 doFrame(),可统一调度并保持帧处理时序一致
  • 调度控制:通过 sendMessageAtTime() 精确对齐执行时刻
  • 队列语义:进入主线程 MessageQueue,确保与其他高优先级任务协同

Q2:Vsync 消息来了但主线程在忙,会丢吗?
image-20250918222045731

  • 不完全是“不会丢”。单次 scheduleVsync() 只请求一次事件;主线程长期繁忙时会出现“跳过多个硬件节拍、最终只处理较新的一帧”的现象。实际分析应结合 FrameTimeline 判断是否产生可见卡顿。
  • AOSP DisplayEventDispatcher::processPendingEvents 明确会用“后到达的 vsync 覆盖先到达的 vsync”(只保留最近一次用于分发)。

Q3:CPU/GPU 是否必须在单个 Vsync 周期内完成?如果任何一个环节超过 1 个 vsync ,都会导致掉帧?

  • 现代 Android 系统采用多缓冲(通常是三缓冲)机制:

    • 应用端:Front Buffer(显示中)+ Back Buffer(渲染中)+ 可能的第三个 Buffer

    • SurfaceFlinger 端:也有类似的缓冲机制

    • 这意味着即使应用的某一帧超过了 Vsync 周期,也不一定会立即掉帧。

  • GPU 异步流水;关键是 queueBuffer 是否赶上 SF 合成窗口,多缓冲可掩盖单帧延迟但可能引入额外时延,可以看到下图里面,App 端的 BufferQueue 和 SurfaceFlinger 端的 Buffer 都是充足的,且有冗余,所以没有掉帧。

  • 但是如果 App 在之前没有堆积 Buffer ,则还是会出现掉帧。

image-20250918222258536

Q5:GPU 和 CPU 是怎么协同的?

  • GPU 渲染是异步的,这带来了额外的复杂性:

    • CPU 工作正常,GPU 成为瓶颈:即使应用主线程在 Vsync 周期内完成工作,GPU 渲染耗时过长仍会导致掉帧
    • GPU Fence 机制:在 Buffer 被 SF latch 的阶段,关键同步点通常是 acquire fence(Buffer 何时可安全读取);present fence 更偏向“该帧何时真正送显”的完成信号。根据系统 Latch Unsignaled Buffers 策略,SurfaceFlinger 在特定条件下可先推进流程,再在真正需要时等待 fence 信号,以此隐藏部分延迟。

    image-20250918222626100

Q6:Vsync Phase(相位差)的真正作用是什么?

  • 提升跟手性:通过调整 sf vsync 的相位差,可以让应用从开始绘制到显示在屏幕上的时间从 3 个 Vsync 周期缩短到 2 个 Vsync 周期。这对于触摸响应等交互场景非常重要。
  • 解决应用绘制超时问题:当应用绘制超时时,合理的 sf 相位差可以为应用争取更多的处理时间,避免因为时序不当导致的掉帧。
  • VsyncWorkDuration 更接近调度预算(workDuration/readyDuration)的可视化,不等价于单一 appOffset 数值;分析时建议结合 vsync-app/sf 与 FrameTimeline 联动判断。
  • 下图中显示的时间段就是我手上的手机配置的 app offset (13.3ms)

image-20250918222707300

Vsync Offset / WorkDuration 的技术实现

在当前 AOSP main 中,配置入口是 VsyncConfiguration 抽象接口,返回的是按场景组织的 VsyncConfigSet。实现上 PhaseOffsets 属于旧路径,WorkDuration 是新路径中更常见的实现之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// frameworks/native/services/surfaceflinger/Scheduler/VsyncConfiguration.h
class VsyncConfiguration {
public:
virtual ~VsyncConfiguration() = default;
virtual VsyncConfigSet getCurrentConfigs() const = 0;
virtual VsyncConfigSet getConfigsForRefreshRate(Fps fps) const = 0;
virtual void setRefreshRateFps(Fps fps) = 0;
virtual void reset() = 0;
};

class WorkDuration : public VsyncConfiguration {
public:
explicit WorkDuration(Fps currentRefreshRate);
// 内部根据 sf/app 的 workDuration 构造不同场景下的 offset 配置
};

关键概念

  • workDuration/readyDuration:调度时的“工作预算”和“就绪提前量”,用于计算回调唤醒时刻
  • app/sf offset:仍可作为常用分析口径,但本质是配置集合与调度模型共同作用的结果
  • 常用口径里“app/sf offset 差值”指两者相位差(通常看 |sfOffset - appOffset| 的绝对值,具体符号以设备实现与统计口径为准)

实际的优化效果

以 120Hz 设备为例,配置 3ms Offset 的效果:

无 Offset(传统方式)

  • T0:应用和 SurfaceFlinger 同时接收 Vsync
  • T0+3ms:应用完成渲染
  • T0+8.333ms:下一个 Vsync,SurfaceFlinger 开始合成
  • T0+16.666ms:用户看到画面(总延迟 16.666ms)

有 Offset(优化方式)

  • T0+1ms:应用接收 Vsync-app,开始渲染
  • T0+3ms:应用完成渲染,提交 Buffer
  • T0+4ms:SurfaceFlinger 接收 Vsync-sf,立即开始合成
  • T0+6ms:SurfaceFlinger 完成合成
  • T0+8.333ms:用户看到画面(总延迟 8.333ms)

通过合理配置 Offset,可以将延迟从 16.666ms 减少到 8.333ms,提升一倍的响应性能。

实际的时间预算分配

以 120Hz 设备为例(8.333ms 周期):

  • 理想情况:应用 4ms + SurfaceFlinger 2ms + 缓冲 2.333ms
  • 但实际可以接受:应用 6ms + SurfaceFlinger 3ms(如果有足够的 Buffer 缓冲)
  • GPU 限制:在低端设备上,GPU 渲染可能需要 10-15ms,成为真正的瓶颈

掉帧的真正原因

  1. 应用端超时 + Buffer 耗尽:连续多帧超时导致 BufferQueue 没有可用 Buffer
  2. GPU 渲染超时:即使 CPU 工作正常,GPU 渲染超时也会掉帧
  3. SurfaceFlinger 超时:系统级合成超时,影响所有应用
  4. 系统资源竞争:CPU/GPU/内存等资源被其他进程占用

Vsync 信号的完整代码流程

Vsync 信号从硬件传递到应用层的完整链路如下。

按 AOSP main 分支对齐的关键代码(精简摘录)

下面片段都按当前 AOSP main 分支的方法签名整理,省略了与主线无关的分支与日志代码。

1)Choreographer 申请下一次 Vsync(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// frameworks/base/core/java/android/view/Choreographer.java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
}

private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

2)Choreographer 接收 Vsync(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// frameworks/base/core/java/android/view/Choreographer.java
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC
// for the internal display and scheduleVsync only allows requesting internal VSYNC.
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData.copyFrom(vsyncEventData);
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}

3)JNI 层桥接:DisplayEventDispatcher(C++)

1
2
3
4
5
6
7
8
9
10
11
12
// frameworks/base/core/jni/android_view_DisplayEventReceiver.cpp
class NativeDisplayEventReceiver : public DisplayEventDispatcher {
public:
NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, jobject vsyncEventDataWeak,
const sp<MessageQueue>& messageQueue, jint vsyncSource,
jint eventRegistration, jlong layerHandle);

private:
void dispatchVsync(nsecs_t timestamp, PhysicalDisplayId displayId, uint32_t count,
VsyncEventData vsyncEventData) override;
void dispatchHotplug(nsecs_t timestamp, PhysicalDisplayId displayId, bool connected) override;
};

4)Native 收发通道:DisplayEventReceiver + BitTube(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// frameworks/native/libs/gui/DisplayEventReceiver.cpp
DisplayEventReceiver::DisplayEventReceiver(gui::ISurfaceComposer::VsyncSource vsyncSource,
EventRegistrationFlags eventRegistration, const sp<IBinder>& layerHandle) {
sf->createDisplayEventConnection(vsyncSource, ..., layerHandle, &mEventConnection);
mDataChannel = std::make_unique<gui::BitTube>();
mEventConnection->stealReceiveChannel(mDataChannel.get());
}

status_t DisplayEventReceiver::requestNextVsync() {
mEventConnection->requestNextVsync();
return NO_ERROR;
}

ssize_t DisplayEventReceiver::getEvents(Event* events, size_t count) {
return gui::BitTube::recvObjects(mDataChannel.get(), events, count);
}

5)SurfaceFlinger 调度与分发(C++)

1
2
3
// frameworks/native/services/surfaceflinger/Scheduler/VSyncDispatch.h
virtual std::optional<ScheduleResult> schedule(
CallbackToken token, ScheduleTiming scheduleTiming) = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// frameworks/native/services/surfaceflinger/Scheduler/EventThread.cpp
void EventThread::onVsync(nsecs_t vsyncTime, nsecs_t wakeupTime, nsecs_t readyTime) {
mPendingEvents.push_back(makeVSync(mVsyncSchedule->getPhysicalDisplayId(), wakeupTime,
++mVSyncState->count, vsyncTime, readyTime));
}

void EventThread::threadMain(std::unique_lock<std::mutex>& lock) {
...
const auto scheduleResult = mVsyncRegistration.schedule({
.workDuration = mWorkDuration.get().count(),
.readyDuration = mReadyDuration.count(),
.lastVsync = mLastVsyncCallbackTime.ns(),
.committedVsyncOpt = mLastCommittedVsyncTime.ns()});
LOG_ALWAYS_FATAL_IF(!scheduleResult, "Error scheduling callback");
...
}

关键时序点分析

通过上述代码流程,我们可以看到完整的时序链路:

  1. HWC 产生硬件 Vsync → SurfaceFlinger Scheduler 获取硬件节拍
  2. Scheduler 计算唤醒窗口VSyncDispatch::schedule(...)
  3. EventThread 生成/派发事件 → 写入 DisplayEventReceiver::Event(通过 BitTube
  4. App 侧 Native 收到事件DisplayEventDispatcher::dispatchVsync(...)
  5. Java FrameDisplayEventReceiver 回调 → 异步消息切到 Looper 队列
  6. Choreographer#doFrame(...) 执行 → Input/Animation/Traversal/Commit

各环节的职责和优化点不同,理解完整流程有助于在 Perfetto 中分析 Vsync 相关性能问题。

FrameTimeline

App 和 SurfaceFlinger 都有 FrameTimeline

image-20250918223035361

  • 轨道Expected TimelineActual Timeline
  • PresentType/JankType
    • PresentType 指示本帧呈现方式(例如 On-time、Late),JankType 指示卡顿类型来源
    • 常见 JankType:AppDeadlineMissedBufferStuffingSfCpuDeadlineMissedSfGpuDeadlineMissed
  • 操作步骤(Perfetto UI)
    1. 在应用进程选择目标 Surface/Layer 或使用 FrameToken 过滤
    2. 对齐 Expected 与 Actual,查看偏移与颜色编码
    3. 向上钻取:Choreographer#doFrameRenderThreadqueueBufferacquire/present_fence
  • 误判规避
    • 仅凭 doFrame 时长判断掉帧不可靠;以 FrameTimeline 的 PresentType/JankType 为准
    • 多缓冲可能掩盖单帧超时,需要看连续帧与 Buffer 可用性

刷新率/显示模式/VRR 对 Vsync 与 Offset/预测的影响

  • 模式切换:刷新率变更会重新配置 VsyncConfiguration,影响 app/sf Offset 与预测模型;
    • Perfetto:查 display mode change 事件与随后的 vsync 间隔变化
  • VRR(可变刷新率):目标周期不恒定,软件预测更依赖 present_fence 反馈校准;
    • Perfetto:观察 vsync 间隔分布与 present_fence 偏差
  • 多显示/外接显示:硬件层可按 physicalDisplayId 上报 vsync;但应用侧 Choreographer 通常仍以内屏/pacesetter 时序为主(实现细节随版本演进)。分析时先确认你看的到底是 HWC/SF 轨道,还是 app 轨道;
    • 版本差异:官方文档明确 Android 10 及以下“Per-display VSYNC 不支持”;Android 11+ 该限制在框架/HWC 能力层面已移除,但应用侧 Choreographer 的请求路径在 main 分支仍有“internal display”相关注释,需结合目标系统分支实测判断
    • Perfetto:按显示 ID 过滤相关 Counter/Slice

Perfetto 实战 Checklist(建议按序查看)

  1. Vsync 信号与周期
    • vsync-app / vsync-sf / vsync-appSf 间隔是否稳定(60/90/120Hz 对应周期)
    • 是否存在异常密集/稀疏的 Vsync(预测抖动)
      image-20250918223148748
  2. Vsync 相位差配置
    • VsyncWorkDuration 是否符合机型预期的 app/sf Offset
    • app 与 sf 的先后是否匹配“先绘制后合成”的策略
      image-20250918222707300
  3. FrameTimeline 判读
    • 先看 PresentType,再看 JankType;确认是 app 还是 SF/GPU 侧问题
    • 选择目标 Surface/FrameToken 定位具体帧
      image-20250918223220718
  4. 应用主线程与渲染线程
    • Choreographer#doFrame 各阶段耗时(Input/Animation/Traversal)
    • RenderThreadsyncAndDrawFrame/DrawFrame 耗时是否异常
      image-20250918223340940
  5. BufferQueue 与 Fence
    • 生产者:RenderThread queueBuffer 之后,Buffer 进入可消费队列;但 SF 是否能立刻 latch 还要看 acquire fencepresent fence 主要用于确认该帧实际送显完成时间。新版本在特定策略下可对 unsignaled buffer 先推进,再在需要时等待 fence。
      image-20250918224205093
    • 消费者 SF 与 BufferTX:SF 在每个合成节拍会尝试为目标 layer 取最新可用 Buffer。若某 layer 的 BufferTX 为 0,通常表示该 layer 暂无新 Buffer,SF 会沿用旧内容继续合成;对这个 App 来说表现为画面停滞/卡顿,但不代表 SF 全局“停止合成”。
      image-20250918223441117
  6. 合成策略与显示
    • SF 是否频繁走 ClientComposition;HWC validate/present 是否异常
    • 多显示/模式切换/VRR 时是否伴随明显预测偏差
      image-20250918223517315
  7. 资源与其他干扰
    • CPU 竞争(大核占用)、GPU 忙、IO/内存抖动(GC/compaction)
    • 其他前台应用/系统服务是否占用关键资源
      image-20250918223532482

参考文档

  1. Android Graphics Architecture
  2. VSYNC Implementation Guide
  3. Frame Pacing
  4. Perfetto Documentation
  5. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  6. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  7. Vsync offset 相关技术分析
  8. Android 13/14高版本SurfaceFlinger出现VSYNC-app/VSYNC-appSf/VSYNC-sf剖析
  9. AOSP - Choreographer.java(main)
  10. AOSP - android_view_DisplayEventReceiver.cpp(main)
  11. AOSP - DisplayEventDispatcher.h(main)
  12. AOSP - DisplayEventReceiver.cpp(main)
  13. AOSP - VSyncDispatch.h(main)
  14. AOSP - EventThread.cpp(main)
  15. Android Multi-display(官方文档)
  16. AOSP - VsyncConfiguration.h(main)
  17. AOSP - DisplayEventDispatcher.cpp(main)
  18. Unsignaled buffer latch(官方文档)

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

Android Perfetto 系列 7 - MainThread 和 RenderThread 解读

本篇是 Perfetto 系列文章的第七篇,主要介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程渲染线程。文章会从 Perfetto 的角度来看 MainThread 和 RenderThread 的工作流程,涉及卡顿、软件渲染、掉帧计算等相关知识。

随着 Google 正式推出 Perfetto 工具替代 Systrace,Perfetto 在性能分析领域已经成为主流选择。本文将结合 Perfetto 的具体 trace 信息,帮助读者理解 MainThread 和 RenderThread 的完整工作流程,让你在使用 Perfetto 分析性能问题时能够:

  • 准确识别关键 trace tag:知道 UI Thread、RenderThread 等关键线程的作用
  • 理解帧渲染的完整流程:从 Vsync 信号到屏幕显示的每个步骤
  • 定位性能瓶颈:通过 trace 信息快速找到卡顿和性能问题的根因

本文目录

系列文章目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容.

本文使用到的 Trace 文件我上传到了 Github :https://github.com/Gracker/SystraceForBlog/tree/master/Android_Perfetto/demo_app_aosp_scroll.perfetto-trace ,需要的可以自取。

注:本文内容基于 Android 16 的最新渲染架构

基于 Perfetto 的渲染流程分析

这里以滑动列表为例,我们通过 Perfetto 截取主线程和渲染线程一帧的工作流程(每一帧都会遵循这个流程,不过有的帧需要处理的事情多,有的帧需要处理的事情少)。在 Perfetto UI 中,重点观察 “UI Thread” 和 “RenderThread” 这两个线程的活动。

帧的概念和基本参数

在分析 Perfetto trace 之前,需要先了解帧(Frame)的基本概念。Android 系统按照固定的时间间隔刷新屏幕内容:

  • 60Hz 设备:每 16.67ms 刷新一次,每秒 60 帧
  • 90Hz 设备:每 11.11ms 刷新一次,每秒 90 帧
  • 120Hz 设备:每 8.33ms 刷新一次,每秒 120 帧

在 Perfetto 中分析渲染性能时,需要重点关注以下两个线程:

  • UI Thread:应用主线程,处理用户输入、业务逻辑、布局计算
  • RenderThread:渲染线程,执行 GPU 渲染命令,与 SurfaceFlinger 交互

主线程和渲染线程的工作流程

image-20250803165650716

通过上面的 Perfetto 截图,可以看到一帧完整的渲染流程。我们可以将 Perfetto 图想象成一条河流:主线程在上游处理逻辑,渲染线程在下游执行绘制。河流从左到右流动,每段代表一个步骤。

重要说明:并非每一帧都会执行所有步骤。Input、Animation、Insets Animation 都是按需执行。Traversal(measure、layout、draw)同样是按需触发:只有 requestLayoutinvalidate、窗口属性/可见性变化等场景才会通过 scheduleTraversals() 投递 CALLBACK_TRAVERSAL。在连续滑动/动画场景下它常常“看起来像每帧都在跑”。

通过以下描述,试着在脑中”播放”这个完整流程:

1. 主线程等待 Vsync 信号

  • Perfetto trace: 主线程处于 Sleep 状态(显示为空闲块)
  • 流程说明: 主线程等待垂直同步信号(Vsync)到来,这确保渲染与屏幕刷新率同步,避免画面撕裂

2. Vsync-app 信号传递过程

  • Perfetto trace: vsync-app 相关事件,SurfaceFlinger app 线程活动
  • 流程说明: 当硬件产生 Vsync 信号时,首先传递给 SurfaceFlinger。SurfaceFlinger 的 app 线程被唤醒,负责管理和分发 Vsync 信号给需要渲染的应用程序。这个中间层设计允许系统级的 Vsync 调度和优化

重要说明

  • Vsync-app 是按需申请的:只有 App 主动请求时才会收到 vsync-app 信号,不申请就没有
  • 多 App 共享机制:同时可能有多个 App 申请 vsync-app 信号
  • 信号归属问题:SurfaceFlinger 中的 vsync-app 信号可能是其他 App 申请的,当前分析的 App 如果没有申请,就不会有帧输出,这是正常现象

3. SurfaceFlinger 唤醒 App 主线程

  • Perfetto trace: FrameDisplayEventReceiver.onVsync
  • 流程说明: SurfaceFlinger 通过 FrameDisplayEventReceiver 机制将 Vsync 信号发送给已注册的 App。App 的 Choreographer 接收到信号后开始启动一帧绘制流程

4. 处理输入事件(Input)

  • Perfetto trace: Input
  • 流程说明: 仅在有输入事件时才执行,主要处理触摸、滑动等用户交互
  • 触发条件:
    • 有 Input 回调:手指按压屏幕并滑动时(如列表滑动、页面拖拽)
    • 无 Input 回调:手指抬起后的惯性滑动阶段、静止状态
  • 注意: Input 回调是由前一帧的用户交互行为决定是否在当前帧执行

5. 处理动画(Animation)

  • Perfetto trace: Animation
  • 流程说明: 仅在有动画需要更新时才执行,更新动画状态和当前帧的动画值
  • 触发条件:
    • 有 Animation 回调:惯性滑动阶段、属性动画运行时、列表 item 创建和内容变化、页面转场动画等
    • 无 Animation 回调:界面静止状态、纯 Input 交互阶段(无动画效果时)
  • 注意: Animation 回调同样由前一帧 post 的回调决定当前帧是否执行

6. 处理 Insets 动画

  • Perfetto trace: Insets Animation
  • 流程说明: 仅在有窗口插入变化时才执行,处理窗口边界动画
  • 触发条件:
    • 有 Insets Animation 回调:键盘弹出/收起、状态栏显示/隐藏、导航栏变化等
    • 无 Insets Animation 回调:窗口边界稳定状态,大部分普通交互场景

7. Traversal(测量、布局、绘制准备)

  • Perfetto trace: performTraversals, measure, layout, draw
  • 流程说明: Android UI 渲染的三大核心流程,但并不是每个 Vsync 都完整执行一遍,是否执行取决于本帧是否有布局/绘制请求。

7.1 Measure(测量阶段)

  • 作用: 确定每个 View 的尺寸大小
  • 过程: 从根 View 开始,递归测量所有子 View 的宽高
  • 关键概念:
    • MeasureSpec:封装了父容器对子 View 的尺寸要求(EXACTLY、AT_MOST、UNSPECIFIED)
    • onMeasure():每个 View 重写此方法来实现自己的测量逻辑
  • Perfetto 中的表现: measure 事件,耗时取决于 View 层级复杂度

7.2 Layout(布局阶段)

  • 作用: 确定每个 View 在父容器中的位置坐标
  • 过程: 基于 Measure 阶段的结果,为每个 View 分配实际的显示位置
  • 关键概念:
    • layout(left, top, right, bottom):设置 View 的四个边界坐标
    • onLayout():ViewGroup 重写此方法来确定子 View 的位置
  • Perfetto 中的表现: layout 事件,通常比 measure 更快

7.3 Draw(绘制阶段)

  • 作用: 将 View 的内容绘制到画布上
  • 现代实现: 不直接绘制像素,而是构建 DisplayList(绘制指令列表)
  • 关键流程:
    • draw(Canvas):绘制 View 自身内容
    • onDraw(Canvas):子类重写实现具体绘制逻辑
    • dispatchDraw(Canvas):ViewGroup 用来绘制子 View
  • Perfetto 中的表现: draw 事件,在硬件加速下主要是构建 DisplayList

ViewRootImpl.performTraversals 核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// frameworks/base/core/java/android/view/ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

private void performTraversals() {
// ... 大量窗口/relayout/可见性/同步相关逻辑
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
// 可能触发 measureHierarchy / performMeasure
}

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}

// 满足条件才会进入 draw;若取消绘制会重新 scheduleTraversals()
performDraw(mActiveSurfaceSyncGroup);
}

注:最新 AOSP 中 performTraversals 的分支远比示例复杂,涉及 relayout、surface 变化、同步组、取消重绘、窗口可见性等;这里仅保留与 Measure/Layout/Draw 直接相关的主干逻辑。

三阶段的执行条件

  • Measure: 在首次绘制、窗口/Insets/配置变化或布局请求触发时执行,不是每帧固定执行
  • Layout: 当本帧存在 layoutRequested 且应用处于可绘制状态时执行
  • Draw: 满足绘制条件时执行;若被 cancelDraw/predraw 等分支中断,会重新调度下一次 Traversal

8. 同步 DisplayList 到渲染线程

  • Perfetto trace: syncAndDrawFrame,可见 “sync” 或 “syncAndDrawFrame” 事件(通常显示为主线程向渲染线程的数据传递点)
  • 流程说明: 主线程通过 syncAndDrawFrame 将本帧 RenderNode/DisplayList 状态同步到 RenderThread。这里不是“纯异步 fire-and-forget”:UI 线程会短暂等待 RenderThread 完成关键同步点(DrawFrameTask::postAndWait),随后尽早解除阻塞;并不会一直等到屏幕真正显示该帧。

9. 渲染线程获取 Buffer

  • Perfetto trace: 常见可观察 DequeueBufferDuration / QueueBufferDuration 相关信息(具体 tag 因版本和厂商实现不同)
  • 流程说明: 渲染线程在绘制提交阶段会通过 ANativeWindow/RenderPipeline 路径完成 buffer 申请与交换。是否等待、等待多久,直接影响这一帧是否赶上 deadline。

10. 处理渲染指令并 flush 到 GPU

  • Perfetto trace: drawing 相关块
  • 流程说明: RenderThread(运行在 CPU 上)通过 HardwareRenderer/CanvasContext 处理 UI 线程同步过来的 RenderNode 树,生成 GPU 命令并提交。GPU 异步执行后产出 fence,用于后续的合成同步。

11. 提交 Buffer(可能 unsignaled)

  • Perfetto trace: queueBuffer(可观察 acquireFence 状态)
  • 流程说明: 帧提交会经过 BufferQueue/BLAST 机制进入 SurfaceFlinger,某些场景下会出现 unsignaled fence 相关行为(由系统策略决定是否允许提前 latch),目标是降低端到端延迟。

12. 触发 Transaction 到 SurfaceFlinger

  • Perfetto trace: TransactionQueue 或 BLAST transaction 事件 ,一般在 queueBuffer 之后,有些 Trace 没有这个 Tag
  • 流程说明: App 侧通过 BLAST/SurfaceControl transaction 将 buffer 与层属性更新关联后提交到 SurfaceFlinger。SurfaceFlinger 再按 LatchUnsignaledConfig 等策略决定 latch 时机并完成合成显示。

在 Perfetto 中识别不同的渲染模式

  • 手指滑动时:每帧都有 InputTraversalRenderThread 的完整链路
  • 惯性滑动时:每帧都有 AnimationTraversalRenderThread,没有 Input
  • 静止状态时:偶尔出现 AnimationTraversalRenderThread,没有 Input

软件绘制 vs 硬件加速

虽然现在基本都使用硬件加速渲染,但了解两种渲染模式的区别仍然有助于理解 Perfetto trace:

方面软件绘制硬件加速
绘制线程主线程RenderThread
绘制引擎Skia (CPU)OpenGL/Vulkan (GPU)
Perfetto 特征主线程有大块 draw 事件主线程快速完成,RenderThread 处理绘制
性能影响可能阻塞主线程异步渲染,性能更好

上面介绍的是基本的渲染流程,更详细的 Choreographer 原理可以参考 Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程


接下来我们重点讲解主线程和渲染线程的深入内容:

  1. 主线程的发展
  2. 主线程的创建
  3. 渲染线程的创建
  4. 主线程和渲染线程的分工

双线程渲染架构的演进

Android 的渲染系统经历了从单线程到双线程的重要演进过程。

单线程时代(Android 4.4 之前)

在早期的 Android 版本中,所有的 UI 相关工作都在主线程中执行:

  • 处理用户输入事件
  • 执行 measure、layout、draw
  • 调用 OpenGL 进行实际绘制
  • 与 SurfaceFlinger 交互

这种设计的问题:

  1. 响应性差:主线程负载过重,容易出现 ANR
  2. 性能瓶颈:CPU 和 GPU 无法并行工作
  3. 帧率不稳定:复杂界面容易导致掉帧

双线程时代(Android 5.0 Lollipop 开始)

Android 5.0 引入了 RenderThread,实现渲染工作的分离:

主线程职责

  • 处理用户输入和业务逻辑
  • 执行 View 的 measure、layout、draw
  • 构建 DisplayList(绘制指令列表)
  • 与渲染线程同步数据

渲染线程职责

  • 接收并处理 DisplayList
  • 执行 OpenGL/Vulkan 渲染命令
  • 管理纹理和渲染资源
  • 与 SurfaceFlinger 交互

这种架构带来的优势:

  1. 并行处理:主线程可以在渲染线程工作时处理下一帧
  2. 响应性提升:主线程不再被渲染阻塞
  3. 性能优化:GPU 资源得到更好利用

主线程的创建过程

Android App 的进程是基于 Linux 的,其管理也是基于 Linux 的进程管理机制,所以其创建也是调用了 fork 函数

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

1
pid_t pid = fork();

Fork 出来的进程,我们这里可以把他看做主线程,但是这个线程还没有和 Android 进行连接,所以无法处理 Android App 的 Message ;由于 Android App 线程运行基于消息机制 ,那么这个 Fork 出来的主线程需要和 Android 的 Message 消息绑定,才能处理 Android App 的各种 Message

这里就引入了 ActivityThread ,确切的说,ActivityThread 应该起名叫 ProcessThread 更贴切一些。ActivityThread 连接了 Fork 出来的进程和 App 的 Message ,他们的通力配合组成了我们熟知的 Android App 主线程。所以说 ActivityThread 其实并不是一个 Thread,而是他初始化了 Message 机制所需要的 MessageQueue、Looper、Handler ,而且其 Handler 负责处理大部分 Message 消息,所以我们习惯上觉得 ActivityThread 是主线程,其实他只是主线程的一个逻辑处理单元。

ActivityThread 的创建

App 进程 fork 出来之后,主路径是:
ZygoteConnection.handleChildProcZygoteInit.zygoteInitRuntimeInit.applicationInitActivityThread.main

com/android/internal/os/ZygoteConnection.java

1
2
3
4
5
6
7
8
9
10
11
private Runnable handleChildProc(ZygoteArguments parsedArgs, FileDescriptor pipeFd,
boolean isZygote) {
// ... 省略前置逻辑
if (!isZygote) {
return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
parsedArgs.mDisabledCompatChanges,
parsedArgs.mRemainingArgs, null /* classLoader */);
} else {
return ZygoteInit.childZygoteInit(parsedArgs.mRemainingArgs);
}
}

对于普通 App 进程,上面会走 zygoteInit 分支并最终进入 ActivityThread.mainchildZygoteInit 是子 zygote 的分支,不是常规 App 主路径。

android/app/ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

// 1. 初始化 Looper、MessageQueue
Looper.prepareMainLooper();

// 2. 初始化 ActivityThread
long startSeq = 0;
if (args != null) {
for (int i = args.length - 1; i >= 0; --i) {
if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
startSeq = Long.parseLong(
args[i].substring(PROC_START_SEQ_IDENT.length()));
}
}
}
ActivityThread thread = new ActivityThread();

// 3. 主要是调用 AMS.attachApplicationLocked,同步进程信息,做一些初始化工作
thread.attach(false, startSeq);

// 4. 获取主线程的 Handler,这里是 H ,基本上 App 的 Message 都会在这个 Handler 里面进行处理
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

// 5. 初始化完成,Looper 开始工作
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

注释里面都很清楚,这里就不详细说了,main 函数处理完成之后,主线程就算是正式上线开始工作.

ActivityThread 的功能

另外我们经常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了

1
2
3
4
5
6
7
class H extends Handler { // 摘抄了部分,基于 Android 16 最新实现
public static final int BIND_APPLICATION = 110; // 应用启动
public static final int CREATE_SERVICE = 114; // 创建Service
public static final int BIND_SERVICE = 121; // 绑定Service
public static final int RECEIVER = 113; // 广播接收
// ... 还有其他四大组件相关的消息类型
}

可以看到,进程创建、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,然后进到具体的 handleXXX

渲染线程的创建和发展

主线程讲完了我们来讲渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工作都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分之前主线程的渲染工作,减轻主线程的负担

软件绘制

我们一般提到的硬件加速,指的就是 GPU 加速,这里可以理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个

1
android:hardwareAccelerated="false"

我们就可以关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,直接 cpu 调用 libSkia 来进行渲染。其 Trace 跟踪表现如下(资源比较老,用 Systrace 图示)

img

与这篇文章开头开启硬件加速的 Perfetto 图对比,可以看到主线程由于要进行渲染工作,所以执行的时间变长了,也更容易出现卡顿,同时帧与帧之间的空闲间隔也变短了,使得其他 Message 的执行时间被压缩。在 Perfetto 中,这种差异通过线程活动的时间长度和密集程度可以清晰地观察到。

硬件加速绘制

正常情况下,硬件加速是开启的,主线程的 draw 主要是在构建/更新 DisplayList(RenderNode 树),然后通过 syncAndDrawFrame 同步给 RenderThread。UI 线程会在同步关键路径上短暂等待,随后尽快返回继续处理主线程消息;RenderThread 再继续执行后续渲染与提交。

渲染线程初始化

渲染线程初始化在真正需要 draw 内容的时候,一般我们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,如果没有则去进行初始化

android/view/ViewRootImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
// 渲染线程初始化
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);

// 初始化 BlastBufferQueue - App 端缓冲区管理器
if (mBlastBufferQueue == null) {
mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
mSurfaceSize.x, mSurfaceSize.y,
mWindowAttributes.format);
mBlastBufferQueue.update(mSurfaceControl,
mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);
}

这里创建的 BlastBufferQueue 将在后续的渲染过程中发挥关键作用:

  • 为 RenderThread 提供高效的 Buffer 管理
  • 支持批量 Transaction 提交,减少与 SurfaceFlinger 的交互开销
  • 在 Perfetto 中可观察到 QueuedBuffer 指标的变化

后续直接调用 draw

android/view/ThreadedRenderer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);

void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();

// 更新 RootDisplayList,构建 RenderNode 树
updateRootDisplayList(view, callbacks);

// 处理动画 RenderNode
if (attachInfo.mPendingAnimatingRenderNodes != null) {
final int count = attachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
registerAnimatingRenderNode(
attachInfo.mPendingAnimatingRenderNodes.get(i));
}
attachInfo.mPendingAnimatingRenderNodes.clear();
attachInfo.mPendingAnimatingRenderNodes = null;
}

// 同步并绘制帧,这里会触发 RenderThread 工作
int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);

// 处理各种结果状态
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
attachInfo.mViewRootImpl.mSurface.release();
attachInfo.mViewRootImpl.invalidate();
}
if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
attachInfo.mViewRootImpl.invalidate();
}
}

上面的 draw 先更新 DisplayList,之后调用 syncAndDrawFrame 进入关键同步阶段,完成 UI Thread 到 RenderThread 的数据对齐。

UI Thread 与 RenderThread 的 DisplayList 同步机制

syncAndDrawFrame 这个关键函数中,发生了以下重要的同步操作:

1
2
3
4
5
6
// frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
int RenderProxy::syncAndDrawFrame() {
// 1. 将 UI Thread 的 DisplayList 同步到 RenderThread
// 这里会把主线程构建的 RenderNode 树传递给渲染线程
return mDrawFrameTask.drawFrame();
}

syncAndDrawFrame 的底层并不是完全非阻塞。最新 AOSP 中 DrawFrameTask::drawFrame() 会执行 postAndWait():先把任务投递到 RenderThread 队列,再在条件变量上等待;RenderThread 在合适的同步点会 unblockUiThread()。因此它是“有等待但尽量早释放 UI”的设计,而不是“UI 完全不等待”。

1
2
3
4
5
6
7
8
9
10
11
12
13
// frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp
int DrawFrameTask::drawFrame() {
mSyncResult = SyncResult::OK;
mSyncQueued = systemTime(SYSTEM_TIME_MONOTONIC);
postAndWait(); // 关键:UI 线程在这里等待
return mSyncResult;
}

void DrawFrameTask::postAndWait() {
AutoMutex _lock(mLock);
mRenderThread->queue().post([this]() { run(); });
mSignal.wait(mLock);
}

具体的同步过程包括:

  1. RenderNode 树的传递:主线程在 draw 过程中构建的 RenderNode 树(包含 DisplayList)会被传递给 RenderThread
  2. 属性同步:View 的变换矩阵、透明度、裁剪区域等属性会一并同步
  3. 资源共享:纹理、Path、Paint 等绘制资源在两个线程之间建立共享机制
  4. 渲染状态传递:当前帧需要的渲染状态信息传递给 RenderThread

这个同步过程是 Android 硬件加速渲染的核心,它实现了 UI Thread 专注于逻辑处理,RenderThread 专注于渲染的分工模式。

渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui

RenderThread 与 BlastBufferQueue 的交互流程

RenderThread 接收到同步的 DisplayList 后,开始真正的渲染工作,这个过程中会与 BlastBufferQueue 进行密切的交互:

1
2
3
4
5
6
7
// frameworks/base/libs/hwui/renderthread/CanvasContext.cpp
void CanvasContext::draw(bool solelyTextureViewUpdates) {
// 1. 计算 dirty 区域并准备本帧
// 2. 通过 mRenderPipeline->draw(...) 生成绘制命令
// 3. 通过 mRenderPipeline->swapBuffers(...) 提交本帧
// 4. 记录 dequeue/queue 时延到 FrameInfo (DequeueBufferDuration/QueueBufferDuration)
}

注:旧版本常见的 dequeueBuffer/queueBuffer/flushTransaction 直观流程仍可作为理解模型,但在最新主干里这些细节被收敛在 RenderPipeline/ANativeWindow 路径中,CanvasContext::draw() 本身已不是旧代码形态。

BlastBufferQueue 的关键特性:

  1. App 端管理:不同于传统的 BufferQueue 由 SurfaceFlinger 创建,BlastBufferQueue 是由 App 端创建和管理
  2. 减少同步等待:通过生产者-消费者模型,减少了 RenderThread 在 dequeueBuffer 时的等待时间
  3. 高效的缓冲区轮转:支持更智能的缓冲区管理策略,特别适配高刷新率显示器
  4. 异步提交:通过 transaction 机制异步地将完成的帧提交给 SurfaceFlinger
  5. 支持 unsignaled buffer:配合 SurfaceFlinger 的 unsignaled latch 策略,允许在特定条件下减少端到端延迟

关于 Latching Unsignaled Buffers 的深入探讨

现代 Android 系统对 presentFence 的处理有精细的控制,并非总是等待。这个机制被称为 **”Latching Unsignaled Buffers”**(捕获未就绪的缓冲区)。

  • 传统模式: SurfaceFlinger 必须等待 App 的 presentFence 被 GPU signal 后,才能 “latch” (捕获) 这个 Buffer 进行合成。这保证了安全性,但增加了延迟。

  • Latch Unsignaled 模式: 在此模式下,SurfaceFlinger 可以立即 latch 一个 GPU 尚未完成渲染的 Buffer(即 fence 未 signaled),并提前开始部分合成工作。当它需要真正使用这个 Buffer 的内容时,它才会在内部等待 presentFence。这通过流水线化进一步隐藏了 GPU 渲染的延迟,对降低游戏、视频等全屏应用的输入延迟至关重要。

控制开关与策略 (Android 13+):

这个行为可以通过系统属性 debug.sf.auto_latch_unsignaled 进行全局调试,但更重要的是,它由一个名为 LatchUnsignaledConfig 的分层策略控制。一个典型的策略是 AutoSingleLayer

  • 当屏幕上只有单个图层更新时(如全屏游戏或视频),系统会自动启用 Latch Unsignaled 模式,因为此时没有复杂的图层依赖,风险最低,收益最大。
  • 当有多个图层更新时,系统会回退到更安全的传统等待模式,以避免潜在的视觉错误。

因此,SurfaceFlinger 并非总是盲目等待 presentFence,而是根据精密的策略来决定是否“抢跑”,以在稳定性和极致性能之间取得平衡。

主线程和渲染线程的分工

主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DisplayList ,但是不涉及与 SurfaceFlinger 直接打交道;渲染线程负责渲染相关的工作,包括与 BlastBufferQueue 的交互、GPU 渲染命令的执行,以及与 SurfaceFlinger 的最终交互。

当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类,这样间接的进行绘制操作的优点如下

  1. DisplayList 可以按需多次绘制而无须同业务逻辑交互
  2. 特定的绘制操作(如 translation、scale 等)可以作用于整个 DisplayList 而无须重新分发绘制操作
  3. 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次
  4. 可以将对 DisplayList 的处理转移至另一个线程(也就是 RenderThread)
  5. 主线程在 sync 结束后可以处理其他的 Message,而不用等待 RenderThread 结束
  6. 通过 BlastBufferQueue 实现更高效的缓冲区管理,减少渲染延迟和主线程阻塞

BlastBufferQueue 的工作原理

BlastBufferQueue 是现代 Android 渲染架构中的关键组件,它改变了传统的缓冲区管理方式:

传统 BufferQueue vs BlastBufferQueue:

  1. 创建主体不同

    • 传统 BufferQueue:由 SurfaceFlinger 创建和管理
    • BlastBufferQueue:由 App 端(ViewRootImpl)创建和管理
  2. 缓冲区获取机制

    • 传统方式:RenderThread 需要通过 Binder 调用向 SurfaceFlinger 请求 Buffer,可能会因为没有可用 Buffer 而阻塞
    • BlastBufferQueue:App 端预先管理缓冲区池,RenderThread 可以更高效地获取 Buffer
  3. 提交机制

    • 传统方式:通过 queueBuffer 直接提交给 SurfaceFlinger
    • BlastBufferQueue:通过 transaction 机制批量提交,减少 Binder 调用开销

在 Perfetto 中观察 BlastBufferQueue:

在 Perfetto 跟踪中,BlastBufferQueue 的状态通过以下关键指标显示:

App 端的 QueuedBuffer 指标

  • Perfetto 显示QueuedBuffer 数值轨道
  • AOSP 定义(BLASTBufferQueue)QueuedBuffer = mNumFrameAvailable + mNumAcquired - mPendingRelease.size()
  • 解释方式:它是一个“综合状态量”,反映可用帧、已获取帧、待 release 的平衡关系,不建议用固定常量偏移去做一刀切换算
  • 实战建议:重点看趋势和持续时间,而不是单个瞬时值

image-20250803170713946

QueuedBuffer 数值变化时机

QueuedBuffer +1 的时机

  • 常见触发:生产侧有新 buffer 到达 BLAST,并进入可处理状态
  • Perfetto 表现QueuedBuffer 轨道上升
  • 含义:App 端到 SF 端之间的“待处理帧压力”增加

image-20250803170852607

QueuedBuffer -1 的时机

  • 触发条件:收到 SurfaceFlinger 的 releaseBufferCallback
  • Perfetto 表现:可观察到 releaseBuffer 相关事件
  • 含义:某个 buffer 完成消费或被处理后释放,队列压力下降

image-20250803171008400

SurfaceFlinger 端的 BufferTX 指标

  • Perfetto 显示:SurfaceFlinger 进程中的 BufferTX 数值轨道
  • AOSP 定义(Layer):该值对应每层 mPendingBuffers 的跟踪;buffer 到达 server 侧会增加,buffer 被 latch 或 drop 会减少
  • 触发条件:与 transaction 和 buffer 生命周期共同相关,不建议简单等价为“收到 transaction 就 +1”
  • 注意:它不是通用“固定最大 3”的指标,受图层类型、生产消费节奏和系统策略影响

image-20250803171228146

App 端和 SF 端的协作流程

  1. App 端:RenderThread 提交新帧后,QueuedBuffer 常见上升
  2. 跨进程:BLAST/SurfaceControl transaction 关联 frameNumber 并进入 SF 侧处理
  3. SF 端BufferTX 随 pending buffer 变化(到达增、latch/drop 减)
  4. 回流releaseBufferCallback 到 App 端后,QueuedBuffer 下降

关键性能观察点

在分析性能时,重点关注:

  • App 端 QueuedBuffer 趋势:连续上升且长时间不回落,通常表示生产/消费节奏失衡;结合主线程 performTraversals 与 RenderThread DrawFrames 判断瓶颈在 App 还是 SF/GPU 侧
  • SurfaceFlinger 端 BufferTX 趋势:长期偏高常见于消费侧压力大;长期偏低且 App 又频繁 miss deadline,往往是生产侧供给不足

性能

如果主线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。主线程执行超时通常会带来两个问题

  1. 卡顿:如果主线程 + 渲染线程每一帧的执行都超过 8.33ms(120fps 的情况下),那么就可能会出现掉帧(说可能是因为有的情况下其实不会掉帧,因为有 app duration 、buffer 堆积等情况)。
  2. 卡死:如果界面线程被阻塞超过几秒钟时间(根据组件不同 , 这里的阈值也不同),用户会看到 “应用无响应“ (ANR) 对话框(部分厂商屏蔽了这个弹框,会直接 Crash 到桌面)

对于用户来说,这两个情况都是用户不愿意看到的,所以对于 App 开发者来说,两个问题是发版本之前必须要解决的,ANR 这个由于有详细的调用栈,所以相对来说比较好定位;但是间歇性卡顿这个,可能就需要使用工具来进行分析了:Perfetto + Trace View (Android Studio 已经集成),所以理解主线程和渲染线程的关系和他们的工作原理是非常重要的,这也是本系列的一个初衷。

Perfetto 独有的 FrameTimeline 功能

Perfetto 相比 Systrace 的一个重要优势是提供了 FrameTimeline 功能,可以一眼就可以看到卡顿的地方。

注意: FrameTimeline 需要 Android 12(S) 或更高版本支持

FrameTimeline 的核心概念

根据 Perfetto 官方文档,当帧在屏幕上的实际呈现时间与调度器预期的呈现时间不匹配时,就会产生卡顿。FrameTimeline 为每个有帧在屏幕上显示的应用添加了两个新的轨道:

image-20250803172616453

1. Expected Timeline(预期时间线)

  • 作用: 显示系统分配给应用的渲染时间窗口
  • 开始时间: Choreographer 回调被调度运行的时间
  • 含义: 为了避免系统卡顿,应用需要在这个时间范围内完成工作

2. Actual Timeline(实际时间线)

  • 作用: 显示应用完成帧的实际时间(包括 GPU 工作)
  • 开始时间: Choreographer#doFrameAChoreographer_vsyncCallback 开始运行的时间
  • 结束时间: max(GPU 时间, Post 时间),其中 Post 时间是帧被提交到 SurfaceFlinger 的时间

当你点击 Actual Timeline 上的一个 追踪的时候,会显示这一帧具体的被消费的时间(可以看延时)。

image-20250803172911195

颜色编码系统

FrameTimeline 使用直观的颜色来标识不同的帧状态:

颜色含义说明
绿色正常帧没有观察到卡顿,理想状态
浅绿色高延迟状态帧率稳定但帧呈现延迟,导致输入延迟增加
红色卡顿帧当前进程导致的卡顿
黄色应用无责任卡顿帧出现卡顿但应用不是原因,SurfaceFlinger 导致的卡顿
蓝色丢帧SurfaceFlinger 丢弃了该帧,选择了更新的帧

点击不同颜色的 ActualTimeline 可以在信息栏看到下面的描述,告诉你卡顿的原因:
image-20250803173304026

卡顿类型分析

FrameTimeline 可以识别多种卡顿类型:

应用端卡顿:

  • AppDeadlineMissed: 应用运行时间超过预期
  • BufferStuffing: 应用在前一帧呈现前就发送新帧,导致 Buffer 队列堆积

SurfaceFlinger 卡顿:

  • SurfaceFlingerCpuDeadlineMissed: SurfaceFlinger 主线程超时
  • SurfaceFlingerGpuDeadlineMissed: GPU 合成时间超时
  • DisplayHAL: HAL 层呈现延迟
  • PredictionError: 调度器预测偏差

配置 FrameTimeline

在 Perfetto 配置中启用 FrameTimeline:

1
2
3
4
5
data_sources {
config {
name: "android.surfaceflinger.frametimeline"
}
}

Perfetto 中 Vsync 信号

在 Perfetto 中,Vsync 信号使用 Counter 类型来显示,这与很多人的直觉认知不同:

  • 0 → 1 的变化:表示一个 Vsync 信号
  • 1 → 0 的变化:同样表示一个 Vsync 信号
  • 错误理解:很多人误以为只有变成 1 才是 Vsync 信号

正确的 Vsync 信号识别

下图中 1 、2、3、4 的时间点都是 Vsync 信号到达

image-20250803173809421

关键要点

  1. 每次数值变化都是一个 Vsync:无论是 0→1 还是 1→0
  2. 信号频率:120Hz 设备上约每 8.33ms 会有一次变化(实际可能因系统调度略有差异,这里指的是连续出帧场景)
  3. 多 App 场景:Counter 可能因为其他 App 的申请而保持活跃状态

分析技巧

判断 App 是否接收到 Vsync

  • 正确方法:查看 App 进程中是否有对应的 FrameDisplayEventReceiver.onVsync 事件
  • 错误方法:仅凭 SurfaceFlinger 中的 vsync-app counter 变化来判断

参考

  1. https://juejin.im/post/5a9e01c3f265da239d48ce32
  2. http://www.cocoachina.com/articles/35302
  3. https://juejin.im/post/5b7767fef265da43803bdc65
  4. http://gityuan.com/2019/06/15/flutter_ui_draw/
  5. https://developer.android.google.cn/guide/components/processes-and-threads

附件

本文涉及到的 Perfetto 跟踪文件也上传了,各位下载后可以在 Perfetto UI (https://ui.perfetto.dev/) 中打开分析

点此链接下载文章所涉及到的 Perfetto 跟踪文件

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ⭐

Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战

本文是 Android Perfetto 系列的第六篇,主要介绍 Android 设备上 120Hz 刷新率的相关知识。如今,120Hz 已成为 Android 旗舰手机的标配,本文将讨论高刷新率带来的优势和挑战,以及从系统角度解析 120Hz 的工作原理。

在过去的几年中,移动设备的屏幕刷新率经历了从 60Hz 到 90Hz,再到现在普遍的 120Hz 的演进过程。这种提升不仅带来了更流畅的视觉体验,也对系统架构和应用开发提出了新的要求。通过 Perfetto 工具,我们可以更直观地理解高刷新率设备上帧渲染的过程和性能表现。

本文目录

系列文章目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

基本概念

什么是屏幕刷新率?

屏幕刷新率是一个硬件概念,指的是屏幕每秒钟刷新显示内容的次数,单位是赫兹(Hz)。

  • 60Hz 屏幕:每秒刷新 60 次,每次刷新间隔约 16.67ms
  • 90Hz 屏幕:每秒刷新 90 次,每次刷新间隔约 11.11ms
  • 120Hz 屏幕:每秒刷新 120 次,每次刷新间隔约 8.33ms

屏幕刷新率决定了显示设备能够展示的最高帧率,但屏幕只负责按固定频率显示内容,具体显示什么内容由软件系统决定。

什么是 FPS?

FPS(Frames Per Second)是一个软件概念,指的是系统每秒生成多少帧内容提供给屏幕显示。

  • 60FPS:系统每秒生成 60 帧内容,每帧有约 16.67ms 的处理时间
  • 90FPS:系统每秒生成 90 帧内容,每帧有约 11.11ms 的处理时间
  • 120FPS:系统每秒生成 120 帧内容,每帧有约 8.33ms 的处理时间

为了获得最佳的视觉体验,FPS 应该与屏幕刷新率匹配。如果 FPS 低于刷新率,会出现掉帧;如果 FPS 高于刷新率,多余的帧会被丢弃,造成资源浪费。

为了获得最佳的视觉体验,理想情况下 FPS 应该与屏幕刷新率匹配,但实际体验与内容类型和用户感知紧密相关:

  1. 内容类型差异

    • 视频内容:电影(24fps)或视频(30fps)即使在 120Hz 屏幕上也能看起来流畅,这是因为视频内容包含自然运动模糊,且符合观看者对该媒介的预期
    • 交互式界面:而滑动列表、动画等交互场景对帧率要求更高,从 120fps 降到 110fps 都可能被用户感知为卡顿
  2. 帧率稳定性:稳定的低帧率(如稳定的 60fps)通常比不稳定的高帧率(如在 90-120fps 之间波动)体验更好

  3. 系统行为

    • 当 FPS 低于刷新率时,显示系统会复用帧或插入黑帧
    • 当 FPS 高于刷新率时,多余的帧会被丢弃,造成计算资源浪费

不同应用场景有不同的流畅度标准,开发者需要根据应用类型选择合适的优化策略。

什么是 Vsync?

Vsync(垂直同步)是将软件帧率与屏幕刷新率同步的机制,目的是避免画面撕裂现象。Android 系统中,Vsync 信号被用来触发应用渲染新一帧的时机,确保渲染过程与屏幕刷新周期保持一致。

为什么 120Hz 成为新标准?

市场从 60Hz 到 90Hz,再到 120Hz 的演进有着明确的技术和用户体验驱动因素:

  1. 更高的流畅度:120Hz 比 60Hz 提供了两倍的视觉信息,使滑动、动画等交互感觉更加流畅自然。

  2. 减少延迟:输入事件到显示结果的延迟从 60Hz 的 16.67ms 减少到 120Hz 的 8.33ms,让用户操作反馈更及时。

  3. 硬件支持成熟:现代移动处理器(如高通骁龙 8 系列、联发科天玑系列)已经有足够性能支持 120Hz 的稳定运行。

  4. 电池技术进步:更高效的电池和电源管理技术缓解了高刷新率带来的功耗压力。

  5. 可变刷新率技术:LTPO 等自适应刷新率技术允许设备在不同场景下智能切换刷新率,平衡流畅度和功耗。

如今,120Hz 不仅是 Android 旗舰机型的标配,连 iOS 设备(iPhone 13 Pro 及以上)也已支持 120Hz 的 ProMotion 技术,标志着高刷新率已成为高端移动设备的基本特性。

系统实现与工作原理

Perfetto 视角下的 120Hz 渲染流程

在 120Hz 刷新率下,Android 系统的渲染流程没有本质变化,主要区别是每一帧的时间预算从 16.67ms 缩短到了 8.33ms ( 当然这里没有讨论 App duration,如果 App Duration 配置大于 8.33ms,那么 App 的 UI + Render 在 App duration 区间完成都是可以的,注意 每个机器的 App Duration 配置不一样)。下图展示了 120Hz 环境下应用渲染的 Perfetto 追踪图:

120Hz 下 App 的渲染流程

在 120Hz 设备上,我们可以看到:

  1. Vsync 间隔:VSYNC 信号每 8.33ms 触发一次
  2. 帧处理流程:每一帧的处理依然遵循 Input → Animation → Traversal 的顺序
  3. 时间压缩:所有处理步骤必须在更短的时间内完成,对系统和应用性能要求更高

上图中出现了两个 Buffer 相关的 Trace,这里做一个简单的说明:

  1. QueuedBuffer:(例如:QueuedBuffer - VRI[ImproveSnsTimelineUI]#748BLAST#748)
    • 这个 Trace Tag 是在 App 进程中打印的
    • 表示应用完成一帧渲染后,将渲染好的 Buffer 放入队列准备提交给 SurfaceFlinger
    • 在使用 BlastBufferQueue 的系统中,这个时刻标志着 RenderThread 完成渲染并准备将结果传输到系统服务
  2. BufferTX:(例如:BufferTX - com.tencent.mm/com.tencent.mm.plugin.sns.ui.improve.ImproveSnsTimelineUI#47974)
    • 这个 Trace Tag 是在 SurfaceFlinger 进程中打印的
    • 表示 SurfaceFlinger 接收到应用传来的 Buffer 并开始处理的时刻
    • TX 代表 “Transfer/Transmission”,即缓冲区的传输过程

正确的流程应该是:

  1. App 的 RenderThread 调用 queueBuffer,此时 App 认为自己已交出一个 Buffer,于是 QueuedBuffer +1
  2. 该 Buffer 被传输给 SF,SF 接收(latch/acquire)后,BufferTX +1
  3. SF 在未来的某个 Vsync 周期,使用这个 Buffer 完成合成并上屏。
  4. 当 SF 不再需要这个 Buffer 时(例如,它已经被新的帧替换,或者已稳定显示了足够长的时间),SF 会释放(release)这个 Buffer。
  5. SF 释放 Buffer 后,BufferTX -1。同时,App 会收到 Buffer 已被释放的回调,此时 App 端的 QueuedBuffer 才会 -1,表示这个 Buffer 已成功返回缓冲池,可被再次使用。

Perfetto 同时也提供了 Buffer 追踪的功能,点击 App 上面的 Actual Timeline (这部分知识可以看 Actual Timeline 介绍),就可以看到这个 Buffer 从生产到消费的全过程。

image-20250426150441133

支撑 120Hz 的系统架构优化

要流畅地支持 120Hz 高刷新率,Android 系统架构做了不少调整和改进。这些变化涉及多个组件,也包括了对整个渲染管线的重要优化。下面我们来详细看看几个关键的技术点:

自适应刷新率技术

现代 Android 设备采用多层次刷新率管理策略:

  • 硬件层支持:LTPO(低温多晶氧化物)显示技术允许屏幕在 1Hz 到 120Hz 范围内精确调节刷新率,而非固定档位切换

  • 内容感知算法:系统通过分析屏幕内容类型自动调整刷新率:

    • 静态内容(阅读、图片浏览):降至 10-30Hz
    • 视频播放:匹配视频源帧率(通常 24-60Hz)
    • 滚动和交互:提升至 90-120Hz
    • 游戏:根据游戏引擎输出帧率动态调整
  • API 支持:Android 提供 Surface.setFrameRate() API,允许应用明确指定其首选帧率,系统会尽可能满足这一请求

1
2
// 应用可以指定首选帧率和刷新率行为
surface.setFrameRate(60.0f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);

120Hz 的优势与挑战

120Hz 带来的体验提升

我拿到第一台120Hz手机的时候,最直观的感受就是:

  1. 一切操作都变得更流畅:从桌面滑动、应用切换到刷微博,画面更新更频繁,内容跟随手指移动也更加精准。尤其在快速滑动朋友圈或微博feed流时,文字依然清晰可辨,而不是一片模糊。

  2. 游戏体验大幅提升:玩《王者荣耀》或《和平精英》这类竞技游戏时,画面流畅度提升让我的操作精准度也跟着提高。在激烈对抗中,能提前8ms看到敌人动作,虽然时间很短,但确实能带来优势。

  3. 眼睛疲劳感减轻:这点可能是个人感受,但长时间盯着120Hz屏幕确实比60Hz舒适,特别是阅读和滑动内容时,眼睛追踪内容的负担减轻了。

  4. 触控体验更精准:120Hz不仅是显示更新快,触控采样率通常也会提高,让操作响应更及时,无论是打字还是精细控制都更准确。

120Hz 面临的实际问题

当然,高刷屏幕也带来了一系列技术挑战:

  1. 功耗问题:实测中,同一台手机在120Hz模式下比60Hz大约多耗电15-20%。对于本就紧张的手机续航来说,这是不小的压力。

  2. 开发门槛提高:原来在60Hz环境下勉强能跑的应用,到了120Hz可能就会显得卡顿。每帧只有8.33ms的处理时间,对开发者的代码效率提出了更高要求。

  3. 发热增加:长时间运行高帧率游戏,手机发热明显比60Hz更严重,这不仅影响体验,还可能导致性能降频。

  4. 应用适配并不完善:很多应用并未针对高刷做优化,即使在120Hz屏幕上,实际输出帧率可能还是60fps,浪费了屏幕潜力。

思考与展望

按需调整:从 ProMotion 看刷新率的智能管理

随着 120Hz 高刷新率逐渐成为旗舰手机的标配,一个值得思考的问题是:我们真的需要在所有场景下都保持 120Hz 的刷新率吗?

苹果的 ProMotion 技术实际上给出了一个更为合理的答案:只有在真正需要高感知度的动画场景下才激活高刷新率,而在其他场景则可以适当降低刷新率以节省电量。

iOS Promotion 的动画建议帧率

从上图可以看到,苹果在iOS开发文档中为不同类型的动画场景提供了非常精细的ProMotion帧率推荐配置:

  1. 高影响力动画(High-impact animations)

    • 适用场景:全屏转场(如照片应用中点击缩略图展开)、第一人称游戏、Sheet弹出展示等
    • 推荐帧率:80-120Hz,首选120Hz(CAFrameRateRange(minimum:80, maximum:120, preferred:120))
    • 使用建议:谨慎使用,仅在关键交互场景应用,以减少电量消耗
  2. 透明度/颜色过渡和微小移动

    • 适用场景:开关状态变化、进度指示器旋转、背景模糊效果等
    • 推荐帧率:使用系统默认帧率范围(CAFrameRateRange.default)
    • 使用建议:这类动画不需要过高帧率,视觉效果差异不大
  3. 低速小动画

    • 适用场景:时钟指针移动、缓慢进度条等
    • 推荐帧率:根据动画速度,可选择8-15Hz、15-24Hz或30-48Hz不等
    • 使用建议:低帧率在这些场景下视觉效果已足够好,同时可显著节省电量
  4. 其他所有情况

    • 推荐使用系统默认帧率

这种精细化的帧率管理策略,不仅让系统能够在用户体验和电池寿命之间取得最佳平衡,也为开发者提供了明确的指导。相比于简单粗暴地全局使用120Hz,这种有针对性的帧率调整方案显然更加科学和高效。

电量与体验的权衡

测试表明,将刷新率从 120Hz 降至 60Hz 可节省约 10-15% 的电量。智能地控制刷新率,能在保持良好用户体验的同时,显著延长电池续航时间。

120Hz 的主要价值在于提升交互流畅度和响应速度,而非始终保持高刷新率。更智能的做法是根据实际需求动态调整:在用户感知敏感的场景使用高刷新率,在用户感知不敏感的场景降低刷新率

开发者的适配策略

应用开发者应当意识到,并非所有内容都需要以最高帧率渲染。通过 Android 提供的 API(如 Surface.setFrameRate()),可以为不同内容类型指定合适的帧率,配合系统的自适应刷新率机制,共同达到最佳的性能与电量平衡。

总之,未来高刷新率技术的发展方向应该是更加智能、更加精细的自适应调节,而非简单地追求更高的数字。真正的技术进步是在用户无感知的情况下,在体验和能效之间找到最佳平衡点。

结论

回顾过去几年高刷屏幕的发展,我认为120Hz确实是手机交互体验的一次重要跃升。虽然它带来了功耗和开发复杂性等挑战,但好处是显而易见的:更流畅的体验、更低的输入延迟、更自然的动画效果。

对于开发者而言,Perfetto这类工具让我们能够看清120Hz下的性能问题,有的放矢地进行优化。虽然从16ms减少到8ms的预算听起来很紧张,但事实上主流处理器已经有足够能力应对这个挑战。只要合理规划UI复杂度、避免主线程阻塞,流畅的120fps体验是完全可以实现的。

从趋势来看,我不认为手机屏幕刷新率会无限攀升。120Hz可能会在相当长的时间内成为标准,而未来的焦点将更多放在如何智能化地调整刷新率,在不同场景下找到体验和功耗的最佳平衡点。毕竟,我们追求的不是数字上的高,而是实际体验的好。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

☑️ ⭐

Android Weekly 2025-14 期

Android Weekly 是一份专注于 Android 技术生态的周刊,每周一更新。本周刊深入挖掘 Android 系统架构、性能优化、跨平台开发、AI 等领域的高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:[微信公众号] | [知乎专栏] | [掘金] | [RSS]

技术文章

  1. 跨平台框架技术视角看 GapBuffer: 本文从跨平台框架开发者的视角,探讨了 GapBuffer 这一数据结构在 UI 框架中的应用,特别是在 Compose 中的实现。文章详细介绍了 GapBuffer 如何通过插入和删除操作来优化 UI 树的变动,从而提高性能。
  2. 移动 OS 设计之性能设计 6 - 技术指标的两类优化目标类型: 本文探讨了移动操作系统性能设计中的技术指标优化,主要分为两类:最优化问题和特定区间内的优化问题。最优化问题追求性能极限,而特定区间内的优化关注在某一性能区间内的稳定性。明确优化目标对于选择合适的技术手段至关重要,避免混用不同类型的策略。
  3. JetBrains Terminal 又发布新架构,Android Studio 将再次迎来新终端: JetBrains 在不到一年内对其终端进行了重大重构,以解决先前版本中的兼容性问题。新的终端架构引入了增强功能,如 AI 驱动的命令生成,但也因兼容性问题收到负面反馈。2025.1 版本将重点放在兼容性和一致性上,确保终端的基本功能不受影响,同时为未来的高级功能铺平道路。
  4. 手机系统的 D-Vsync 渲染显示管线优化: 本文介绍了一种智能手机显示渲染优化方案,称为解耦垂直同步(D-VSync)。D-VSync 通过解耦渲染和显示的设计,解决了传统 VSync 的局限性,尤其是在长帧和短帧负载波动时导致的卡顿问题。此方案已在开源鸿蒙和安卓系统中实现,显著减少了掉帧和渲染延迟。
  5. 应用预测框架-基于 Android S: 本文介绍了基于 Android S 的应用预测框架,详细分析了其核心机制、实际应用场景及用户体验优化效果,并探讨了如何使用 TensorFlow 进行模型训练和部署以提升预测准确性。
  6. LWN:减少 TLB 压力的措施!: 本文探讨了如何减少 CPU 的 TLB(Translation Lookaside Buffer)压力。TLB 缓存虚拟地址转换结果,显著加速内存访问,但 TLB 失效会导致高昂代价,因此优化 TLB 使用是重要课题。文章讨论了在 Linux 存储、文件系统、内存管理和 BPF 峰会上 Rik van Riel 关于内存管理的主题会议,其中包括减少 TLB 压力的方法,如使用透明大页(THP)和多尺寸透明大页(mTHP),以及如何更好地管理这些大页以提高 TLB 利用率。此外,文章还探讨了用户空间如何帮助减少 TLB 失效,以及如何优化内核机制以更好地支持 mTHP。
  7. 对数和自然对数的底: 这篇文章探讨了对数的历史背景及其在数学中的应用,尤其是纳皮尔对数表的制作过程及其在天文学计算中的重要性。文章详细描述了纳皮尔如何在没有幂概念的情况下,通过几何意义和三角公式来发展对数概念。
  8. DRM(Digital Rights Management)生态以及架构介绍: DRM(数字版权管理)技术通过加密、授权管理和安全传输等手段,保护数字内容在分发、存储和播放过程中的安全性。随着数字内容从物理媒介转向在线流媒体平台,DRM 成为数字内容分发的基石。Google 的 Widevine 因其开放的生态系统和灵活的安全等级成为流媒体领域的主流方案。
  9. Linux | 关于 CPU 调频的一些 QA: 本文对 Android 开发过程中关于 CPU 调频的一些常见问题进行了详细的解答,并介绍了与 CPU 调频相关的技术细节和实现方法。
  10. 理解 VSync-1-软件 VSync 及节拍器: 本文是 VSync 系列文章的第一篇,主要介绍了软件 VSync 的概念、模型、分类及其产生过程,并详细探讨了 VsyncDispatch 及其子类 VSyncDispatchTimerQueue 的创建与实现。
  11. 理解 VSync-2-app,appsf sf 注册回调: 本文详细介绍了在 Android 系统中,应用程序(app)和 SurfaceFlinger(sf)如何向 VsyncDispatch 注册回调的过程。通过分析代码流程,揭示了涉及的类和回调函数的创建与调用关系。
  12. 理解 VSync-3-应用添加链接: 本文详细介绍了在 Android 系统中,应用如何与 VSync 信号进行交互的过程,特别是在 Java 层和 Native 层中如何建立连接并处理 VSync 信号。
  13. 理解 VSync-4-应用申请与接收 VSync(上): 本文详细分析了应用如何请求和接收 VSync 信号,涵盖从 Java 层到 Native 层的调用链,并解释了 VSyncRequest 的状态转换及其在应用中的实现逻辑。
  14. 技术简报 2025 第四期: 这篇文章探讨了技术学习的重点,强调应该专注于概念而非技术本身。文章还详细介绍了多种文件格式的设计和理解,包括 ELF、DEX 等,并分享了一些技术见解和思考。
  15. 用魔法打败魔法:互联网大厂虚拟机分析还原: 本文探讨了互联网大厂的虚拟机分析与还原,详细介绍了如何通过逆向工程技术来分析和还原虚拟机保护的应用程序。文章分享了作者的个人分析方法和思考过程,并提供了相关代码示例和工具使用建议。
  16. Dart 单线程异步模型:从原理到工程实践的系统化解析: Dart 的单线程异步模型通过非阻塞 I/O 和事件循环实现“单线程不阻塞”,有效解决传统单线程的同步阻塞问题。文章详细解析了 Dart 的事件驱动模型与多线程模型的区别,介绍了事件循环的双队列架构及其在异步任务调度中的作用,并提供了工程实践中的常见误区及修正方案。通过系统化理解 Dart 的异步模型,开发者可以在 I/O 密集型场景中发挥其高效调度优势,并通过任务分片技术突破 CPU 密集型任务的瓶颈。
  17. Android.bp 中添加条件判断编译方式: 这篇文章讨论了如何在 Android 的构建系统中使用条件判断来适应不同设备和版本的需求,尤其是通过 Android.mk 和 Android.bp 文件进行差异化编译。文章介绍了如何利用条件判断来选择适合的 API 或库,以便在一个公共代码分支中实现对多个产品设备的兼容,减少维护成本。
  18. Android 提升开发测试效率,程序员应该多干了些什么?: 本文讨论了如何提高 Android 开发测试效率,分享了一些实际操作建议和技巧,以帮助程序员提高工作效率,避免低效的沟通和重复工作。
  19. 电影电视剧网红广告屏自动轮播介绍视频特效制作,Compose 轻松实现: 这篇文章介绍了如何使用 Compose 框架制作自动横向滚动的广告视频动画。文章详细描述了数据模型的设计、动画的实现逻辑以及具体的代码实现步骤。通过使用 Compose 的不同组件和工具,用户可以轻松创建带有音乐背景的广告视频特效。
  20. Prioritize media privacy with Android Photo Picker and build user trust: 本文讨论了如何通过 Android Photo Picker 提升用户隐私并建立用户信任。Google Play 致力于在处理敏感权限和用户数据时提供更清晰的选择,以便用户可以自信地授予权限而不牺牲应用功能或隐私。
  21. Gemini in Android Studio for businesses: Develop with confidence, powered by AI: Android 开发者博客宣布推出适用于企业的 Android Studio 中的 Gemini,这是一款专为满足小型和大型组织的隐私、安全和管理需求而设计的产品。通过订阅 Gemini Code Assist 标准版或企业版,开发者和管理员可以解锁这些功能和优势。此外,企业可以通过 Google Cloud 控制台直接为组织中的开发者分配许可证。Gemini 提供了多种安全特性和行业认证,确保客户代码和数据的安全。企业还可以通过连接到 GitHub、GitLab 或 BitBucket 存储库,实现代码定制化。Gemini 在 Android Studio 中为整个软件开发生命周期提供智能协助,提高生产力。
  22. Flutter 2025 年产品路线图发布: 每一年 Google Flutter 团队都会发布一份产品路线图,包括 Flutter 框架和 Dart 编程语言,让开发者能够了解官方团队的优先事项,并据此做出自己的计划安排。产品路线图也会随着客户反馈和新兴市场机会的变化而不断发展。开发者们可以通过每季度的调查问卷以及 GitHub 上 issue 的反馈来推进这些工作的优先级。
  23. 速学 Android 16 新功能:带有进度的通知类型: 本文介绍了 Android 16 的新功能,特别是 Notification.ProgressStyle 通知类型的应用及其代码实现。这种通知类型允许用户更好地追踪任务进度,并通过自定义设置显示在通知栏中。文中还提供了具体的代码示例,展示了如何使用此功能在导航、外卖等场景中显示实时位置和进度。
  24. Android 应用的 CPU 调度策略优化: 本文探讨了在 Android 应用中优化 CPU 调度策略的方法,包括设置线程优先级、配置实时调度策略、利用 Android 平台特性以及通过 cpuset 控制 CPU 资源分配等。文章详细介绍了如何通过调整线程的优先级和绑定 CPU 核心来提升应用性能。

杂记

  1. 为什么我看空英伟达股票 [译]: 本文深入分析了英伟达股票的潜在风险,尽管其在 AI 和深度学习领域占据主导地位,但面临来自新兴技术和竞争对手的多重挑战。作者认为,英伟达的高估值可能无法长期维持,因为市场上出现了新的硬件架构、软件框架,以及更高效的 AI 训练和推理方法,可能削弱英伟达的市场优势。
  2. YC 创始人 Paul Graham 最好的一篇文章: YC 创始人 Paul Graham 的最新博客文章探讨了人生的意义,特别是关于创造的主题。文章认为,单纯的享乐无法带来持久的快乐,真正有价值的是创造新事物。创造不仅能体现人性的智慧,还能带来自我实现的幸福感。无论是创业、写作还是其他形式的创造,关键在于对他人有益,并能在过程中获得成就感。
  3. 聊一聊 2025 年 Android 人的求职之路: 阿豪在 2025 年求职 Android 岗位时,发现市场环境变得更加严峻。大部分岗位集中在一线城市,二线城市的机会大幅减少。求职者普遍面临降薪和转岗的风险,外包岗位增多。阿豪建议通过内推增加面试成功率,并强调熟悉简历上的技能和项目。最终,阿豪选择了传音控股的 Android 系统开发岗位,因为其薪资竞争力、市场潜力和良好的工作氛围。
  4. Searching for outliers: 这篇文章探讨了博客文章和其他生活领域中重尾分布的现象。作者分享了个人写作经历,强调在重尾分布中,少数成功的例子往往能够产生重大影响,而大多数尝试可能平淡无奇。文章通过多个例子说明了如何识别和利用重尾分布,以便更有效地寻找和评估潜在的“异类”机会。
  5. 独立开发周记 112:3 月数据总结,收入多样: 独立开发者道哥在其周记中总结了 3 月的数据表现,强调了极简日记应用的下载量和收入的显著增长,并分享了多样化收入的探索经历。此外,他也提到了个人生活中的一些小趣事。
  6. The Best Programmers I Know | Matthias Endler: 这篇文章总结了成为优秀程序员的关键特质,包括:深入阅读参考文档,全面了解工具,认真解读错误信息,善于分解问题,勇于动手实践,乐于助人和分享,保持持续学习的热情,不拘泥于地位,建立良好声誉,保持耐心,不怕承认不知道,避免猜测,并编写简单而易维护的代码。
  7. 科技爱好者周刊#344:制造业正在“零工化”: 这里记录每周值得分享的科技内容,周五发布。
  8. G̶o̶o̶g̶l̶e̶r̶… ex-Googler.: 这篇文章讲述了一位曾在谷歌工作的员工被公司裁员后的心情。他感到震惊、愤怒和失望,并分享了他在 Chrome 团队的工作经历以及裁员对他的影响。

AI

  1. 微软推出 Playwright 官方 mcp server,效果试用!: 微软推出了 Playwright-mcp 服务器,这是一个基于 MCP 协议的工具,旨在增强 AI 应用的实用性。MCP 协议由 Anthropic 公司于 2024 年推出,旨在标准化大语言模型与外部数据源和工具的交互接口。Playwright-mcp 可以与 Vscode copilot 无缝结合使用,帮助用户无代码完成浏览器操作和验证。
  2. AI 编程:从 Copilot 到 Autopilot: 这篇文章探讨了 AI 编程工具的发展及其对专业程序员和非专业用户的影响。文章讨论了 AI 编程工具的演变,从简单的代码补全插件到更智能的独立 IDE 产品,以及它们如何降低编程门槛,使更多人能够实现软件开发。AI 编程被认为是目前唯一盈利的 AI 赛道,因其显著提高了编程效率,并且能够满足长尾需求。文章还讨论了 AI 编程工具的市场竞争格局及其未来发展趋势。
  3. Koin 最新中文文档来了!官方同步,AI 驱动: Koin 的最新中文文档由 Open AIDoc 项目发布,该项目由社区伙伴 El 发起,旨在提供官方同步的中文技术文档,以解决开发者的阅读痛点。通过 AI 自动化翻译,项目提供多种语言版本,并实时更新以保持与官方一致。
  4. Prompt Engineering: 这篇文章探讨了提示工程(Prompt Engineering),即为大型语言模型设计输入提示的艺术和科学。文章指出,撰写有效提示并不需要成为数据科学家或机器学习工程师,但要使提示最有效可能会很复杂。影响提示效果的因素包括所用模型、模型的训练数据、模型配置、用词选择、风格和语气、结构及上下文。提示工程是一个迭代过程,不当的提示可能导致模糊、不准确的响应。文章还介绍了在 Vertex AI 或通过 API 直接向 Gemini 模型提示的技巧和最佳实践,并讨论了提示设计中可能面临的挑战。
  5. Gemini API 最新进展:Gemini 2.5 Flash & Pro、Live API、Veo 2: 本文介绍了谷歌在 Cloud Next 大会上发布的最新 Gemini API 更新,包括 Gemini 2.5 Flash & Pro、Live API 和 Veo 2。这些更新旨在增强开发者的能力,推动人工智能应用的发展。Gemini 2.5 Pro 是迄今为止性能最佳的 AI 模型,具有强大的思考能力。Veo 2 现已面向生产环境开放,能够生成高质量的视频内容。Live API 提供了动态、实时的互动体验,支持多语言和语音活动检测等功能。
  6. Github Copilot 近期重要更新一览 (2025.4.9): 本文介绍了 GitHub Copilot 近期的重要更新,包括推出 Pro+版本、对高级模型请求次数的限制以及在 VSCode 和 JetBrains IDE 中新增的功能。这些更新旨在提高用户体验和功能多样性。

鸡血

Hacker News 上《我认识的最优秀的程序员》的文章,写得很实在,挺有共鸣。作者总结了他见过的那些顶尖程序员的共同点:

  1. 死磕官方文档 (Read the Reference): 这点我觉得太对了!遇到问题,别老是第一个就跑去 Stack Overflow 或者问 AI,也别瞎猜。大佬们会直接去看官方文档、去看源码。那才是第一手资料,而且很多时候写得比想象中清楚。
  2. 深入理解工具 (Know Your Tools Really Well): 光会用不够,得真正懂你用的技术。不光是知道怎么操作,还要了解它的历史、原理、局限性、生态。这样才能用得好,配置起来也心里有数。
  3. 认真读报错信息 (Read The Error Message): 别扫一眼就过!大佬们会仔细看报错,琢磨里面每个词的意思。作者说,很多时候答案就在报错信息里,看懂了就能解决大部分问题。
  4. 拆解问题 (Break Down Problems): 遇到难题卡壳了怎么办?高手会把大问题拆成一堆小问题,直到每个小问题都好解决。这基本就是程序员的核心工作:拆解。
  5. 不怕“脏活累活” (Don’t Be Afraid To Get Your Hands Dirty): 顶尖的开发者不怕钻研陌生的代码库,不会说“这个我不熟”就不管了。他们就是上手去搞,边搞边学,不知不觉就成了这块的专家。
  6. 持续学习,不瞎跟风 (Never Stop Learning): 技术更新快,大佬们会一直学习新东西,但不是盲目追潮流。他们会认真评估新技术的优劣,如果不用,也能清楚说出为啥,以及替代方案是啥。很多大佬年纪不小了,但思维还是很活跃。
  7. 不耻下问,也不怕说“我不知道” (Don’t Be Afraid to Say “I Don’t Know”): 承认不知道不是丢人的事,反而是学习的开始。大佬们不怕暴露自己的知识边界,而且能坦诚地跟人交流,哪怕对方是刚入行的新人。
  8. 拒绝猜测 (Don’t Guess): 遇到不确定的地方,别凭感觉猜。去查证、去问、去调试。猜对了可能让你养成坏习惯,猜错了直接导致 bug。

投稿指南

欢迎投稿分享您的:

  • 技术博客
  • 实践经验
  • 工具推荐

投稿方式:

  • 公众号后台回复”投稿”
  • 本周刊下面留言
  • 发邮件 :dreamtale.jg@gmail.com
  • 微信联系:Gracker_Gao

关于作者

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 掘金 - Gracker:https://juejin.cn/user/1816846860560749
  2. 知乎 - Grackerhttps://www.zhihu.com/people/gracker
  3. 个人博客 - Android Performance : 写东西的地方
  4. 个人介绍 - 欢迎加微信群组多多交流 :里面有个人的微信和微信群链接。
  5. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  6. 微信公众号 Android Performance
  7. Android 性能优化知识星球 : 个人运营的一个知识星球,欢迎加入,多谢支持~

版权声明

  • 本周刊遵循 CC BY-NC-SA 4.0 协议
  • 转载请注明出处:Android Weekly 第 X 期
  • 欢迎订阅、分享,让更多开发者受益
☑️ ⭐

Android Weekly 2025-12 期

Android Weekly 是一份专注于 Android 技术生态的周刊,每周一更新。本周刊深入挖掘 Android 系统架构、性能优化、跨平台开发、AI 等领域的高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:[微信公众号] | [知乎专栏] | [掘金] | [RSS]

技术文章

  1. Android Vulkan 官宣转正并统一渲染堆栈 ,这对 Flutter 又有什么影响?: Android Vulkan 官宣转正并统一渲染堆栈,这标志着 Vulkan 成为 Android 唯一的 GPU 硬件抽象层(HAL),所有应用和游戏将必须基于 Vulkan 实现。为解决 OpenGL 和 Vulkan 的兼容性问题,ANGLE 被引入作为兼容层。此外,Google 通过 Vulkan 配置文件(VPA)提高一致性,并与 Unity 和联发科合作优化性能。对于 Flutter,Android 的碎片化对 Impeller 的落地带来挑战,但未来可能通过 ANGLE 和 Vulkan 的普及改善这一状况。
  2. 移动 OS 设计之性能设计 3 - 专制型资源管理策略至关重要: 文章探讨了移动操作系统在资源管理和服务质量保障方面的设计策略,强调专制型资源管理的重要性,特别是在面对复杂、多任务的移动环境时。文章通过对比安卓和苹果的设计理念,分析了资源分配、优先级管理以及容量规划等关键策略。
  3. 移动 OS 设计之性能设计 4 - 服务质量信息的传递: 这篇文章的主要内容是基于第三篇文章「移动 OS 设计之性能设计 3 - 专制型资源管理策略至关重要」的内容。在那篇文章中,我提到移动 OS 相较于以往的操作系统,其计算能力和设备资源都有限。因此,移动 OS 的服务质量需要被明确界定。这种服务质量的体现强调的是可预期的表现。在某些业务场景中,服务并不是越快越好,而是在设备资源的限制下,提供符合用户预期的服务即可。在明确了服务质量的划分后,随之会引发其他问题。实际上,所有的技术方案都是如此。在软件领域中没有所谓的“银弹”,即不存在一个万能的解决方案。每一个方案在解决某个问题的同时,往往会引发新的问题。而这些新的问题则需要通过其他方案来解决,直到这些问题被转换为在其他层面上可接受的问题,或者被等价地转换成用户并不关心的问题。
  4. 如何配置 Clion 编写 aosp 的 c++程序: 这篇文章介绍了如何在 Clion 中配置并编写 AOSP(Android 开源项目)的 C++程序,主要适用于无法使用 ASFP IDE 的环境。文章详细说明了使用 AIDEGen 工具生成 CMakeLists 文件的方法,并提供了对 CMake 文件的修改建议及 Clion 索引优化配置步骤。
  5. AI 时代的性能分析:GPU Profiling 初探: 在 CPU 优化的过程中,例如我们遇到 CPU 打满的情况,我们可以通过 perf 等工具进行 Profiling,然后将数据可视化成火焰图等形式进行分析;同样的,在 GPU 的优化过程中,我们也可以通过 Profiling 来进行性能优化。
  6. 再次勇闯稳定性岗位!vivo 社招面经——linux 系统工程师(底软稳定性与性能): 今天再一次给大家带来稳定性方向的面经,希望通过我的面经,让更多的人了解这个“神秘”的岗位,让更多的人了解稳定性到底问什么。
  7. 利用内存页筛选法手撕内存越界行为: 本文详细介绍了如何利用内存页筛选法定位内存越界问题,结合实际案例分析,探讨了内存踩踏的直接原因、间接原因,以及如何通过调整参数解决问题,并对内存分配器 Scudo 的特性进行了深入讨论。
  8. Flutter 新一代状态管理框架 signals ,它究竟具备什么魔法和优势: Flutter 新一代状态管理框架 signals,通过自动状态绑定和依赖追踪实现了高效的状态管理。文章详细分析了 signals 的工作原理、与其他框架的对比以及其在 Flutter 中的应用方式。
  9. 程序员的自我修养 - 第一章 温故而知新: 《程序员的自我修养》第一章通过从底层硬件到上层应用的层层解析,阐述了计算机系统的基本架构、操作系统的核心功能以及多线程与内存管理等关键概念。文章以“温故而知新”为主题,帮助读者理解计算机科学的基础知识,并通过历史发展和现代技术的对比,揭示了技术的演进和核心不变的本质。
  10. 高通 Oryon 处理器微架构分析: 高通浓眉大眼又开始搞起了 CPU 设计,以前没什么反响,基本还是用 ARM 公版的核,自从 2021 年收购了 Nuvia,情况变得不一样了,这个公司是苹果 M1 的团队一些成员出走而创立的公司,原本是要搞服务器的,高通收购后又开始搞 PC 和手机端的核,这个公司的收购对高通的发展影响巨大,Oryon 的成功意味着高通不仅挤入了自研 CPU 的第一梯队,还标志着高通具备了 SoC 上所有核心组件自研的能力,并且都是先进水准,比如基带,GPU 这些。为此还和 ARM 对簿公堂,ARM 最后输掉了官司。今天我们看看这款高通“大核”Oryon 的微架构设计。
  11. 编译器是什么,它又能做什么?: 编译器是将程序员编写的代码翻译为机器码的工具,在安全性、性能优化和开发体验等方面发挥重要作用。文章通过 Ken Thompson 的“编译器幽灵”事件引出编译器的重要性,详细讲述了编译器在安卓系统中的应用及其优化方法,并探讨了 AI 领域对编译器需求的扩展,强调了编译器在现代计算生态中的核心地位。
  12. 聊一聊安卓 WallpaperService 壁纸窗口触摸事件接收原理-第一篇: 在使用 android 手机时候,大家壁纸可能一般都是使用的静态壁纸,静态壁纸一般就是设置一张静态,这种静态壁纸因为是固定的一张图片,所以对壁纸触摸交互这块需求比较少,但是如果设置的是一个动态壁纸,那么这个触摸交互需求就会大大增加。
  13. android studio Debug 安卓 aosp 源码 userdebug 版本看不到局部变量怎么办?: 安卓 aosp 源码 userdebug 版本调试时无法看到局部变量的解决方法,文章分析了问题原因并提供了解决方案,包括使用 eng 版本和修改代码等,同时提出了更彻底的解决方法,通过 eng 版本编译特定模块并覆盖 userdebug 版本的方式来实现局部变量的显示。
  14. Linux 6.10 | CPU 调度: 本文基于 MTK 平台和 Linux Kernel 6.1,围绕 CPU 调度机制进行了分析,重点探讨了唤醒场景下的选核策略、能耗优化方法以及负载均衡等内容。通过多个案例和代码解析,展示了 MTK 平台的调度策略如何在不同场景中实现任务的高效分配,同时也指出了复杂调度策略可能带来的副作用。
  15. LWN:2025 疯狂的 mapcount!: 内核的内存管理子系统必须处理的众多重要任务之一,是跟踪内存页如何映射到系统上运行的进程的地址空间。只要存在到给定页面的映射,就必须保持该页面的存在。事实证明,跟踪这些映射比看起来应该的要困难,并且内存管理子系统内向 folios (页组)的迁移正在增加其自身的复杂性。作为 “mapcount madness” 会议的后续,David Hildenbrand 在 2024 Linux 存储、文件系统、内存管理和 BPF 峰会上发布了 一个补丁系列,旨在改进 folios 的映射计数处理 — 但在某些情况下,精确的跟踪统计仍然难以实现。
  16. 我对 Android Looper 的重新设计: 这篇文章探讨了对 Android Looper 的重新设计,提出通过使用 ConcurrentSkipListSet 跳表优先级队列优化消息插入效率,并引入消息分级机制(高优先级、普通、低优先级、闲置消息)来提升系统性能和 UI 流畅性。设计重点包括优先处理 vsync 等关键 UI 消息、通过 barrier 屏障实现异步处理、优化广播和服务的响应及时性,以及避免低优先级消息饥饿问题,同时建议精简线程以减少资源消耗。
  17. 任务被唤醒后在哪个 CPU 跑?wake_affine 和 select_idle_sibling: 在 Linux 内核中,若任务 A 唤醒任务 B,则 A 被称为 waker;而 B 被称为 wakee。由于 B 原先是睡眠的,现在醒来,它需要找一个 CPU 来跑它,找哪个 CPU 来跑呢?这里是有学问的。唤醒的同时往往伴随着通信(共享数据的访问),比如很可能是 A 在管道、socket 里面写了数据唤醒 B 去读;或者 A 写了一片数据到共享内存,通知 B 去读。这个时候任务 B 醒来后,迁移到 A 所在的 CPU 或者与 A 所在 CPU 最亲近的 CPU 跑,则可能更容易命中 A 写入的 hot-cache(当然也需要综合考虑 B 以前所在的 CPU、A 所在的 CPU,A 与 B 之间的拓扑关系以及 A 和 B 各自 runqueue 的繁忙程度)。
  18. Media3 1.6.0 — what’s new?: Android 开发者博客:Media3 1.6.0 有哪些新功能?
  19. 实战:在 Compose 中优雅地实现提示: 本文详细讲解了如何在 Jetpack Compose 和 Compose Multiplatform 中优雅地实现提示功能,包括背景覆盖、锚点坐标计算、形状剪辑、提示绘制以及提示控制等内容,并提供了完整的代码示例和实现步骤。
  20. Kotlin 中那些无法复现的 Java 写法: Kotlin 对 Java 是 100% 兼容的,正如官方所说。 可即便如此,这就代表 Java 中所有的写法就都能在 Kotlin 中复现出来吗?倒也未必。 今天我就来简单盘点一些我发现的那些在 Kotlin 中无法复现出来的 Java 写法。
  21. 地图之 Compose 轻松绘制,可视化带点击事件,可扩展二次开发: 本文详细介绍了如何在 Android 中使用 Compose 实现地图可视化,包括从 SVG 数据到 Vector.xml 的处理、地图大小和边距的调整、点击事件的实现等,并提供了完整的代码示例与封装好的库供开发者使用。
  22. NativeAllocationRegistry—-通过绑定 Java 对象辅助回收 native 对象内存的机制: 本文详细介绍了 NativeAllocationRegistry 类在 Java 和 Native 内存管理中的应用,尤其是在 Java 对象被垃圾回收后如何自动释放相关的 Native 对象内存。文章分为背景、原理、手动释放和自动释放的具体实现,并提供了代码解析和机制说明。同时提到了一些与此机制相关的应用场景,如 Bitmap 内存释放和 LeakCanary 的内存泄漏检测。
  23. 利用 bytehook 修复 EGL_BAD_ALLOC 异常: 利用 bytehook 修复 Android 低版本(<=Android 9)中因系统 Bug 导致的 EGL_BAD_ALLOC 异常问题,通过 hook 函数解决空 Surface 引发的 fatal 异常,并提供了相关代码实现与分析。
  24. Flutter 知识集锦 | 获取函数调用栈: 本文介绍了 Flutter 开发中如何获取函数调用栈信息的几种方法,包括异常时的调用栈信息、捕捉异常时的调用栈信息,以及在任意位置主动获取调用栈信息的方式。文章强调了函数调用栈信息在问题定位、源码分析及日志记录中的重要作用。
  25. Make WebViews edge-to-edge: 这篇文章介绍了如何在 Android 应用中确保 WebView 与“边到边”显示兼容,特别是在 Android 15 和 16 中强制实施边到边绘制的情况下。文章详细讨论了如何处理 WebView 的插图(Insets),以及如何根据应用是否拥有网页内容来采取不同的实现方法,包括通过 JavaScript 注入插图或调整容器的填充。还提到了如何处理输入法(IME)插图以确保键盘弹出时内容不会被遮挡。
  26. 【笔记】Android 耗时统计: 这篇文章主要介绍了在 Android 开发中如何通过 OnFrameMetricsAvailableListener 和 Choreographer 来监控 UI 性能指标,分析卡顿问题并优化应用流畅度。
  27. 使用 Perfetto 进行流畅度分析: 本文详细介绍了如何使用 Perfetto 工具进行流畅度分析,包括抓取 Trace 文件、分析卡顿原因、使用快捷键提升效率,以及结合日志进行可视化分析等内容。
  28. 关于 Perfetto 分析 Trace 耗时问题排查: 本文详细分析了 Perfetto 工具中 Trace 耗时问题的排查方法,涵盖了主线程、渲染线程、GL 线程、SurfaceFlinger、Display 及性能问题等多个方面的耗时情况,并提供了原因说明及优化建议。
  29. 又解一个 bug - Fragment 异常显示问题: 首页文章分析了一个 Android 开发中关于 Fragment 显示与隐藏的异常问题,并详细探讨了 FragmentTransaction 的 hide 方法实现原理及其潜在的坑点。作者通过案例分享了如何避免类似问题,并提出了设计跨平台框架时的优化建议。
  30. Perfetto 上手指南 2 —— 基础使用: Perfetto 上手指南 2 —— 基础使用,介绍了 Perfetto 的基础操作和界面功能。文章详细说明了如何进入 Perfetto Trace 分析界面、界面基本内容及其操作方法,包括快捷键使用、标记操作、插旗子和 Pin 操作等。文章还对 Trace 内容区的主要元素(如 slice、counter、CPU Sched Slice、thread_state)进行了说明,帮助用户更好地理解和使用 Perfetto 进行性能分析。
  31. 开发 PopTranslate 背后的故事: 图拉鼎开发者分享了新产品 PopTranslate 的开发历程与背后故事。这款 macOS 原生工具以 AI 引擎为核心,提供翻译、解释和重写功能,旨在提升用户效率和体验。文章详细介绍了从前一款产品 MarkMark 的疲惫开发状态到探索新方向的过程,并描述了与妻子的合作、设计开发过程以及未来的展望。
  32. lld 链接器:链接驱动和基本使用: 链接是由源代码构建生成可执行文件的最后一个逻辑环节,它的输出就是可执行文件 EXE 或动态链接库 DSO。
  33. lld 链接器的运行流程(1)——主干流程: 接前文(lld 链接器:链接驱动和基本使用) 对 lld 基本情况的介绍,本文和后续文章介绍 lld 实现的 ELF 格式文件的链接流程
  34. lld 中的重定位:原理、流程与数据结构: 接前文对 lld 核心流程(lld 链接器的运行流程)的介绍,本文继续介绍 lld 对链接中一个核心机制的实现——重定位。
  35. Android×AI 技术周刊 - 第 3 期: 本期周刊既有 Android 生态的更新,也有 AI 领域的开源黑科技与实战指南!无论你是想抢先体验 Android 16 Beta 的能力,还是探索 AI 多模态生成、智能体开发的前沿技术,这里都有不容错过的干货。
  36. Android× 鸿蒙 ×AI 技术周刊 - 第 4 期: Android× 鸿蒙 ×AI 技术周刊 - 第 4 期总结
  37. vLLM 深度解析:production stack: 本文资料来自 vLLM Office Hours 第 21 次会议的记录,时间是 2025 年 3 月 6 日,主题是 vLLM 生产堆栈的深度探讨。主讲人包括 Red Hat 的 Michael Goin 和 Yihua Cheng。会议内容涉及 vLLM 的最新动态、production stack 的架构、性能优化、未来路线图等。
  38. vLLM 深度解析:Deekseek and vLLM -1: 本文资料来自 vLLM Office Hours 第 20 次会议的记录,时间是 2025 年 2 月 27 日,主题是要讨论 vllm 对于 deepseek 的更新,内容丰富。
  39. kotlin-weekly-450: kotlin-weekly-450
  40. Issue #666: Android Weekly 第 666 期,真是 6 ~

杂记

  1. 致敬“普通”的工程师——一位软件工程师对“10 倍工程师”神话的质疑与反思: 本文由 Honeycomb.io 联合创始人兼 CTO Charity Majors 撰写,探讨了“10 倍工程师”这一概念的局限性,并强调团队合作和构建支持普通工程师高效工作的系统的重要性。作者认为,软件开发的真正价值在于团队整体的效率,而非个别工程师的超凡能力。
  2. Age is a problem at Apple: 作者批评苹果董事会和领导团队年龄过于老化,平均年龄分别为 68 岁和 60 岁,缺乏年轻视角和多样性,导致决策与年轻用户需求脱节,例如 AI 产品的设计问题。他呼吁苹果引入“新鲜血液”,平衡经验与创新,以应对技术行业快速变化的挑战,并以 Meta 董事会更年轻的年龄结构为对比,强调年轻领导者的重要性。
  3. 技术管理思考:工作中人性问题: 所有不符合逻辑事情不要干!整体和端到端审视,一定要逻辑自洽。
  4. 科技爱好者周刊#342:面试的 AI 作弊——用数字人去面试: 这里记录每周值得分享的科技内容,周五发布。
  5. 2024 年总结: 这篇文章是作者对 2024 年的总结,内容涵盖了写作、社区建设、生活、工作和新年计划等方面的回顾与展望。作者回顾了过去一年的成就与挑战,并展望了未来的目标,强调了身心健康和生活平衡的重要性。
  6. 我认识的最差程序员: 衡量开发者生产力最棒的一点是,你能快速识别出差劲的程序员。今天我要和你讲讲我所认识的最差程序员,以及为什么我拼了命也要把他留在团队中。
  7. 下一个“N 年思考”: 这篇文章记录了作者在技术领域的职业成长和思考,分享了他在过去几年中围绕技术核心问题所取得的成就,以及对未来技术方向的探索和挑战。他详细描述了在软件工程、DFX 能力建设、跨设备诊断、AI 与软件工程结合等方面的实践与成果,同时提出了一些未解决的问题和未来的努力方向。
  8. AI 时代下的工程领导力:如何打造高效团队 - 来自谷歌工程负责人、Chrome 开发者的宝贵经验分享: 今天偶然读到 Chrome 开发者、Google 工程负责人、著名技术书籍作者 -Addy Osmani 的一篇文章「Leading Effective Engineering Teams in the Age of GenAI」,讲的特别好,对于产品和研发方向如何变得高效,不管你是团队领导者、还是团队成员,都很有价值,分享给朋友们
  9. Leading Effective Engineering Teams in the Age of GenAI: 这篇文章探讨了在生成式人工智能(GenAI)时代,如何有效领导软件工程团队,重点在于技术领导力的演变、AI 工具的使用策略、团队技能提升、以及 AI 技术对工程师职业发展的影响。文章还提供了实际案例研究和未来领导力的方向。
  10. AppStore 首页推荐后,依然月入不足 3000,独立开发两年血泪复盘: 这篇文章是一位独立开发者对其两年独立开发生涯的复盘与反思。尽管曾获得 AppStore 首页推荐并得到用户好评,但因收入不足以维持生计,最终选择放弃全职独立开发。作者分享了失败的原因、独立开发的经验教训,以及对产品推广和商业化的深刻理解,同时也提出了对未来独立开发方向的建议。
  11. 谷歌决定闭源 Android?Fake News!: 谷歌将继续开源 Android,但开发流程将更为私密化。媒体误传谷歌将闭源 Android,实际情况是某些模块的开发早已是私有化的。谷歌此举旨在提高效率和版本发布速度,但会对开源贡献者和技术爱好者产生一定影响。
  12. Google 调整 Android 开源政策,核心开发全面转向私有分支,这将对行业产生什么影响?: 谷歌调整了 Android 开源政策,其核心开发全面转向私有分支。这一变化对手机厂商、应用开发者、技术爱好者以及没有与谷歌合作的设备厂商产生了不同程度的影响。主要原因可能包括降低成本、提高效率以及 Android 生态的逐步成熟。
  13. 给 IT 年轻人职业建议 5(在工作中找到乐趣): 一个人职业生涯中,工作和兴趣完全匹配几乎不存在。工作中往往是一些枯燥繁琐的事情,如改不完的软件的 BUG,写不完的汇报材料,还有开不完的低效会议,及其周边团队无意义的推卸拉扯。技术人员更喜欢安静的做做设计,写写代码,读读代码,学学新技术等。理想的工作是不存在的,那就需要在日常工作中找到一些乐趣。怎么找到乐趣其实一句话就是学会“上下求索,好奇心”。

AI

  1. 关于 DeepSeek 我是怎么研究的(5): 文章详细介绍了 DeepSeek-R1 模型的研究背景、训练过程和性能评估,阐述了其通过结合强化学习和冷启动数据的微调来提升推理能力,并在多个基准测试中表现优异。
  2. 4 段超神提示词解锁 Claude 3.7 能力上限: Claude 在升级到 3.7 之后,能力获得了大幅提升,在前端编程方面已经远远超出其他模型,重新定义了 AI PPT 的概念,我已经把所有图表都交给 Claude 3.7 用代码直接写了。最近一直想把方法分享给大家,但苦于没有精力动笔,刚好我的朋友归藏做了一些研究,向阳乔木做了一些延展,写出了这篇很棒的文章。
  3. DeepSeek 赋能 A 股交易:用 AI 让你快人一步!: 用 AI 赋能 A 股交易:DeepSeek 工具解析市场动态,助你快人一步!
  4. sonnylazuardi/cursor-talk-to-figma-mcp: 此项目实现了 Cursor AI 与 Figma 之间的模型上下文协议(MCP)集成,允许 Cursor 以编程方式读取和修改 Figma 设计。提供了详细的安装、使用指南以及功能工具列表,用于设计文档操作、元素创建、样式设置、布局调整等。
  5. [译]AI 计算民主化 第一部分:DeepSeek 对 AI 的影响: DeepSeek 对 AI 的影响 探讨了 DeepSeek 技术如何通过创新方法减少对昂贵硬件的依赖,推动 AI 算力民主化。文章回顾了作者 Chris Lattner 的职业生涯,强调了软硬件协同设计、共享基础设施的重要性,并提出解决 AI 算力生态挑战的思路。文章还是系列内容的开篇,核心围绕 CUDA 技术及其影响展开深入探讨。
  6. [译]AI 算力民主化 第二部分:究竟何为“CUDA”?: 本文是“AI 算力民主化”系列的第二部分,深入解析了 CUDA 的技术内涵、历史演进及其在现代 AI 计算中的核心地位。文章详细阐述了 CUDA 作为一个完整生态系统的多层次架构,并探讨了其发展历程及对 AI 领域的深远影响。
  7. [译]AI 算力民主化 第三部分:CUDA 是如何成功的?: 本文深入解析了 CUDA 在 GPU 计算领域取得统治地位的原因,探讨了其技术优势、战略执行、生态系统锁定效应以及 AI 发展的历史机遇。文章还分析了英伟达如何通过软硬件深度耦合、跨代兼容性和开发者生态建设,成功巩固了 CUDA 在深度学习、生成式 AI 等领域的核心地位。
  8. [译]AI 算力民主化 第四部分:CUDA 虽为现有主导者,但它真的完美吗?: 本文探讨了 CUDA 作为 AI 计算领域的主导技术,其优势与局限性。从开发者视角分析了 CUDA 对 AI 工程师、性能优化工程师以及跨平台开发者的影响,同时指出了其复杂性、技术债务和供应商锁定效应对未来 AI 发展的潜在挑战。文章还提及了英伟达如何利用 CUDA 巩固其市场地位,但也面临创新与兼容性之间的矛盾。
  9. Perfetto 快速上手指南 1 —— Trace 的抓取: Perfetto 是 google 从 Android10 开始引入的一个全新的平台级跟踪分析工具。它可以记录 Android 系统运行过程中的关键数据,并通过图形化的形式展示这些数据。Perfetto 不仅可用于系统级的性能分析,也是我们学习系统源码流程的好帮手。
  10. [译]AI 算力民主化 第五部分:CUDA C++替代方案(如 OpenCL)现状如何?: 本文探讨了 CUDA C++替代方案(如 OpenCL)的现状,分析了其技术优势、局限性以及未能成为 AI 计算领域主导平台的原因。文章详细阐述了 OpenCL 的历史背景、技术缺陷、行业竞争与协作的挑战,以及与现代 AI 需求之间的脱节。同时,文章对 NVIDIA 通过 CUDA 与 AI 框架的协同设计所取得的成功进行了对比,总结了成功系统的关键要素,并提出了对未来 AI 编译器发展的思考。
  11. [译]AI 算力民主化 第六部分:TVM 和 XLA 等 AI 编译器现状?: 本文探讨了 AI 编译器(如 TVM 和 XLA)的现状及其在 AI 算力民主化中的角色,分析了其技术优势与局限性,同时对生成式 AI 的兴起如何改变 AI 编译器需求进行了深入剖析。文章还引用了 TVM 和 XLA 的经验教训,并提到了 MLIR 和 Triton 等新兴技术可能带来的突破。
  12. 全新 DeepSeek V3 发布,代码能力大幅提升: DeepSeek V3 更新 0324 版本,应该是基于 V3 继续训练的产物。
  13. 在 ChatWise 中使用 MCP 工具: MCP (Model Context Protocol),也就是模型上下文协议,是一个让模型访问外部资源的规范,由 Claude 的开发商 Anthropic 提出。开发者可以根据 MCP 规范实现一些服务 (server),然后模型可以通过这些服务获取额外的能力和上下文,比如使用网络搜索、操控外部浏览器等等。
  14. “聊天式编程”让代码听你的话:Cursor 打造极致心流体验: 最近半年深度体验了 Cursor,享受到了很多乐趣,这篇文章将分享对于 Cursor 的一些实践与思考。
  15. 闲谈丨一名 AI 体验者的自述: 这篇文章是一名 AI 体验者的自述,分享了 AI 技术对其生活和工作的深刻影响,以及如何通过 AI 提升效率、解决问题。同时文章也探讨了 AI 的局限性以及如何正确使用 AI 的思考方式。
  16. Android 使用 Edge-to-Edge 实现沉浸式状态栏详解: 在 Android 中实现 Edge-to-Edge 布局(内容延伸到状态栏和导航栏下方)并适配不同版本、刘海屏/挖孔屏设备,需要综合处理系统栏的显示、颜色、安全区域和兼容性问题。以下是分步骤的完整实现方案

书籍推荐

Android 性能优化之道

这是一套从 Android 性能优化本质入手,指导读者实现从硬件层到操作系统层再到应用层全面优化的实战方法论。本书由 Android 方向 Google 开发者专家撰写,融合了作者 10 年大厂实战经验,其中不仅包括作者实操过的监控、优化、防劣化等方向的各种典型案例,还包括多个实战小技巧,可以帮助读者解决工作中遇到的 90%以上的能优化问题。
本书内存、速度和流畅性、稳定性、包体积、耗电、磁盘占用、流量、降级这 8 个方向的性能优化内容。这些内容方向均从原理和实战两个维度进行解读。其中,原理部分直指优化的本质,不仅包括相关基础知识,还包括性能优化的底层逻辑;实战部分以指导读者实操为主要目标,以案例为主要讲解形式,深度解读作者精心总结的各种实战案例中用到的技术和原理。本书基于 Android 14 撰写,但也会涉及 Android 14 以外的其他 Android 版本的源码。

jd:Android 性能优化之道

打通 Linux 操作系统和芯片开发

为什么选择写打通操作系统和芯片开发的内容?我们知道计算机是个变化极快的行业,特别是从事互联网行业的朋友,经常面对技术的更新,开发语言的迭代,每天过的都很焦虑,随着新人的入职,技术的变化,老人的技术经验似乎无法得到发挥,这也是为什么都说程序员有 35 岁失业的根本原因。那么技术更新不那么快的行业是不是就好点了呢?的确如此,比如更加底层的嵌入式行业,操作系统行业,芯片行业等都会比互联网行业好很多,特别是同时懂软件和硬件的工程师,甚至随着时间的推移,越老越吃香,而且国家越来越重视底层技术的开发。即便是在互联网行业,如果你对底层技术有着深厚的积累,依然可以很有竞争力,就相当是拥有了武侠片中的内功,一旦有了雄厚的内功,其它武功你一看就明白,一学就会,任何招式你和别人打出去的威力就不是一个级别。这种帮助无论对嵌入式开发者,还是对互联网程序员都是非常明显的。

jd:打通 Linux 操作系统和芯片开发

鸡血

星球

满 500 人了,顺便打个广告。这段时间忙完了,开始要系统性输出了~ 也欢迎加入星球的同学预约一起看 Trace ~

投稿指南

欢迎投稿分享您的:

  • 技术博客
  • 实践经验
  • 工具推荐

投稿方式:

  • 公众号后台回复”投稿”
  • 本周刊下面留言
  • 发邮件 :dreamtale.jg@gmail.com
  • 微信联系:Gracker_Gao

关于作者

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 掘金 - Gracker:https://juejin.cn/user/1816846860560749
  2. 知乎 - Grackerhttps://www.zhihu.com/people/gracker
  3. 个人博客 - Android Performance : 写东西的地方
  4. 个人介绍 - 欢迎加微信群组多多交流 :里面有个人的微信和微信群链接。
  5. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  6. 微信公众号 Android Performance
  7. Android 性能优化知识星球 : 个人运营的一个知识星球,欢迎加入,多谢支持~

版权声明

  • 本周刊遵循 CC BY-NC-SA 4.0 协议
  • 转载请注明出处:Android Weekly 第 X 期
  • 欢迎订阅、分享,让更多开发者受益
🔲 ⭐

Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程

本文介绍了 App 开发者不经常接触到但在 Android Framework 渲染链路中非常重要的一个类 Choreographer,包括 Choreographer 的引入背景、简介、部分源码解析、与 MessageQueue 的交互、在 APM 中的应用,以及手机厂商基于 Choreographer 的一些优化思路。

Choreographer 的引入主要是配合 Vsync,为上层应用的渲染提供稳定的 Message 处理时机。当 Vsync 信号到来时,系统通过对 Vsync 信号周期的调整,控制每一帧绘制操作的时机。目前主流手机的屏幕刷新率已达到 120Hz,即每 8.3ms 刷新一次,系统为配合屏幕刷新频率,相应调整 Vsync 周期。每个 Vsync 周期到来时,Vsync 信号唤醒 Choreographer 执行应用的绘制操作,这正是引入 Choreographer 的主要作用。了解 Choreographer 还可以帮助应用开发者深入理解每一帧的运行原理,同时加深对 MessageHandlerLooperMessageQueueInputAnimationMeasureLayoutDraw 等核心组件的认识。许多 APM(应用性能监控)工具也利用了 Choreographer(通过 FrameCallback)、FrameMetrics/gfxinfo framestats(底层依赖 FrameInfo)、MessageQueue(通过 IdleHandler)和 Looper(通过自定义 MessageLogging)这些组合机制进行性能监测。深入理解这些机制后,开发者可以更有针对性地进行性能优化,形成系统化的优化思路。

本文目录

本文是 Perfetto 系列文章的第五篇,主要是对 Perfetto 中的 Choreographer 进行简单介绍

本系列的目的是通过 Perfetto 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Perfetto 这个图形化的角度,你可以理解的更深入一些。

Perfetto 系列目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 本文的 Systrace 版本:Android 基于 Choreographer 的渲染机制详解
  3. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

主线程运行机制的本质

在讲 Choreographer 之前,我们先理一下 Android 主线程运行的本质,其实就是 Message 的处理过程,我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息,如下图所示

MethodTrace 图示

Perfetto 图示

image-20250331235337987

演进

引入 Vsync 之前的 Android 版本,渲染一帧相关的 Message ,中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,不稳定,如下图

MethodTrace 图示

Trace 图示(资源比较老,用 Systrace 图示)

可以看到这时候的瓶颈是在 dequeueBuffer, 因为屏幕是有刷新周期的, FB 消耗 Front Buffer 的速度是一定的, 所以 SF 消耗 App Buffer 的速度也是一定的, 所以 App 会卡在 dequeueBuffer 这里,这就会导致 App Buffer 获取不稳定, 很容易就会出现卡顿掉帧的情况.

对于用户来说,稳定的帧率才是好的体验,比如你玩王者荣耀,相比 fps 在 60 和 40 之间频繁变化,用户感觉更好的是稳定在 50 fps 的情况.

所以 Android 的演进中,最初引入了 Vsync + TripleBuffer + Choreographer 的机制,后来从 Android R (Android 11) 开始引入并持续完善 BlastBufferQueue,共同构成了现代 Android 稳定帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。

引入 Choreographer

Choreographer 的引入主要是配合 Vsync,为上层应用的渲染提供稳定的 Message 处理时机。当 Vsync 信号到来时,系统通过对 Vsync 信号周期的调整,控制每一帧绘制操作的时机。目前主流手机的屏幕刷新率已达到 120Hz,即每 8.3ms 刷新一次,系统为配合屏幕刷新频率,相应调整 Vsync 周期。每个 Vsync 周期到来时,Vsync 信号唤醒 Choreographer 执行应用的绘制操作,如果每个 Vsync 周期应用都能渲染完成,那么应用的 fps 就是120,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用

当然目前主流旗舰手机的刷新率已达到120Hz,Vsync周期已缩短至8.3ms,上图中的操作要在更短的时间内完成,对性能的要求也越来越高,具体可以看新的流畅体验,90Hz 漫谈 这篇文章(文章虽然讨论90Hz,但同样的原理适用于120Hz)

Choreographer 简介

Choreographer 扮演 Android 渲染链路中承上启下的角色

  1. 承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理)、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作),判断卡顿掉帧情况,记录 CallBack 耗时等
  2. 启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync);请求 Vsync(FrameDisplayEventReceiver.scheduleVsync)

从上面可以看出,Choreographer 在 Android 渲染管线中扮演关键的协调者角色,其重要性在于通过 Choreographer + SurfaceFlinger + Vsync + BlastBufferQueue 这一套完整的渲染机制,确保 Android 应用能够以稳定的帧率运行(60 fps、90 fps 或 120 fps),有效减少帧率波动带来的视觉不适感。

了解 Choreographer 还可以帮助应用开发者深入理解每一帧的运行原理,同时加深对 MessageHandlerLooperMessageQueueInputAnimationMeasureLayoutDraw 等核心组件的认识。许多 APM(应用性能监控)工具也利用了 Choreographer(通过 FrameCallback)、FrameMetrics/gfxinfo framestats(底层依赖 FrameInfo)、MessageQueue(通过 IdleHandler)和 Looper(通过自定义 MessageLogging)这些组合机制进行性能监测。深入理解这些机制后,开发者可以更有针对性地进行性能优化,形成系统化的优化思路。

另外,虽然图表是解释流程的有效方式,但本文将更多依赖 Perfetto 和 MethodTrace 工具的可视化输出。Perfetto 以时间线方式(从左到右)展示整个系统的运行状况,涵盖 CPU、SurfaceFlinger、SystemServer 和应用进程等关键组件的活动。使用 PerfettoMethodTrace 可以直观展示关键执行流程,当您熟悉系统代码后,Perfetto 的输出能够直接映射到设备的实际运行状态。因此,本文除了引用少量网络图表外,主要依靠 Perfetto 来展示分析结果。

从 Perfetto 的角度来看 Choreographer 的工作流程

下图以滑动设置界面为例子,我们先看一下从上到下设置界面的一个完整的预览图,可以看到 Perfetto 中从左到右,每一个绿色的帧都表示一帧,表示最终我们可以手机上看到的画面

  1. 图中每一个 VSYNC-app 的值的区间是一个 Vsync 的时间,对应当前设备的刷新率,如 60Hz 时为 16.6ms,120Hz时为8.3ms,上升沿或者下降沿就是 Vsync 到达的时间
  2. 每一帧处理的流程:接收到 Vsync 信号回调-> UI Thread –> RenderThread –> SurfaceFlinger
  3. UI Thread 和 RenderThread 就可以完成 App 一帧的渲染,在 Android 11 及以上版本中,会通过 BlastBufferQueue 路径把渲染完的 Buffer 提交给 SurfaceFlinger 去合成,然后我们就可以在屏幕上看到这一帧
  4. 可以看到 Settings 滑动的每一帧耗时都很短(Ui Thread 耗时 + RenderThread 耗时),但是由于 Vsync 的存在,每一帧都会等到 Vsync 才会去做处理

image-20250401001138400

有了上面这个整体的概念,我们将 UI Thread 的每一帧放大来看,看看 Choreographer 的位置以及 Choreographer 是怎么组织每一帧的

image-20250401002047386

Choreographer 的工作流程

  1. Choreographer 初始化
  2. 初始化 FrameHandler,绑定 Looper
  3. 初始化 FrameDisplayEventReceiver,与 SurfaceFlinger 建立通信用于接收和请求 Vsync
  4. 初始化 CallBackQueues
  5. SurfaceFlinger 的 appEventThread 唤醒发送 Vsync,Choreographer 回调 FrameDisplayEventReceiver.onVsync,进入 Choreographer 的主处理函数 doFrame
  6. Choreographer.doFrame 计算掉帧逻辑
  7. Choreographer.doFrame 处理 Choreographer 的第一个 callback:input
  8. Choreographer.doFrame 处理 Choreographer 的第二个 callback:animation
  9. Choreographer.doFrame 处理 Choreographer 的第三个 callback:insets animation
  10. Choreographer.doFrame 处理 Choreographer 的第四个 callback:traversal
  11. traversal-draw 中 UIThread 与 RenderThread 同步数据
  12. Choreographer.doFrame 处理 Choreographer 的第五个 callback:commit
  13. RenderThread 处理绘制命令
  14. 在 Android 11 及以上版本中,RenderThread 通过 BlastBufferQueue 向 SurfaceFlinger 提交绘制内容(不同版本细节持续演进)
  15. BlastBufferQueue 由 App 端创建和管理
  16. 通过生产者 (BBQ_BufferQueue_Producer) 和消费者 (BufferQueue_Consumer) 模型工作
  17. UI 线程等待 RenderThread 的概率与时长通常会降低,可以更早地准备下一帧(仍取决于具体同步点)

第一步初始化完成后,后续就会在步骤 2-10 之间循环

同时也附上这一帧所对应的 MethodTrace(这里预览一下即可,下面会有详细的大图)

Choreographer 与 RenderThread 及 BlastBufferQueue 的交互

从 Android R (Android 11) 开始,RenderThread 与 SurfaceFlinger 之间的交互发生了重要变化,其核心是 BlastBufferQueue 的引入与后续演进。下面我们来看看这一机制是如何工作的。

BlastBufferQueue 工作原理

BlastBufferQueue 是一个专为 UI 渲染优化的 BufferQueue 变体,它替代了传统由 SurfaceFlinger 创建的 BufferQueue,转为由 App 端创建和管理,用于 App 与 SurfaceFlinger 之间的缓冲区管理。

  1. 更高效的缓冲区管理
    在传统的 BufferQueue 中,App 需要通过 dequeueBuffer 获取一个可用的缓冲区,然后渲染内容,最后通过 queueBuffer 将缓冲区提交给 SurfaceFlinger。这个过程中,如果没有可用的缓冲区,App 需要等待,这会导致阻塞。

    BlastBufferQueue 通过更智能的缓冲区管理,减少了这种等待,特别是在高刷新率设备上,效果更明显。

  2. RenderThread 与 UI 线程的解耦
    在较老实现中,UI 线程与 Buffer 提交路径耦合更紧,主线程更容易在关键同步点发生等待。引入 BlastBufferQueue 并持续优化后,UI 线程通常可以更早完成本帧 Java 侧工作并进入下一帧准备,从而降低主线程阻塞概率(是否阻塞仍取决于具体同步点与场景)。

  3. 创建机制
    BlastBufferQueue 在 ViewRootImpl 的 relayoutWindow 过程中创建:

    1
    2
    3
    4
    5
    6
    // 创建 BBQ 的示例代码
    if (mBlastBufferQueue == null) {
    mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
    mSurfaceSize.x, mSurfaceSize.y,
    mWindowAttributes.format);
    }
  4. 与 Choreographer 的配合
    Choreographer 仍然是协调这一切的核心。当 Vsync 信号到来时,Choreographer 会触发 doFrame,执行各种回调,其中 CALLBACK_TRAVERSAL 会触发 ViewRootImpl 的 performTraversals(),最终走到 draw 流程。

    在 draw 流程中,通过 BlastBufferQueue,RenderThread 可以更独立地工作,而 UI 线程可以更早地返回处理其他任务。

下面我们就从源码的角度,来看一下具体的实现

源码解析

下面从源码的角度来简单看一下,源码只摘抄了部分重要的逻辑,其他的逻辑则被剔除,另外 Native 部分与 SurfaceFlinger 交互的部分也没有列入,不是本文的重点,有兴趣的可以自己去跟一下。下面的源码基于 AOSP mainline(frameworks/base HEAD,核对日期:2026-02)实现,不同 Android release 的细节可能会有差异。

Choreographer 的初始化

Choreographer 的单例初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 获取当前线程的 Looper
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
// 构造 Choreographer 对象,使用 VSYNC_SOURCE_APP 作为Vsync源
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};

Choreographer 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
// 1. 初始化 FrameHandler
mHandler = new FrameHandler(looper);
// 2. 初始化 DisplayEventReceiver
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//3. 初始化 CallbacksQueues
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
......
}

FrameHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final class FrameHandler extends Handler {
......
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME://开始渲染下一帧的操作
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC://请求 Vsync
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK://处理 Callback
doScheduleCallback(msg.arg1);
break;
}
}
}

Choreographer 初始化链

在 Activity 启动过程,执行完 onResume 后,会调用 Activity.makeVisible(),然后再调用到 addView(), 层层调用会进入如下方法

1
2
3
4
5
6
7
8
9
ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) 
-->WindowManagerImpl.addView(View, LayoutParams) (android.view)
-->WindowManagerGlobal.addView(View, LayoutParams, Display, Window) (android.view)
-->ViewRootImpl.ViewRootImpl(Context, Display) (android.view)
public ViewRootImpl(Context context, Display display) {
......
mChoreographer = Choreographer.getInstance();
......
}

FrameDisplayEventReceiver 简介

Vsync 的注册、申请、接收都是通过 FrameDisplayEventReceiver 这个类,所以可以先简单介绍一下。 FrameDisplayEventReceiver 继承 DisplayEventReceiver , 有三个比较重要的方法

  1. onVsync – Vsync 信号回调
  2. run – 执行 doFrame
  3. scheduleVsync – 请求 Vsync 信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
......
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame, VsyncEventData eventData) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
// VsyncEventData 在 Android 13/14 已存在,较新版本继续扩展了可用字段
mVsyncEventData = eventData;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mVsyncEventData);
}

public void scheduleVsync() {
......
nativeScheduleVsync(mReceiverPtr);
......
}
}

Choreographer 中 Vsync 的注册

从下面的函数调用栈可以看到,Choreographer 的内部类 FrameDisplayEventReceiver.onVsync 负责接收 Vsync 回调,通知 UIThread 进行数据处理。

那么 FrameDisplayEventReceiver 是通过什么方式在 Vsync 信号到来的时候回调 onVsync 呢?答案是 FrameDisplayEventReceiver 的初始化的时候,最终通过监听文件句柄的形式,其对应的初始化流程如下

android/view/Choreographer.java

1
2
3
4
5
6
7
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
......
}

android/view/Choreographer.java

1
2
3
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

android/view/DisplayEventReceiver.java

1
2
3
4
5
6
public DisplayEventReceiver(Looper looper, int vsyncSource) {
......
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
}

简单来说,FrameDisplayEventReceiver 的初始化过程中,通过 BitTube (本质是一个 socket pair),来传递和请求 Vsync 事件,当 SurfaceFlinger 收到 Vsync 事件之后,通过 appEventThread 将这个事件通过 BitTube 传给 DisplayEventDispatcher,DisplayEventDispatcher 通过 BitTube 的接收端监听到 Vsync 事件之后,回调 Choreographer.FrameDisplayEventReceiver.onVsync,触发开始一帧的绘制,如下图

DisplayEventReceiver 与 SurfaceFlinger 的通信细节

在 Android 系统中,DisplayEventReceiver 通过 JNI 调用 nativeInit 方法来建立与 SurfaceFlinger 服务的通信通道。这个过程涉及多个关键步骤:

  1. 创建 NativeDisplayEventReceiver 对象:在 Java 层调用 nativeInit 后,JNI 创建一个 NativeDisplayEventReceiver 实例,用于接收 Vsync 信号。

  2. 获取 IDisplayEventConnection:通过 ISurfaceComposer 接口获取 IDisplayEventConnection,这是一个 Binder 接口,用于与 SurfaceFlinger 服务通信。

  3. 建立 BitTube 连接:BitTube 是一个基于 socket pair 的通信机制,专为高频、小数据量的跨进程通信设计。它在 App 进程和 SurfaceFlinger 进程之间创建一个高效的通信通道。

  4. 文件描述符监听:通过 Looper 监听 BitTube 的文件描述符,当有 Vsync 信号到来时,Looper 会通知 DisplayEventDispatcher 处理事件。

整个通信流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
App 进程                                    SurfaceFlinger 进程
| |
|-- 创建 DisplayEventReceiver -------------->|
| |
|<- 返回 IDisplayEventConnection (Binder) ----|
| |
|-- 创建 BitTube -------------------------->|
| |
|<- 文件描述符交换 --------------------------|
| |
|-- 注册文件描述符到 Looper ----------------|
| |
|-- 请求 Vsync (requestNextVsync) ---------->|
| |
|<- 发送 Vsync 事件数据 ---------------------|
| |
|-- Looper 通知 -> handleEvent -------------|
| |
|-- 回调 Java 层 onVsync -------------------|
| |

这种设计的优势在于避免了使用 Binder 传递高频的 Vsync 事件数据,通过直接的 socket 通信提高了性能和实时性,这对于保证流畅的 UI 渲染至关重要。同时,由于 BitTube 使用了文件描述符,可以无缝集成到 Android 的 Looper 机制中,使得整个系统能够以事件驱动的方式工作。

Choreographer 处理一帧的逻辑

Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理

android/view/Choreographer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onVsync(long timestampNanos, long physicalDisplayId, int frame, VsyncEventData eventData) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
mVsyncEventData = eventData;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mVsyncEventData);
}

doFrame 函数主要做下面几件事

  1. 计算掉帧逻辑
  2. 记录帧绘制信息
  3. 执行 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_INSETS_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT

记录帧绘制信息

Choreographer 中 FrameInfo 来负责记录帧的绘制信息,doFrame 执行的时候,会把每一个关键节点的绘制时间记录下来,我们使用 dumpsys gfxinfo 就可以看到。当然 Choreographer 只是记录了一部分,剩余的部分在 hwui 那边来记录。

从 FrameInfo 这些标志就可以看出记录的内容,后面我们看 dumpsys gfxinfo 的时候数据就是按照这个来排列的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

public @interface FrameInfoFlags {}

public static final int FRAME_TIMELINE_VSYNC_ID = 1;

// The intended vsync time, unadjusted by jitter
public static final int INTENDED_VSYNC = 2;

// Jitter-adjusted vsync time, this is what was used as input into the
// animation & drawing system
public static final int VSYNC = 3;

// The id of the input event that caused the current frame
public static final int INPUT_EVENT_ID = 4;

// When input event handling started
public static final int HANDLE_INPUT_START = 5;

// When animation evaluations started
public static final int ANIMATION_START = 6;

// When ViewRootImpl#performTraversals() started
public static final int PERFORM_TRAVERSALS_START = 7;

// When View:draw() started
public static final int DRAW_START = 8;

// When the frame needs to be ready by
public static final int FRAME_DEADLINE = 9;

// When frame actually started.
public static final int FRAME_START_TIME = 10;

// Interval between two consecutive frames
public static final int FRAME_INTERVAL = 11;

doFrame 函数记录从 Vsync time 到 markPerformTraversalsStart 的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
void doFrame(long frameTimeNanos, int frame, VsyncEventData eventData) {
......
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
// 处理 CALLBACK_INPUT Callbacks
mFrameInfo.markInputHandlingStart();
// 处理 CALLBACK_ANIMATION Callbacks
mFrameInfo.markAnimationsStart();
// 处理 CALLBACK_INSETS_ANIMATION Callbacks
// 处理 CALLBACK_TRAVERSAL Callbacks
mFrameInfo.markPerformTraversalsStart();
// 处理 CALLBACK_COMMIT Callbacks
......
}

执行 Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void doFrame(long frameTimeNanos, int frame, VsyncEventData eventData) {
......
// 处理 CALLBACK_INPUT Callbacks
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 处理 CALLBACK_ANIMATION Callbacks
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 处理 CALLBACK_INSETS_ANIMATION Callbacks
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
// 处理 CALLBACK_TRAVERSAL Callbacks
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
// 处理 CALLBACK_COMMIT Callbacks
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
......
}

Input 回调调用栈

input callback 一般是执行 ViewRootImpl.ConsumeBatchedInputRunnable

android/view/ViewRootImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class ConsumeBatchedInputRunnable implements Runnable {
@Override
public void run() {
doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
}
}
void doConsumeBatchedInput(long frameTimeNanos) {
if (mConsumeBatchedInputScheduled) {
mConsumeBatchedInputScheduled = false;
if (mInputEventReceiver != null) {
if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos)
&& frameTimeNanos != -1) {
scheduleConsumeBatchedInput();
}
}
doProcessInputEvents();
}
}

Input 时间经过处理,最终会传给 DecorView 的 dispatchTouchEvent,这就到了我们熟悉的 Input 事件分发

Animation 回调调用栈

一般我们接触的多的是调用 View.postOnAnimation 的时候,会使用到 CALLBACK_ANIMATION

1
2
3
4
5
6
7
8
9
10
11
public void postOnAnimation(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.mChoreographer.postCallback(
Choreographer.CALLBACK_ANIMATION, action, null);
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().post(action);
}
}

那么一般是什么时候回调用到 View.postOnAnimation 呢,我截取了一张图,大家可以自己去看一下,接触最多的应该是 startScroll,Fling 这种操作

其调用栈根据其 post 的内容,下面是松手之后的 fling 动画。

另外我们的 Choreographer 的 FrameCallback 也是用的 CALLBACK_ANIMATION

1
2
3
4
5
6
7
8
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

Traversal 调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//为了提高优先级,先 postSyncBarrier
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 真正开始执行 measure、layout、draw
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 这里把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 真正开始
performTraversals();
}
}
private void performTraversals() {
// measure 操作
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// layout 操作
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
// draw 操作
if (!cancelDraw && !newSurface) {
performDraw();
}
}

doTraversal 的 TraceView 示例

下一帧的 Vsync 请求

由于动画、滑动、Fling 这些操作的存在,我们需要一个连续的、稳定的帧率输出机制。这就涉及到了 Vsync 的请求逻辑,在连续的操作,比如动画、滑动、Fling 这些情况下,每一帧的 doFrame 的时候,都会根据情况触发下一个 Vsync 的申请,这样我们就可以获得连续的 Vsync 信号。

看下面的 scheduleTraversals 调用栈(scheduleTraversals 中会触发 Vsync 请求)

我们比较熟悉的 invalidate 和 requestLayout 都会触发 Vsync 信号请求

我们下面以 Animation 为例,看看 Animation 是如何驱动下一个 Vsync ,来持续更新画面的

ObjectAnimator 动画驱动逻辑

android/animation/ObjectAnimator.java

1
2
3
public void start() {
super.start();
}

android/animation/ValueAnimator.java

1
2
3
4
5
6
7
8
9
private void start(boolean playBackwards) {
......
addAnimationCallback(0); // 动画 start 的时候添加 Animation Callback
......
}
private void addAnimationCallback(long delay) {
......
getAnimationHandler().addAnimationFrameCallback(this, delay);
}

android/animation/AnimationHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
// post FrameCallback
getProvider().postFrameCallback(mFrameCallback);
}
......
}

// 这里的 mFrameCallback 回调 doFrame,里面 post了自己
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
// post 自己
getProvider().postFrameCallback(this);
}
}
};

调用 postFrameCallback 会走到 mChoreographer.postFrameCallback ,这里就会触发 Choreographer 的 Vsync 请求逻辑

android/animation/AnimationHandler.java

1
2
3
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

android/view/Choreographer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
// 请求 Vsync scheduleFrameLocked ->scheduleVsyncLocked-> mDisplayEventReceiver.scheduleVsync ->nativeScheduleVsync
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

通过上面的 Animation.start 设置,利用了 Choreographer.FrameCallback 接口,每一帧都去请求下一个 Vsync
动画过程中一帧的 TraceView 示例

源码小结

  1. Choreographer 采用线程单例模式设计,与Looper强耦合。每个线程只能拥有一个Choreographer实例,且必须绑定一个有效的Looper对象,因为其内部Handler依赖Looper进行消息分发。在应用中,通常绑定主线程的Looper以确保UI操作的线程安全性。

  2. DisplayEventReceiver 是一个抽象基类,其JNI实现创建IDisplayEventConnection对象作为Vsync信号的监听器。通过此机制,SurfaceFlinger的AppEventThread发出的Vsync中断信号能够被精确传递到Choreographer实例。当Vsync信号到达时,系统回调DisplayEventReceiver的onVsync方法,触发渲染流程。

  3. DisplayEventReceiver 提供scheduleVsync方法用于请求Vsync信号。应用程序需要更新UI时,先通过此方法申请下一个Vsync中断,然后在onVsync回调中执行实际的绘制逻辑,确保渲染与屏幕刷新同步。

  4. Choreographer 定义了FrameCallback接口,其doFrame方法在每次Vsync到来时被调用。这一设计对Android动画系统具有重要意义,使动画能够与屏幕刷新率精确同步,相比早期自行计时的实现,提供了更加流畅、省电的动画体验。

  5. Choreographer 核心功能是接收Vsync信号并触发通过postCallback注册的回调函数。框架定义了五种类型的回调,按照执行优先级排序:

    1. CALLBACK_INPUT:处理输入事件,如触摸、按键等交互
    2. CALLBACK_ANIMATION:处理各类动画计算与更新
    3. CALLBACK_INSETS_ANIMATION:处理系统插入动画,如软键盘、状态栏动画等
    4. CALLBACK_TRAVERSAL:处理视图树的测量、布局与绘制
    5. CALLBACK_COMMIT:执行 post-draw 收尾工作,并在必要时修正 frame time(用于更准确地反映延迟与抖动)
  6. ListViewRecyclerView 的Item复用机制(ViewHolder模式)在框架层面上的具体实现会涉及到CALLBACK_INPUT和CALLBACK_ANIMATION阶段。在滑动或快速滚动时,Item的初始化、测量与绘制可能在Input回调中触发(如直接响应触摸事件),也可能在Animation回调中执行(如惯性滑动或自动滚动)。RecyclerView通过更高效的复用机制和预取(Prefetch)策略,能够在这两个阶段更智能地准备ViewHolder,减少主线程阻塞,尤其在高刷新率设备上表现更为出色。

  7. CALLBACK_INPUTCALLBACK_ANIMATION 在执行过程中会修改View的各种属性(如位置、透明度、变换矩阵等),因此必须先于CALLBACK_TRAVERSAL执行,以确保所有状态更新都能在当前帧的测量、布局与绘制过程中被正确应用。这种严格的执行顺序保证了Android UI渲染的一致性和可预测性。

APM 与 Choreographer

由于 Choreographer 的位置,许多性能监控的手段都是利用 Choreographer 来做的。需要注意的是:FrameInfo 本身是 Framework 内部结构(@hide),应用侧通常通过 FrameCallbackFrameMetricsdumpsys gfxinfo framestats、Perfetto 等手段间接获取帧信息。常用的方法如下:

  1. 利用 FrameCallback 的 doFrame 回调
  2. 利用 FrameMetrics API
  3. 利用 framestats(底层依赖 FrameInfo):adb shell dumpsys gfxinfo <packagename> framestats
  4. 利用 SurfaceFlinger latency:adb shell dumpsys SurfaceFlinger --latency
  5. 利用 SurfaceFlinger PageFlip 机制:adb service call SurfaceFlinger 1013(需要系统权限)
  6. Choreographer 自身的掉帧计算逻辑
  7. BlockCanary 基于 Looper 的性能监控
  8. Perfetto 工具的强大监控能力
    1. Perfetto 已是 Android 主线 tracing 基础设施(Android 10+ 已明显替代早期 systrace.py 工作流)
    2. Perfetto可以捕获更详细的系统性能数据,包括Choreographer的工作细节
    3. 使用Perfetto UI可以可视化分析帧渲染过程

利用 Perfetto 进行高级监控

Perfetto 是 Android 新一代的系统跟踪工具,目前已是 Android 主线系统跟踪方案。它提供了比 Systrace 更强大的功能:

  1. 更全面的性能数据采集
    Perfetto 可以同时收集 CPU、内存、图形渲染、系统服务等多维度的数据

  2. 更低的开销
    采用高效的跟踪引擎,对系统性能影响更小

  3. 更好的 Choreographer 追踪
    可以详细跟踪 Choreographer 的工作过程,包括:

    • Vsync 信号的接收和处理
    • doFrame 方法的执行细节
    • 各类回调的执行时间
    • UI 线程和 RenderThread 的协作过程
  4. 追踪 BlastBufferQueue 相关链路
    能够跟踪 BlastBufferQueue 的工作过程,帮助开发者理解缓冲区管理机制

使用 Perfetto 跟踪 Choreographer

1
2
3
4
5
6
7
8
# 开始记录 Perfetto trace(示例:15 秒,常用类别)
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 15s \
sched freq idle am wm gfx view binder_driver hal

# 完成后获取 trace 文件
adb pull /data/misc/perfetto-traces/trace.perfetto-trace

# 在 Perfetto UI 中分析(https://ui.perfetto.dev/)

在 Perfetto UI 中,可以找到名为 “Choreographer#doFrame” 的事件,它展示了每一帧的处理时间和细节。还可以查看 UI 线程和 RenderThread 之间的协作关系,以及与 SurfaceFlinger 的交互。

利用 framestats(底层依赖 FrameInfo)进行监控

adb shell dumpsys gfxinfo framestats

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Window: StatusBar
Stats since: 17990256398ns
Total frames rendered: 1562
Janky frames: 361 (23.11%)
50th percentile: 6ms
90th percentile: 23ms
95th percentile: 36ms
99th percentile: 101ms
Number Missed Vsync: 33
Number High input latency: 683
Number Slow UI thread: 273
Number Slow bitmap uploads: 8
Number Slow issue draw commands: 18
Number Frame deadline missed: 287
HISTOGRAM: 5ms=670 6ms=128 7ms=84 8ms=63 9ms=38 10ms=23 11ms=21 12ms=20 13ms=25 14ms=39 15ms=65 16ms=36 17ms=51 18ms=37 19ms=41 20ms=20 21ms=19 22ms=18 23ms=15 24ms=14 25ms=8 26ms=4 27ms=6 28ms=3 29ms=4 30ms=2 31ms=2 32ms=6 34ms=12 36ms=10 38ms=9 40ms=3 42ms=4 44ms=5 46ms=8 48ms=6 53ms=6 57ms=4 61ms=1 65ms=0 69ms=2 73ms=2 77ms=3 81ms=4 85ms=1 89ms=2 93ms=0 97ms=2 101ms=1 105ms=1 109ms=1 113ms=1 117ms=1 121ms=2 125ms=1 129ms=0 133ms=1 150ms=2 200ms=3 250ms=0 300ms=1 350ms=1 400ms=0 450ms=0 500ms=0 550ms=0 600ms=0 650ms=0

---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
0,10158314881426,10158314881426,9223372036854775807,0,10158315693363,10158315760759,10158315769821,10158316032165,10158316627842,10158316838988,10158318055915,10158320387269,10158321770654,428000,773000,
0,10158332036261,10158332036261,9223372036854775807,0,10158332799196,10158332868519,10158332877269,10158333137738,10158333780654,10158333993206,10158335078467,10158337689561,10158339307061,474000,885000,
0,10158348665353,10158348665353,9223372036854775807,0,10158349710238,10158349773102,10158349780863,10158350405863,10158351135967,10158351360446,10158352300863,10158354305654,10158355814509,471000,836000,
0,10158365296729,10158365296729,9223372036854775807,0,10158365782373,10158365821019,10158365825238,10158365975290,10158366547946,10158366687217,10158367240706,10158368429248,10158369291852,269000,476000,

利用 SurfaceFlinger 进行监控

命令解释:

  1. 数据的单位是纳秒,时间是以开机时间为起始点
  2. 每一次的命令都会得到128行的帧相关的数据

数据:

  1. 第一行数据,表示刷新的时间间隔refresh_period
  2. 第1列:这一部分的数据表示应用程序绘制图像的时间点
  3. 第2列:在SF(软件)将帧提交给H/W(硬件)绘制之前的垂直同步时间,也就是每帧绘制完提交到硬件的时间戳,该列就是垂直同步的时间戳
  4. 第3列:在SF将帧提交给H/W的时间点,算是H/W接受完SF发来数据的时间点,绘制完成的时间点。

掉帧 jank 计算

每一行都可以通过下面的公式得到一个值,该值是一个标准,我们称为jankflag,如果当前行的jankflag与上一行的jankflag发生改变,那么就叫掉帧

ceil((C - A) / refresh-period)

利用 SurfaceFlinger PageFlip 机制进行监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken("android.ui.ISurfaceComposer");
mFlinger.transact(1013, data, reply, 0);
final int pageFlipCount = reply.readInt();

final long now = System.nanoTime();
final int frames = pageFlipCount - mLastPageFlipCount;
final long duration = now - mLastUpdateTime;
mFps = (float) (frames * 1e9 / duration);
mLastPageFlipCount = pageFlipCount;
mLastUpdateTime = now;
reply.recycle();
data.recycle();

Choreographer 自身的掉帧计算逻辑

SKIPPED_FRAME_WARNING_LIMIT 默认为30 , 由 debug.choreographer.skipwarning 这个属性控制

1
2
3
4
5
6
7
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}

BlockCanary

Blockcanary 做性能监控使用的是 Looper 的消息机制,通过对 MessageQueue 中每一个 Message 的前后进行记录,打到监控性能的目的

android/os/Looper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}

MessageQueue 与 Choreographer

在 Android 消息机制中,异步消息具有特殊的处理优先级。系统可以通过 enqueueBarrier 方法向消息队列插入一个屏障(Barrier),使得该屏障之后的所有同步消息暂时无法被执行,直到调用 removeBarrier 方法移除屏障。而被标记为异步的消息则不受屏障影响,可以正常处理。

消息默认为同步类型,只有通过 Message 的 setAsynchronous 方法(该方法为隐藏 API)才能将消息设置为异步。在初始化 Handler 时,可以通过特定参数指定该 Handler 发送的所有消息均为异步类型,此时 Handler 的 enqueueMessage 方法会自动调用 Message 的 setAsynchronous 方法。

异步消息的核心价值在于能够绕过消息屏障继续执行,如果没有设置屏障,异步消息与同步消息的处理方式完全相同。通过 removeSyncBarrier 方法可以移除之前设置的屏障。

SyncBarrier 在 Choreographer 中使用的一个示例

scheduleTraversals 的时候 postSyncBarrier

1
2
3
4
5
6
7
8
9
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//为了提高优先级,先 postSyncBarrier
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

doTraversal 的时候 removeSyncBarrier

1
2
3
4
5
6
7
8
9
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 这里把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 真正开始
performTraversals();
}
}

Choreographer post Message 的时候,会把这些消息设为 Asynchronous ,这样 Choreographer 中的这些 Message 的优先级就会比较高,

1
2
3
4
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);

厂商优化

系统厂商由于可以直接修改源码,也利用这方面的便利,做一些功能和优化,不过由于保密的问题,代码就不直接放上来了,我可以大概说一下思路,感兴趣的可以私下讨论

移动事件优化

Choreographer 本身是没有 input 消息的, 不过修改源码之后,input 消息可以直接给到 Choreographer 这里, 有了这些 Input 消息,Choreographer 就可以做一些事情,比如说提前响应,不去等 Vsync

后台动画优化

当 Android 应用退到后台时,如果未被系统终止,其仍可能继续执行各类操作。在某些情况下,应用会持续调用 Choreographer 中的 Animation Callback,即使这些动画对用户不可见,这些 Callback 的执行完全无意义,却会对 CPU 资源造成较高的占用。

因此,系统厂商在 Choreographer 中会针对这种情况做优化,通过一系列策略限制不符合条件的后台应用继续执行无意义的动画回调,有效降低系统资源占用。

帧绘制优化

和移动事件优化一样,由于有了 Input 事件的信息,在某些场景下我们可以通知 SurfaceFlinger 不用去等待 Vsync 直接做合成操作

应用启动优化

我们前面说,主线程的所有操作都是给予 Message 的 ,如果某个操作,非重要的 Message 被排列到了队列后面,那么对这个操作产生影响;而通过重新排列 MessageQueue,在应用启动的时候,把启动相关的重要的启动 Message 放到队列前面,来起到加快启动速度的作用

animation callback 前置

在上一帧的主线程做完之后,距离下一个 vsync 其实是有一定时间的空闲的,这段时间其实可以用来准备下一帧。那么我们就可以把下一帧的 animation callback 提前到这里来做,这样可以有效利用 cpu 的空闲时间,减少掉帧

插帧

跟 animation callback 前置一样,在上一帧的主线程做完之后,距离下一个 vsync 其实是有一定时间的空闲的,这段时间其实可以用来准备下一帧。只不过这里我们直接生成新的一帧:即一个 Vsync 里面,执行两次 doFrame 。插帧相当于是提前准备好后面几帧的数据,这样在遇到真正耗时的帧的时候,不会出现卡顿。

插帧实现主要是在滑动场景,用在实现了 OverScroller 的滑动组件上,比如 ListView,RecyclerVIew 这些。因为如果用了 OverScroller,我们就会在 touch up 的时候, 就知道了滑动的时间和距离,就可以在这中间做手脚(插帧),相当于在一个 Vsync 内画好了后面好几个 VSync 里面的内容。

高帧率优化

现代 Android 设备上的高刷新率(120 Hz)将 Vsync 间隔从 16.6 ms 缩短至 8.3 ms,这带来了巨大的性能和功耗挑战。如何在一帧内完成渲染的必要操作,是手机厂商必须要思考和优化的地方:

  1. 超级 App 的性能表现以及优化
  2. 游戏高帧率合作
  3. 120 fps、90 fps 和 60 fps 相互切换的逻辑

参考资料

  1. https://www.jianshu.com/p/304f56f5d486
  2. http://gityuan.com/2017/02/25/choreographer/
  3. https://developer.android.com/reference/android/view/Choreographer
  4. https://www.jishuwen.com/d/2Vcc
  5. https://juejin.im/entry/5c8772eee51d456cda2e8099
  6. Android 开发高手课
  7. Perfetto - System profiling, app tracing, and trace analysis
  8. 使用 Perfetto 分析 UI 性能
  9. BufferQueue and Gralloc
  10. Android 15 图形渲染优化

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 基于 Choreographer 的渲染机制详解 - Perfetto 版
掘金 - Android 基于 Choreographer 的渲染机制详解 - Perfetto 版

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android 性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

☑️ ⭐

Android Weekly 2025-09 期

Android Weekly 是一份专注于 Android 技术生态的周刊,每周一更新。本周刊深入挖掘 Android 系统架构、性能优化、跨平台开发、AI 等领域的高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:[微信公众号] | [知乎专栏] | [掘金] | [RSS]

技术文章

  1. Flutter 上的 Platform 和 UI 线程合并是怎么回事?它会带来什么?: Flutter 3.29 引入了一个重要调整:在 Android 和 iOS 平台上,Dart 代码将直接运行在应用的主线程上,不再单独使用 Dart UI 线程。这一变更旨在优化 Native 和 Dart 的交互性能,简化线程管理,并减少异步通信带来的问题。通过线程合并,Flutter 可以更高效地处理任务,例如文本输入和平台视图渲染,但也可能带来插件兼容性问题。
  2. 秒开率从 18%到 64%,我们对小程序模拟器做了什么?: 小程序是一种运行在快手生态内,无需下载安装、即用即走的轻量级应用。其中,模拟器是快手开发者所使用的工具中最核心的模块之一,但因性能问题收到开发者反馈。为此,24 年 Q2 快手启动了模拟器性能优化专项,从线上数据看:模拟器秒开率从 18%提升至 64%,FCP P90 从 4.4s 提升至 1.9s。本文详细介绍优化措施和成效。
  3. Core 文件损坏还能补救吗?有的兄弟,有的。: 文章主要介绍了 core 文件损坏的相关情况及修复方法。包括 core 文件在 gdb、core-parser 上出现的问题,如截断、无法解析等,还详细阐述了通过 Fakecore 功能进行修复的步骤,如重建 linkmap、校准、重建 Fakecore 等,以及针对特殊情况如线程栈截断的处理方式。
  4. Facebook 开源 Screenshot Tests for Android:视觉回归自动化测试: Screenshot Tests for Android 是 Facebook 开源的一款自动化测试工具,核心功能是在 Android 设备测试过程中生成快速且确定的截图。截图可用于追踪应用界面的变化,从而有效预防视觉回归。视觉回归是指应用界面在更新后出现的不期望的视觉变化,如布局错乱、颜色不一致等问题。通过 Screenshot Tests for Android,可以更容易地捕捉这些问题,确保应用界面的稳定性和一致性。
  5. 深入研究 Android 启动速度优化(下): 在上一篇文章深入研究 Android 启动速度优化(上)中,梳理了应用启动的整个过程和问题,启动优化阶段与指标是什么,启动耗时方法的数据统计八种工具与分析,以及一些常见的启动时间问题。可以说是完成了启动优化工作最难的一部分 。
  6. 从 input 响应性能差的 issue 演示 perfetto trace 用法: 从 input 响应性能差的 issue 演示 perfetto trace 用法,分析了函数耗时、CPU 频率和调度等问题,并提出了优化思路。
  7. 基于 gpu counters 数据的性能优化: 基于 GPU Counters 数据的性能优化,文章探讨了通过 GPU 性能指标分析游戏性能瓶颈并提出优化方向。以三个游戏的性能数据为例,分析了内存读写、顶点和片元指标的差异,并总结了性能优化的通用思路和自动化的重要性。
  8. 提升移动游戏体验:性能和功耗的双重优化策略: 与主机和 PC 游戏开发不同的是,由于移动设备的安全限制,游戏开发商并没有足够的系统权限,无法根据手机实际的硬件情况进一步调优。因此,在移动游戏体验的优化上,除了游戏开发者们自己的优化,移动设备生产商的努力也同样至关重要的(这就是为何即便是同一芯片平台的设备不同品牌实际体验也有着明显差别的原因)。本篇文章将以移动设备厂商开发者视角,围绕性能和功耗两个最重要的方面,深入探讨如何优化游戏体验,力求为玩家提供更流畅和持久的游戏体验。需要说明的是,游戏体验是一个非常主观且庞大的话题,本篇只是介绍其中的性能和功耗,其他诸如触控、插帧、超分等不在讨论话题之内。
  9. kotlin-weekly-448: kotlin-weekly-448
  10. 不要升级,Flutter Debug 在 iOS 18.4 beta 无法运行,提示 mprotect failed: Permission denied: iOS 真机升级到 18.4 beta 后,Flutter Debug 运行会出现权限不足错误。原因是 iOS 加强内存权限限制,Flutter Debug 运行未签名二进制文件且需动态生成代码,而 release/profile 因代码完全打包成机械码无此问题。Flutter 官方在研究修复,真机未升级则无影响。
  11. Android 策略设计模式的使用:使用设计模式,减少烂代码,让项目更好维护: 这篇文章介绍了在 Android 项目开发中使用策略设计模式解决支付方式代码混乱、难以维护的问题。通过定义接口、创建默认实现类、具体实现类和策略工厂,实现了支付方式的分类管理。还阐述了策略设计模式的概念、组成部分、优点、缺点及应用场景。
  12. 深入探索 Android IPC:解锁进程间通信的奥秘: 文章深入探讨了 Android IPC,包括进程与线程的概念及关系、多进程模式、IPC 的基础概念与序列化方式、实现方式(Binder、AIDL、Messenger 等)、多进程模式带来的问题及解决方案,还介绍了 IPC 的应用场景与案例分析,并对其未来发展进行了展望。
  13. 理解 Android 中的内存泄漏以及如何让 LeakCanary 来帮忙: 这篇文章介绍了 Android 中的内存泄漏,包括其定义、危害、常见原因,重点讲解了 LeakCanary 检测和修复内存泄漏的作用、工作原理、集成步骤及局限性,还给出了修复泄漏的步骤和结论,强调内存管理的重要性。
  14. 2025 年 Android 六边形战士开发趋势,需要掌握哪些技能可自我反省: 本文探讨了 2025 年 Android 六边形战士开发趋势,包括必备技能(如多种语言和技术)、应用层(如原生 XML 布局、Compose 等)、系统层面、性能优化(启动、布局、内存等)、特殊功能(摄像头、音视频处理等)、逆向安全等方面,强调技术学习要与时俱进,多掌握技能。
  15. Android NDK 示例(六)图片内存监控: 这篇文章主要介绍了在 Android 中使用 NDK 实现图片内存监控的方法。指出图片内存占用大,不同 Android 版本对图片处理不同。使用 ShadowHook 库,分别拦截不同版本的图片创建函数,获取宽高等信息。文中详细说明了依赖配置、实现逻辑、外部使用及相关代码示例。

杂记

  1. 创业日记 独立开发周记 106:提高转化率: 这篇文章是一位独立开发者的周记,记录了其在开发、优化应用和个人生活中的点滴经历。作者通过量化工作、优化应用功能和尝试新设计来提高产品转化率,同时也分享了生活中的一些趣事和消费经历。

  2. 读《黑客与画家(10 万册纪念版)》: 你的职位产生的业绩应该是可测量的,否则你做得再多,也不会得到更多的报酬。此外,你还必须有可放大性,也就是说你做出的决定能够产生巨大的效应。任何一个通过自身努力而致富的个人,在他身上应该都能同时发现可测量性和可放大性。高科技=可放大性

  3. 科技爱好者周刊#339:代币是什么: 这里记录每周值得分享的科技内容,周五发布。

  4. Meet the Android Studio Team: A Conversation with Android Developer UX Manager, Dan Dole: 这篇文章介绍了 Android Studio 团队及其对开发者体验的贡献,重点是 Dan Dole 的职业旅程和他对用户体验(UX)的独特见解。他讨论了人工智能(AI)和机器学习如何改变开发者的工作方式,并分享了团队如何通过倾听反馈和技术创新来满足开发者的需求。

  5. Android Studio Ladybug Feature Drop 稳定版已推出: Android Studio Ladybug 🐞 Feature Drop (2024.2.2) 稳定版已推出!借助 Android Studio 中的 Gemini、Wear 功能块的动画预览支持、App Links Assistant 等功能,提高您的工作效率。所有这些新功能都旨在帮助您更快地构建高品质 Android 应用。

  6. 02-24~03-02.老胡的周刊(第 179 期): 老胡的信息周刊,记录这周我看到的有价值的信息,主要针对计算机领域,内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。

  7. 以终为始,面向价值观生活: 理想的人生,应该是随时死去,都不留遗憾。

  8. 迷雾: 最近,作者一直在全力开发一个名为 Follow 的项目,主要使用 React Native 编写,但遇到了许多技术挑战,特别是在需要调用 native 方法时。尽管花了大量时间研究 React Native 和 swift 等相关技术,但感到收获有限,这让作者有些郁闷但同时也感到偶尔的喜悦。项目进展方面成功将 Shiro 升级到 Next.js 15,但开源版本暂不更新。 日常中,作者参加了好友 pseudoyu 的婚礼,并与许多远程工作的同事首次见面。此外,作者开始意识到繁重的工作和久坐生活对健康的不良影响,计划关注身体健康。开源方面,作者还开发了一个替代 GitHub Notification 的工具 Linear。

  9. 体验碎周报第 221 期(2025.2.24): 系统的知识来源于对碎片的整理和思考,为了更好地输出系统知识,记录本周我发现的体验设计和思考,为构建系统知识做准备。

AI

  1. 掌握这些 DeepSeek 提问技巧,开发效率翻倍!: 掌握 DeepSeek 提问技巧,提高开发效率,文章详细阐述了如何在不同开发场景中精准提问以获得高质量回答,包括定位技术问题、制定技术方案、优化代码性能、学习新技术等,并提供了通用提问模板和实用技巧。
  2. 多模态人物视频驱动技术回顾与业务应用: 多模态人物视频驱动技术在商业、教育、医疗及文化娱乐等领域具有广泛应用,通过综述关键技术(如口唇驱动、头部驱动和肢体驱动)及其在业务场景中的实践,本文展示了相关技术的最新进展及其商业价值。
  3. Cursor+Claude3.7 的绝杀:从原型到 app,两步完成 app 开发: 最近在 X 上看到了一些人在用 Claude 3.7 Sonnet 生成 app 原型图的尝试,受到启发,发现这么先生成不同界面的原型图再让 Cursor 基于原型图开发 app 会是很好的尝试。尤其是,你也可以不两步直接生成,而是在过程中更可视化地思考你要生产的原型,这对于非专业的产品经理来说,会是好得多的方式。
  4. 一个提示词 claude 生成一个 app 的 ui/ux: 产品文档知识库目录中包含关于使用提示词生成应用程序 UI/UX 的相关内容,同时分享了一个名为“流光卡片”的工具及其 API 使用文档,并提供了相关社交平台链接和教程。
  5. 【99%的人不知道】Cursor + Claude 3.7 的绝技:从原型到产品,两步完成 app 开发!: 这个视频展示了如何利用 Cursor 编辑器和 Claude 3.7 AI 模型,通过两个简单步骤快速开发一个完整的 APP 原型。这种方法特别适合非专业开发者快速将想法转化为可操作的应用程序。
  6. 吹爆 AI ?Flutter 开发在 Cursor & Trae 不一样的 AI 真实体验: 作者分享了在 Flutter 开发中使用 Trae 和 Cursor 的 AI 辅助编码体验,包括将状态管理框架从 redux 迁移到 riverpod 的需求。Trae 思考慢且迁移效果不佳,Cursor 也无法一步完成。尽管如此,它们在某些方面对工作效率有提升,同时还对比了 DeepSeek、Grok 3 和 ChatGPT 等,最后提及了 Anthropic 对 Claude 的发展规划。
  7. 关于 DeepSeek 我是怎么研究的(3): 本文介绍了 DeepSeek 涉及的推理机制(Reasoning Schema),包括其定义、组成部分(推理结构、策略和操作)以及工作原理。通过一个 24 点游戏的例子展示了 DeepSeek 的思考过程,并对比了推理(Reason)与推断(Inference)的区别,强调了推理在逻辑性和解释性方面的优势。
  8. 关于 DeepSeek 我是怎么研究的(4): 本文详细介绍了 DeepSeek-V3 模型的架构特点,包括基于 Transformer 框架、采用 MLA 和 DeepSeekMoE 架构。重点讨论了细粒度专家划分、共享专家分离、负载均衡策略以及 MLA 和 MTP 技术。此外,还介绍了训练基础设施及 FP8 混合精度训练框架。
  9. AI 辅助编程的崛起意味着什么?: 使用一段时间后,我的整体感受是:AI 辅助编程将会是不可逆转的趋势。越早掌握一款得心应手的工具,越早完成与 AI 的磨合,对程序员的个人发展越有益。
  10. AIGC Weekly #110: Grok3 发布,具备强大功能和上下文处理能力;谷歌 Veo2 视频生成模型开放使用,表现优秀但清晰度有所下降;Figure 推出 Helix 机器人视觉语言模型,实现多机器人协作;AI 在技术面试中的影响显著,需调整面试策略以应对 AI 挑战。

鸡血

投稿指南

欢迎投稿分享您的:

  • 技术博客
  • 实践经验
  • 工具推荐

投稿方式:

  • 公众号后台回复”投稿”
  • 本周刊下面留言
  • 发邮件 :dreamtale.jg@gmail.com
  • 微信联系:Gracker_Gao

关于作者

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 掘金 - Gracker:https://juejin.cn/user/1816846860560749
  2. 知乎 - Grackerhttps://www.zhihu.com/people/gracker
  3. 个人博客 - Android Performance : 写东西的地方
  4. 个人介绍 - 欢迎加微信群组多多交流 :里面有个人的微信和微信群链接。
  5. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  6. 微信公众号 Android Performance
  7. Android 性能优化知识星球 : 个人运营的一个知识星球,欢迎加入,多谢支持~

版权声明

  • 本周刊遵循 CC BY-NC-SA 4.0 协议
  • 转载请注明出处:Android Weekly 第 X 期
  • 欢迎订阅、分享,让更多开发者受益
☑️ ⭐

Android Perfetto 系列 4:使用命令行在本地打开超大 Trace

本篇是 Perfetto 系列文章的第四篇,如何使用 trace_processor_shell 在本地打开超过 2G 的大文件。在实际的问题分析过程中,我们经常会碰到非常大的 Trace 文件(大于 2GB),直接扔进 ui.perfetto.dev 是没法打开的,这是因为浏览器内存的限制。这时候我们就需要使用官方提供的 trace_processor_shell 工具来本地打开大文件。

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

本文目录

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

本系列旨在通过 Perfetto 这个工具,从一个新的视角审视 Android 系统的整体运作方式。此外,它还旨在提供一个不同的角度来学习 App 、 Framework、Linux 等关键模块。尽管你可能已经阅读过许多关于 Android Framework、App 、性能优化的文章,但或许因为难以记住代码或不明白其运行流程,你仍感到困惑。通过 Perfetto 这个图形化工具,你可能会获得更深入的理解。

Perfetto 系列目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

0. trace_processor_shell 工具下载

官方下载地址:https://github.com/google/perfetto/releases ,找到最新的 release 版本,选择自己的平台下载即可:

下载之后里面就会有 trace_processor_shell 工具(以 Mac 平台为例)

trace_processor_shell 是 Perfetto 开源项目的核心工具之一,提供高性能的本地 Trace 解析服务。通过 –httpd 参数启动 HTTP 服务器后,它允许:

  • 本地原生加速:绕过浏览器 WASM 的性能限制,直接调用 C++ 实现的解析引擎。基于 Rust/C++ 混合实现的解析引擎,优化了内存布局和并行处理,支持流式解析超大型 trace 文件。
  • 交互式分析:与 Perfetto UI 深度集成,支持动态查询和可视化。
  • 离线调试:无需上传 trace 到云端,保护隐私并支持内网环境。

其他的参数

参数作用示例值
--http-port指定监听端口--httpd :8080
--preload预加载常用数据表--preload sched
--num-threads设置解析线程数(默认 CPU 核数)--num-threads 8

1. 使用 trace_processor_shell 打开 Trace 大文件

./trace_processor_shell –httpd ../jank-航旅纵横-火车票-上下滑动超级卡顿.perfetto-trace

这时候在网页端打开 https://ui.perfetto.dev ,会有下面的弹框

弹框选项功能详解如下

1. YES, use loaded trace

  • 功能:直接复用当前 Trace Processor 已加载的 trace 文件状态(即命令行中指定的 ../jank-航旅纵横-火车票-上下滑动超级卡顿.perfetto-trace)。
  • 适用场景
    若你已通过 trace_processor_shell --httpd 加载了 trace 文件,且希望 UI 直接使用当前进程的解析状态(包括已执行的 SQL 查询、过滤条件等),选择此选项。
  • 优势
    避免重复解析文件,节省时间和内存。

2. YES, but reset state

  • 功能:强制重置 Trace Processor 状态,重新加载当前 trace 文件(或加载新文件)。
  • 适用场景
    • 需要清除当前 Trace Processor 的所有状态(如临时查询结果、过滤器等),重新开始分析。
    • 想通过同一端口加载另一个 trace 文件(需先停止当前进程或更换端口)。
  • 等效操作
    等同于关闭当前 trace_processor_shell 进程后重新执行命令。

3. NO, Use builtin WASM

  • 功能:完全绕过本地 Trace Processor 服务,改用浏览器内置的 WebAssembly (WASM) 引擎解析 trace 文件。
  • 适用场景
    • 本地 Trace Processor 服务不可用或存在兼容性问题。
    • 需要支持分享链接、下载修改后的 trace 文件等 WASM 模式专属功能。
  • 代价
    大文件(如 >100MB)解析速度显著下降,且可能因浏览器内存限制崩溃。

如果选择 YES, use loaded trace , 打开 Trace 后,下面这几个功能是不可用的

2. 命令行启动 vs 直接打开 UI 的区别

通过命令行启动 (trace_processor_shell --httpd)

  • 核心机制
    本地启动一个高性能的 C++ Trace Processor 服务(监听 127.0.0.1:9001),提供原生加速的 trace 解析能力。
  • 优势
    • 性能:原生代码解析速度远超 WASM,尤其适合大型 trace 文件(如 >100MB)。
    • 功能扩展性:支持 SQL 查询、自定义指标计算等高级功能。
    • 状态保持:Trace Processor 的解析状态(如 SQL 临时表)可跨页面会话保留。
  • 限制
    • 无法直接通过 UI 分享 trace 文件链接或下载修改后的文件。
    • 同一时间仅允许一个浏览器标签页使用加速服务。

直接打开 UI 网页 (ui.perfetto.dev)

  • 核心机制
    完全依赖浏览器内置的 WebAssembly 引擎解析 trace 文件,无本地服务参与。
  • 优势
    • 便捷性:无需安装或启动本地工具,适合快速查看小型 trace。
    • 功能完整性:支持分享链接、下载修改后的 trace 文件等协作功能。
  • 劣势
    • 性能瓶颈:WASM 解析速度慢,大文件可能导致浏览器卡顿或崩溃。
    • 功能限制:不支持部分高级 SQL 查询和自定义分析功能。

总结建议

  • 优先命令行启动:处理大型 trace 或需要复杂分析时,使用 trace_processor_shell --httpd 提升性能。
  • 临时轻量分析:直接上传到 ui.perfetto.dev 更方便,但需注意文件大小限制。

3. Mac 权限问题

Mac 上直接运行 ./trace_processor_shell –httpd 会报下面的错误

需要在设置-隐私与安全,点击 Allow 才可以继续运行

参考文档

  1. Perfetto Github 库
  2. Perfetto 官方文档

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

🔲 ☆

Android ANR 系列 3 :ANR 案例分享

本文为 Android App ANR 系列的第三篇,主要分享几个 ANR 的案例,系列文章目录如下

  1. Android App ANR 系列 1 :理解 Android ANR 设计思想
  2. Android App ANR 系列 2 :ANR 分析套路和关键 Log 介绍
  3. Android App ANR 系列 3 :ANR 案例分享

ANR(Application Not Responding),应用程序无响应,简单一个定义,却涵盖了很多 Android 系统的设计思想

首先,ANR 属于应用程序的范畴。这不同于 SNR(System Not Respoding),SNR 反映的问题是系统进程(system_server)失去了响应能力,而 ANR 明确将问题圈定在应用程序。SNR 由 Watchdog 机制保证,具体可以查阅 Watchdog 机制以及问题分析; ANR 由消息处理机制保证,Android 在系统层实现了一套精密的机制来发现 ANR,核心原理是消息调度和超时处理

其次,ANR 机制主体实现在系统层。所有与 ANR 相关的消息,都会经过系统进程(system_server)调度,然后派发到应用进程完成对消息的实际处理,同时,系统进程设计了不同的超时限制来跟踪消息的处理。 一旦应用程序处理消息不当,超时限制就起作用了,它收集一些系统状态,譬如 CPU/IO 使用情况、进程函数调用栈,并且报告用户有进程无响应了(ANR 对话框,部分 Rom 不显示 ANR 对话框,而是直接闪退到主界面)

然后,ANR 问题本质是一个性能问题。ANR 机制实际上对应用程序主线程的限制,要求主线程在限定的时间内处理完一些最常见的操作(启动服务、处理广播、处理输入), 如果处理超时,则认为主线程已经失去了响应其他操作的能力。主线程中的耗时操作,譬如密集 CPU 运算、大量 IO、复杂界面布局等,都会降低应用程序的响应能力

最后,部分 ANR 问题是很难分析的。有时候由于系统底层的一些影响,导致消息调度失败,出现问题的场景又难以复现。 这类 ANR 问题往往需要花费大量的时间去了解系统的一些行为,超出了 ANR 机制本身的范畴。有一些 ANR 问题很难调查清楚,因为整个系统不稳定的因素很多,例如 Linux Kernel 本身的 Bug 引起的内存碎片过多、硬件损坏等。这类比较底层的原因引起的 ANR 问题往往无从查起,并且这根本不是应用程序的问题,浪费了应用开发人员很多时间,如果你从事过整个系统的开发和维护工作的话会深有体会。所以我不能保证了解了本章的所有内容后能够解决一切 ANR 问题,如果出现了很疑难的 ANR 问题,我建议最好去和做 Framework、驱动和内核的朋友聊聊,或者,如果问题只是个十万分之一的偶然现象,不影响程序的正常运行,我倒是建议不去理它

– From duanqz

ANR 常见原因

对于 ANR 的原因,通常要做到 :大胆假设,小心求证 。发现异常的地方提取之后,先假设是这里的问题导致的,然后以这个假设为出发点,看前后的 Log 看看是否能支持自己的假设,如果不能,那么换一个点.

问题出在当前进程

  1. 死锁
  2. 主线程调用 thread 的 join()方法、sleep()方法、wait()方法或者等待线程锁的时候
  3. 主线程阻塞在 nSyncDraw
  4. 主线程耗时操作,如复杂的 layout,庞大的 for 循环,IO 等
  5. 主线程被子线程同步锁 block
  6. 主线程等待子线程超时
  7. 主线程 Activity 生命周期函数执行超时
  8. 主线程 Service 生命周期函数执行超时
  9. 主线程 Broadcast.onReceive 函数执行超时(即使调用了 goAsync )
  10. 渲染线程耗时
  11. 耗时的网络访问
  12. 大量的数据读写
  13. 数据库操作
  14. 硬件操作(比如 Camera)
  15. service binder 的数量达到上限
  16. 其它线程终止或崩溃导致主线程一直等待
  17. Dump 内存操作
  18. 大量 SharedPerference 同时读写

问题出在远端进程或者系统

  1. 与 SystemServer 进行 Binder 通信,SystemServer 执行耗时
    1. 方法本身执行耗时导致超时
    2. SystemServer Binder 锁竞争太多,导致等锁超时
  2. 等待其他进程返回超时,比如从其他进程的 ContentProvider 中获取数据超时
  3. Window 错乱导致 Input 超时
  4. ContentProvider 对应的进程频繁崩溃,也会杀掉当前进程
  5. 整机低内存
  6. 整机 CPU 占用高
  7. 整机 IO 使用率高
  8. SurfaceFlinger 超时
  9. 系统冻结功能出现 Bug
  10. System Server 中 WatchDog 出现 ANR
  11. 整机触发温控限制频率

ANR 案例分享

ANR 案例:头条 - 死锁

主要操作是频繁的从侧边栏拉出今日头条进行分屏操作,多次操作之后,应用发生了 ANR,这个会导致手机短暂的在分屏栏中的应用界面黑屏并卡死。不过 4-5 秒之后又正常。

应用发生 ANR 的原因是自身主线程被阻塞导致。

1
2
3
4
5
6
7
8
9
10
11
12
"main"prio=5tid=1Blocked
| group="main"sCount=1dsCount=0obj=0x74f9bbe8self=0xe7084400
| sysTid=28210nice=0cgrp=defaultsched=0/0handle=0xe9dcd534
| state=S schedstat=(13454428309928953492) utm=121stm=13core=3HZ=100
| stack=0xff3b6000-0xff3b8000stackSize=8MB
| held mutexes=
at com.ss.android.common.applog.LogReaper.insertCrashLog(SourceFile:98)
- waiting to lock <0x0d3fbd00> (a com.ss.android.common.applog.LogReaper) held by thread34
at com.ss.android.common.applog.AppLog.uncaughtException(SourceFile:1408
at u.aly.n.uncaughtException(SourceFile:34)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1063)

关键信息

1
waiting to lock <0x0d3fbd00> (a com.ss.android.common.applog.LogReaper) held by thread34

因此,在下面的 ANR 日志总,查找 tid==34

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"LogReaper"prio=5tid=34TimedWaiting
| group="main"sCount=1dsCount=0obj=0x12fcaba0self=0xcb226e00
| sysTid=28274nice=0cgrp=defaultsched=0/0handle=0xc9f9b920
| state=S schedstat=(77341565269270880) utm=7stm=0core=5HZ=100
| stack=0xc9e99000-0xc9e9b000stackSize=1038KB
| held mutexes=
at java.lang.Object.wait!(Native method)
- waiting on <0x00fc7065> (a java.util.concurrent.atomic.AtomicInteger)
at java.lang.Object.wait(Object.java:407)
at com.ss.android.action.b.d.a(SourceFile:216)
at com.ss.android.newmedia.b.onLogSessionBatchEvent(SourceFile:468)
at com.ss.android.common.applog.DBHelper.batchSession(SourceFile:616)
- locked <0x0d4ff1c4> (a com.ss.android.common.applog.DBHelper)
at com.ss.android.common.applog.LogReaper.switchSession(SourceFile:175)
at com.ss.android.common.applog.LogReaper.switchSession(SourceFile:153)
at com.ss.android.common.applog.LogReaper.processItem(SourceFile:122)
- locked <0x0d3fbd00> (a com.ss.android.common.applog.LogReaper)
at com.ss.android.common.applog.LogReaper.run(SourceFile:632)

关键信息:

1
waiting on <0x00fc7065> (a java.util.concurrent.atomic.AtomicInteger)

应用内代码阻塞引起的

ANR 案例:冻结导致 ANR

搜索 am_anr 查看 anr 发生的时间

1
05-0100:51:39.594 1449 5234I am_anr  : [0,2169,com.xxx.weather2,820526660,Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.  Outbound queue length: 0.  Wait queue length: 1.)]

从 ANR 描述可以看到,是当前的 Window 还在处理上一个 Input 事件超时,导致新的事件没有被及时处理,所以发生了 ANR

搜索 ANR in 查看当时的 cpu 信息

1
2
3
4
5
6
7
8
9
05-0100:51:53.974 1449 5234E ActivityManager: ANR in com.xxx.weather2 (com.xxx.weather2/com.xxx.weather.xxxMainActivity)
05-0100:51:53.974 1449 5234E ActivityManager: PID:2169
05-0100:51:53.974 1449 5234E ActivityManager: Reason:Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 1.)
05-01 00:51:53.974 1449 5234 E ActivityManager: Parent: com.xxx.weather2/com.xxx.weather.xxxMainActivity
05-01 00:51:53.974 1449 5234 E ActivityManager: Load: 29.89 / 31.82 / 32.27
05-01 00:51:53.974 1449 5234 E ActivityManager: CPU usage from 4583ms to 12043ms later (2020-05-01 00:51:44.177 to 2020-05-01 00:51:51.637):
05-01 00:51:53.974 1449 5234 E ActivityManager: 18% 1449/system_server: 9.1% user + 9.3% kernel / faults: 7819 minor 1 major
05-01 00:51:53.974 1449 5234 E ActivityManager: 10% 720/surfaceflinger: 6.2% user + 4.4% kernel / faults: 734 minor 26 major
05-01 00:51:53.974 1449 5234 E ActivityManager: 10% 651/android.hardware.audio@5.0-service-mediatek: 9.5% user + 0.8% kernel / faults: 1 minor 4 major

可以看到当时的 cpu 并不繁忙,所以不是系统负载原因导致的 ANR,那么后续分析就要从 Log 中抽取对应的信息,来分析超时的 input 事件发生前后系统在做什么

分析过程如下

  1. 找到 ANR 发生时候的具体时间:00:51:39.594
  2. 从 5s 前的 Log 开始看,找到有用的信息:00:51:34.478
  3. 发现这个时间点并没有什么异常,只是应用没有响应 key_back_down,导致 5s 后报了 anr
  4. 从 ANR 之后的 Log 中发现,xxxHansManager : unfreeze uid: 10182 package: com.xxx.weather2 reason: Signal 这句 Log,说明 com.xxx.weather2 之前是冻结状态,这里进行了解冻,那么可以大胆猜想,是不是 com.xxx.weather2 被冻结之后,无法响应事件导致的
  5. 搜索冻结 Log,可以看到在 00:51:04.000 的时候,系统把 com.xxx.weather2 冻结了 xxxHansManager : freeze uid: 10182 package: com.xxx.weather2 reason: LcdOff
  6. 这个需要让系统看是否是冻结的逻辑问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
由于 LcdOff, 所以启动冻,weather2 被冻结 :com.xxx.weather2 reason: LcdOff
05-01 00:51:03.994 1449 2187 I xxxHansManager : freeze uid: 10245 package: com.tencent.mm reason: LcdOff
05-01 00:51:03.994 1449 2187 D xxxListManagerImpl: com.tencent.mm in autoStart list!
05-01 00:51:03.998 1449 2187 I xxxHansManager : freeze uid: 10246 package: com.xunmeng.pinduoduo reason: LcdOff
05-01 00:51:03.998 1449 2187 D xxxListManagerImpl: com.xunmeng.pinduoduo in autoStart list!
05-01 00:51:04.000 1449 2187 I xxxHansManager : freeze uid: 10182 package: com.xxx.weather2 reason: LcdOff
05-01 00:51:04.002 645 30914 I netd : firewallSetUidRule(4, 10182, 2) <0.09ms>
05-01 00:51:04.005 1449 2187 I xxxHansManager : freeze uid: 10187 package: com.heytap.yoli reason: LcdOff
05-01 00:51:04.005 1449 2187 D xxxListManagerImpl: com.heytap.yoli in autoStart list!
05-01 00:51:04.005 1449 2187 I xxxHansManager : isHansImportantCase uid: 10196 pkg: cn.kuwo.player reason: audiofocus
05-01 00:51:04.007 1449 2187 I xxxHansManager : freeze uid: 10234 package: com.tencent.mobileqq reason: LcdOff
05-01 00:51:04.008 1449 2187 D xxxListManagerImpl: com.tencent.mobileqq in autoStart list!
05-01 00:51:04.014 1449 2187 I xxxHansManager : freeze uid: 10235 package: com.tencent.qqlive reason: LcdOff

weather2 接收到 KEYCODE_BACK 的 ACTION_DOWN
05-01 00:51:04.471 1449 9842 D xxxPhoneWindowManager: interceptKeyBeforeQueueing:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x8, repeatCount=0, eventTime=38812125, downTime=38812125, deviceId=-1, source=0x101, displayId=0 }
05-01 00:51:04.471 1449 9842 I sysui_multi_action: [757,803,799,key_back_down,802,1]
05-01 00:51:04.477 1449 1620 D xxxPhoneWindowManager: interceptKeyBeforeDispatching key: win=Window{1b36ae8 u0 com.xxx.weather2/com.xxx.weather.xxxMainActivity} event = KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x8, repeatCount=0, eventTime=38812125, downTime=38812125, deviceId=-1, source=0x101, displayId=0 }
05-01 00:51:04.477 1449 1620 D xxxPhoneWindowManager: interceptKeyBeforeDispatching newEvent keyCode = 4

weather2 接收到 KEYCODE_BACK 的 ACTION_UP
05-01 00:51:34.478 1449 1620 D xxxPhoneWindowManager: interceptKeyBeforeDispatching key: win=Window{1b36ae8 u0 com.xxx.weather2/com.xxx.weather.xxxMainActivity} event = KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x8, repeatCount=0, eventTime=38842126, downTime=38842126, deviceId=-1, source=0x101, displayId=0 }
05-01 00:51:34.478 1449 1620 D xxxPhoneWindowManager: interceptKeyBeforeDispatching newEvent keyCode = 4

KEYCODE_BACK 的 ACTION_UP 5s 没有响应,触发 ANR
05-01 00:51:39.484 1449 1620 I WindowManager: Input event dispatching timed out sending to com.xxx.weather2/com.xxx.weather.xxxMainActivity. Reason: Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 1.
05-01 00:51:39.484 2256 5518 I QUALITY-TOTAL: exp: anr
05-01 00:51:39.594 1449 5234 I am_anr : [0,2169,com.xxx.weather2,820526660,Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 1.)]
05-01 00:51:39.593 1449 1620 D ActivityManager: ANR post Runnable for ProcessRecord{ae0c833 2169:com.xxx.weather2/u0a182} to deal with anr happend at 38847248@#@2169
05-01 00:51:39.593 1449 1620 D ActivityManager: ANR caller(2) = com.android.server.am.ActivityManagerService$LocalService.inputDispatchingTimedOut:19872 com.android.server.wm.ActivityRecord.keyDispatchingTimedOut:2641 com.android.server.wm.AppWindowToken.keyDispatchingTimedOut:2007 com.android.server.wm.InputManagerCallback.notifyANR:111 com.android.server.input.InputManagerService.notifyANR:1822 <bottom of call stack> <bottom of call stack> <bottom of call stack>

系统发 Signal 3 给 weather2(SIGNAL_QUIT = 3,给到 Signal Catcher 线程用于输出 Trace)
05-01 00:51:40.868 2169 2202 I oloros.weather: Thread[7,tid=2202,WaitingInMainSignalCatcherLoop,Thread*=0xd9878400,peer=0x138c0250,"Signal Catcher"]: reacting to signal

系统检测到 Signal 给到的 weather2, 所以给 com.xxx.weather2 解冻
05-01 00:51:40.869 1449 9811 I xxxHansManager : unfreeze uid: 10182 package: com.xxx.weather2 reason: Signal
05-01 00:51:40.869 2169 2202 I oloros.weather:
05-01 00:51:40.869 2169 2169 W ViewRootImpl[xxxMainActivity]: Dropping event due to no window focus: KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x8, repeatCount=0, eventTime=38812125, downTime=38812125, deviceId=-1, source=0x101, displayId=0 }, hasFocus:true, mStopped:true, mPausedForTransition:false
05-01 00:51:40.869 2169 2169 W ViewRootImpl[xxxMainActivity]: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x28, repeatCount=0, eventTime=38847249, downTime=38812125, deviceId=-1, source=0x101, displayId=0 }, hasFocus:true, mStopped:true, mPausedForTransition:false

可以看到这里 weather2 卡了 36391.9ms,也就是从 00:51:04.471(KEYCODE_BACK 发出) 到 00:51:40.869(当前时间) 这段时间
05-01 00:51:40.869 1449 1620 I InputDispatcher: Window '1b36ae8 com.xxx.weather2/com.xxx.weather.xxxMainActivity (server)' spent 36391.9ms processing the last input event: KeyEvent
05-01 00:51:40.869 2169 2169 V ViewRootImpl[xxxMainActivity]: Sending input event to IME: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x28, repeatCount=0, eventTime=38847249, downTime=38812125, deviceId=-1, source=0x101, displayId=0 }
05-01 00:51:40.870 2169 2169 I Choreographer: Skipped 2212 frames! The application may be doing too much work on its main thread.

ANR 案例:Broadcast 超时导致 ANR

这个案例 Log 缺失,具体就是用户在进行迁移,BroadcastReceiver 的 onReceive 中会起一个线程处理,最终报的是 BroadcastReceiver ANR

1
2
04-0717:03:17.174435 1448 1476V WindowManager: Lookingforfocus: Window{efa2a4f u0 正在升级数据库,数据量较大,请耐心等待}, flags=25296898, canReceive=true1
04-0717:03:17.174459 1448 1476V WindowManager: findFocusedWindow: Foundnewfocus @ Window{efa2a4f u0 正在升级数据库,数据量较大,请耐心等待}2

分析如下:

  1. 日历后台广播 ANR 的原因是 onReceive 执行超时,之前没有看出来是因为 onReceive 里面耗时操作是用的 new Thread 的操作,以为在子线程里面做耗时操作,就不会影响后面广播的执行
  2. 后面发现 onReceive 里面调用了 PendingResult result = goAsync(); 这句话会在有序广播接收器执行的时候,可以在子线程执行耗时操作,而不会影响 receiver 的生命周期.这个方法非常简单,返回 mPendingResult 并将其设置为 null。
  3. 如果我们在 onReceive 方法中调用该方法,这意味着广播处理流程被打断了,当 onReceive 方法执行完毕,由于 mPendingResult 为 null,因此并不会马上回调 AMS.finishReceiver 方法。而且由于 goAsync 返回了 PendingResult,因此我们可以任意时刻、任意线程去调用 PendingResult.finish 去回调 AMS。相当于将一个同步回调变成了异步回调。而在这异步回调过程中,我们可以新起线程进行一些耗时的 IO 操作等等。简单来说,goAsync 提供了一种机制,让我们可以在异步线程中处理广播消息,以防止主线程被阻塞。https://juejin.im/post/5c15fc10e51d454ad55ef9fa
  4. 这个案例中,虽然在 onReceive 中使用了线程去处理耗时任务,但是由于调用了 goAsync,所以还是会计算超时时间,如果在规定的时间内没有完成,就算是在子线程,也会触发 BroadcastReceiver 的 ANR

可以看 goAsync 这个方法的官方注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* This can be called by an application in {@link #onReceive} to allow
* it to keep the broadcast active after returning from that function.
* This does <em>not</em> change the expectation of being relatively
* responsive to the broadcast, but does allow
* the implementation to move work related to it over to another thread
* to avoid glitching the main UI thread due to disk IO.
*
* <p>As a general rule, broadcast receivers are allowed to run for up to 10 seconds
* before they system will consider them non-responsive and ANR the app. Since these usually
* execute on the app's main thread, they are already bound by the ~5 second time limit
* of various operations that can happen there (not to mention just avoiding UI jank), so
* the receive limit is generally not of concern. However, once you use {@code goAsync}, though
* able to be off the main thread, the broadcast execution limit still applies, and that
* includes the time spent between calling this method and ultimately
* {@link PendingResult#finish() PendingResult.finish()}.</p>
*
* <p>If you are taking advantage of this method to have more time to execute, it is useful
* to know that the available time can be longer in certain situations. In particular, if
* the broadcast you are receiving is not a foreground broadcast (that is, the sender has not
* used {@link Intent#FLAG_RECEIVER_FOREGROUND}), then more time is allowed for the receivers
* to run, allowing them to execute for 30 seconds or even a bit more. This is something that
* receivers should rarely take advantage of (long work should be punted to another system
* facility such as {@link android.app.job.JobScheduler}, {@link android.app.Service}, or
* see especially {@link android.support.v4.app.JobIntentService}), but can be useful in
* certain rare cases where it is necessary to do some work as soon as the broadcast is
* delivered. Keep in mind that the work you do here will block further broadcasts until
* it completes, so taking advantage of this at all excessively can be counter-productive
* and cause later events to be received more slowly.</p>
*
* @return Returns a {@link PendingResult} representing the result of
* the active broadcast. The BroadcastRecord itself is no longer active;
* all data and other interaction must go through {@link PendingResult}
* APIs. The {@link PendingResult#finish PendingResult.finish()} method
* must be called once processing of the broadcast is done.
*/
public final PendingResult goAsync(){
PendingResult res = mPendingResult;
mPendingResult =null;
returnres;
}

其用法如下

1
2
3
4
5
6
7
8
9
10
11
12
@Override1
public void onReceive(final Context context, final Intent intent){
finalPendingResult result = goAsync();
Runnable worker =newRunnable() {
@Override
public void run(){
onReceiveAsync(context, intent);
result.finish();
}
};
mAsyncHandler.post(worker);
}

ANR 案例:Launcher - Input ANR

搜索 am_anr 找到 ANR 发生的时间点

  1. 属于 input dispatch anr
  2. 属于应用处理 input 事件超时(而不是 no focus window)
1
19:44:56.815  2515 25056 I am_anr  : [0,3365,com.xxx.launcher,819478085,Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.  Outbound queue length: 0.  Wait queue length: 9.)]

搜索 ANR in 找到 cpu 使用情况 1.从 Load : 0.02 / 0.01 / 0.0 来看,整机的负载并不高,大概率是逻辑导致的 ANR.
2.logd 和 SurfaceFlinger 的 cpu 使用略高,可以当做怀疑的对象重点观察.

1
2
3
4
5
6
7
8
9
10
11
12
1-0119:45:07.753 251525056E ActivityManager: ANR in com.xxx.launcher (com.xxx.launcher/.Launcher)
19:45:07.753 251525056E ActivityManager: PID:3365
19:45:07.753 251525056E ActivityManager: Reason: Input dispatching timed out (Waiting to send key event because the focused window hasnotfinished processing all of the input events that were previously delivered to it. Outbound queue length:0. Wait queue length:9.)
19:45:07.753 251525056E ActivityManager: Parent: com.xxx.launcher/.Launcher
19:45:07.753 251525056E ActivityManager: Load:0.02/0.01/0.0
19:45:07.753 251525056E ActivityManager: CPU usage from0ms to10936ms later (2020- 19:44:56.751to2020- 19:45:07.688):
19:45:07.753 251525056E ActivityManager: 97%546/logd:97% user +0.2% kernel / faults:12minor
19:45:07.753 251525056E ActivityManager: 25%956/surfaceflinger:11% user +13% kernel / faults:151minor17major
19:45:07.753 251525056E ActivityManager: 1.4%1664/media.codec:0.9% user +0.5% kernel / faults:28729minor4major
19:45:07.753 251525056E ActivityManager: 7.5%2515/system_server:2.9% user +4.6% kernel / faults:1818minor8major
19:45:07.753 251525056E ActivityManager: 3.2%3365/com.xxx.launcher:2.1% user +1% kernel / faults:3535minor29major
19:45:07.753 251525056E ActivityManager: 2%3620/com.xxx.persist.system:1.2% user +0.7% kernel / faults:870minor

分析 trace.txt 查看是否有有用的信息,首先看 Launcher 的主线程堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"main"prio=5tid=1Native
| group="main"sCount=1dsCount=0flags=1obj=0x72a50cd0self=0x6f7ae10800
| sysTid=3365nice=-10cgrp=defaultsched=0/0handle=0x700123bed8
| state=S schedstat=(37451337214473599138010494263) utm=26967stm=10484core=0HZ=100
|stack=0x7fc0270000-0x7fc0272000stackSize=8192KB
| held mutexes=
kernel: (couldn't read /proc/self/task/3365/stack)
native: #00pc0000000000071a8c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
native: #01pc0000000000075710 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex(voidvolatile*,bool,int,bool, timespecconst*)+140)
native: #02pc00000000000d9744 /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+60)
native: #03pc00000000002bf5e8 /system/lib64/libhwui.so (android::uirenderer::renderthread::DrawFrameTask::postAndWait()+168)
native: #04pc00000000002bf510 /system/lib64/libhwui.so (android::uirenderer::renderthread::DrawFrameTask::drawFrame()+44)12
at android.graphics.HardwareRenderer.nSyncAndDrawFrame(Native method)
at android.graphics.HardwareRenderer.syncAndDrawFrame(HardwareRenderer.java:422)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:671)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:3983)
- locked <0x030694da> (a java.lang.Object)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3782)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3085)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1994)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8201)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1085)
at android.view.Choreographer.doCallbacks(Choreographer.java:908)
at android.view.Choreographer.doFrame(Choreographer.java:835)
at android.view.Choreographer$FrameHandler.handleMessage(Choreographer.java:1013)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:238)
at android.app.ActivityThread.main(ActivityThread.java:7798)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)

可以看到这里主线程阻塞在了等待 RenderThread 返回,那么继续查看 RenderThread 的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"RenderThread"daemon prio=7tid=25Native
| group="main"sCount=1dsCount=0flags=1obj=0x13100b00self=0x6f0c698c00
| sysTid=3616nice=-10cgrp=defaultsched=0/0handle=0x6f072e0d50
| state=S schedstat=(35808915205059629728352628704) utm=29226stm=6582core=3HZ=100
|stack=0x6f071ea000-0x6f071ec000stackSize=991KB
| held mutexes=
kernel: (couldn't read /proc/self/task/3616/stack)
native: #00pc00000000000c49f4 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+4)
native: #01pc000000000007d518 /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+132)
native: #02pc0000000000059e58 /system/lib64/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+244)
native: #03pc000000000005ad94 /system/lib64/libbinder.so (android::IPCThreadState::waitForResponse(android::Parcel*,int*)+60
native: #04pc000000000005ab38 /system/lib64/libbinder.so (android::IPCThreadState::transact(int,unsignedint, android::Parcelconst&, android::Parcel*,unsignedint)+180)
native: #05pc000000000004f000 /system/lib64/libbinder.so (android::BpBinder::transact(unsignedint, android::Parcelconst&, android::Parcel*,unsignedint)+228)
native: #06pc000000000007fef8 /system/lib64/libgui.so (android::BpGraphicBufferProducer::dequeueBuffer(int*, android::sp<android::Fence>*,unsignedint,unsignedint,int,unsignedlong,unsignedlong*, android::FrameEventHistoryDelta*)+224)
native: #07pc00000000000b9880 /system/lib64/libgui.so (android::Surface::dequeueBuffer(ANativeWindowBuffer**,int*)+392)
native: #08pc0000000000380540 /system/lib64/libhwui.so (android::uirenderer::renderthread::ReliableSurface::hook_dequeueBuffer(ANativeWindow*, ANativeWindowBuffer**,int*)+104)
native: #09pc000000000000989c /vendor/lib64/egl/eglSubDriverAndroid.so (???)
native: #10pc0000000000009388 /vendor/lib64/egl/eglSubDriverAndroid.so (???)
native: #11pc000000000022cd2c /vendor/lib64/egl/libGLESv2_adreno.so (???)
native: #12pc0000000000212b8c /vendor/lib64/egl/libGLESv2_adreno.so (???)
native: #13pc0000000000020838 /system/lib64/libEGL.so (android::eglQuerySurfaceImpl(void*,void*,int,int*)+248)
native: #14pc00000000002c6794 /system/lib64/libhwui.so (android::uirenderer::renderthread::EglManager::beginFrame(void*)+224)
native: #15pc00000000002d414c /system/lib64/libhwui.so (android::uirenderer::renderthread::CanvasContext::draw()+236)
native: #16pc00000000002d34e8 /system/lib64/libhwui.so (_ZNSt3__110__function6__funcIZN7android10uirenderer12renderthread13DrawFrameTask11postAndWaitEvE3$_0NS_9allocatorIS6_EEFvvEEclEv$c303f2d2360db58ed70a2d0ac7ed911b+380)
native: #17pc00000000002de044 /system/lib64/libhwui.so (android::uirenderer::WorkQueue::process()+228)
native: #18pc00000000002ddd24 /system/lib64/libhwui.so (android::uirenderer::renderthread::RenderThread::threadLoop()+576)
native: #19pc0000000000013654 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+328)
native: #20pc00000000000da200 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36)
native: #21pc00000000000769d4 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)

可以看到 RenderThread 是卡在了 binder 通信上,调用的方法是 dequeueBuffer,在等待 dequeueBuffer 返回结果。正常情况下主线程是不等待 dequeueBuffer 的,在 nSyncAndDrawFrame 返回后就继续执行了,这里卡住是因为在等 RenderThread 的上一个 draw 任务完成,也就是说上一个 draw 任务卡在了 dequeueBuffer,导致这一帧的主线程卡住导致超时

对帧渲染流程熟悉的话,应该知道 dequeueBuffer 的对端是 SurfaceFlinger,这时候需要查看 SurfaceFlinger 是否有问题(这里如果有 binder info 就可以直接看到 renderthread 在跟 SurfaceFlinger 的哪个线程进行通信,binder info 中可以看到 binder 通信的细节,比如这个案例我们应该可以看到(由于没有 dump 出来 binder info,所以下面的只是一个例子)

1
2
3
4
5
6
7
// 956 是 SurfaceFlinger,这里是 SurfaceFlinger 的 incoming binder 信息
proc956:
incoming transaction28350131:0000000000000000from3365:3616to956:1357code2flags10pri0:110r1 node28346208size112:0data0000000000000000

// 3365 是 Launcher,3365:3616 => 956:1356 意思是从 Launcher(3365)的渲染线程(3616)到 SurfaceFlinger(956) 的 Binder:956_4(1357)
proc3365:
outgoing transaction28350131:0000000000000000from3365:3616to956:1357code2flags10pri0:110r1

这里我们搜 dequeueBuffer,来看 RenderThread 是在与 SurfaceFlinger 的那个 Bidner 线程通信,发现是 Binder:956_4

对应的 sysTid=1357

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"Binder:956_4"sysTid=1357
#00pc0000000000071a8c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId:340eb42dd36c6541aec54c306eb4d0ee)
#01pc0000000000075710 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex(voidvolatile*,bool,int,bool, timespecconst*)+140) (BuildId:340eb42dd36c6541aec54c306eb4d0ee)
#02pc00000000000d97e8 /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_timedwait+120) (BuildId:340eb42dd36c6541aec54c306eb4d0ee)
#03pc0000000000072df0 /system/lib64/libc++.so (_ZNSt3__118condition_variable15__do_timed_waitERNS_11unique_lockINS_5mutexEEENS_6chrono10time_pointINS5_12system_clockENS5_8durationIxNS_5ratioILl1ELl1000000000EEEEEEE+108) (BuildId:5aad1075509f6e517eb78db32da8fbf6)
#04pc000000000006f9d8 /system/lib64/libgui.so (android::BufferQueueProducer::waitForFreeSlotThenRelock(android::BufferQueueProducer::FreeSlotCaller, std::__1::unique_lock<std::__1::mutex>&,int*)const+788) (BuildId: e017958f19275814d1f2d55ce8b10bfa)
#05pc000000000006fd8c /system/lib64/libgui.so (android::BufferQueueProducer::dequeueBuffer(int*, android::sp<android::Fence>*,unsignedint,unsignedint,int,unsignedlong,unsignedlong*, android::FrameEventHistoryDelta*)+776) (BuildId: e017958f19275814d1f2d55ce8b10bfa)
#06pc000000000007f0a8 /system/lib64/libgui.so (android::BnGraphicBufferProducer::onTransact(unsignedint, android::Parcelconst&, android::Parcel*,unsignedint)+3580) (BuildId: e017958f19275814d1f2d55ce8b10bfa)
#07pc000000000004d67c /system/lib64/libbinder.so (android::BBinder::transact(unsignedint, android::Parcelconst&, android::Parcel*,unsignedint)+136) (BuildId: bb7ca5d12323a310f26473506cf070ed)
#08pc000000000005a55c /system/lib64/libbinder.so (android::IPCThreadState::executeCommand(int)+1008) (BuildId: bb7ca5d12323a310f26473506cf070ed)
#09pc000000000005a0b8 /system/lib64/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+156) (BuildId: bb7ca5d12323a310f26473506cf070ed)
#10pc000000000005a898 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+220) (BuildId: bb7ca5d12323a310f26473506cf070ed)
#11pc000000000008093c /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24) (BuildId: bb7ca5d12323a310f26473506cf070ed)
#12pc0000000000013654 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+328) (BuildId:594f10db565bb0b9cf0223c7a1990ce5)
#13pc00000000000da200 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36) (BuildId:340eb42dd36c6541aec54c306eb4d0ee)
#14pc00000000000769d4 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId:340eb42dd36c6541aec54c306eb4d0ee)

可以看到这里是卡在了 waitForFreeSlotThenRelock,其代码逻辑在 frameworks/native/libs/gui/BufferQueueProducer.cpp 中, 感兴趣的可以自己看一下

这里卡在 waitForFreeSlotThenRelock 的意思就是,没有足够的 Buffer,因为每个 Layer 对应的 Buffer 的个数是一定的(2 个或者 3 个或者 4 个,一般是 3 个或者 4 个),如果 4 个 Buffer 都在使用过程中,那么应用调用 dequeueBuffer 去申请 free 的 Buffer 是不会成功的,需要进行等待,这里就是在等待 free 的 buffer 超时,导致了应用的 ANR

Log 里面的 SurfaceFlinger、hwui 的部分 Log 也频繁打印来提示问题点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SurfaceFlinger 的 Log 提示可用 buffer 数量不足
19:44:42.371 956 956 E BufferQueueConsumer: [com.xxx.launcher/com.xxx.launcher.Launcher#0] acquireBuffer: max acquired buffer count reached: 2 (max 1)
19:44:42.371 956 956 E BufferLayerConsumer: [com.xxx.launcher/com.xxx.launcher.Launcher#0] updateTexImage: acquire failed: Function not implemented (-38)

// 提示是 dequeueBuffer 失败
19:45:45.127 3365 3616 W OpenGLRenderer: dequeueBuffer failed, error = -110; switching to fallback

19:45:45.138 3365 3616 I OpenGLRenderer: Davey! duration=4015ms; Flags=0, IntendedVsync=21148991192817, Vsync=21148991192817, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=21148991206202, AnimationStart=21148991249587, PerformTraversalsStart=21148991252765, DrawStart=21148991798858, SyncQueued=21148991974796, SyncStart=21152137782451, IssueDrawCommandsStart=21152138724534, SwapBuffers=21156150646824, FrameCompleted=21156152280366, DequeueBufferDuration=0, QueueBufferDuration=0,

// MessageQueue 的信息标识是哪个 Message block
19:45:45.142 3365 3365 E ANR_LOG : >>> msg's executing time is too long
19:45:45.142 3365 3365 E ANR_LOG : Blocked msg = { when=-4s2ms what=0 target=android.view.Choreographer$FrameHandler callback=android.view.Choreographer$FrameDisplayEventReceiver } , cost = 4002 ms
19:45:45.142 3365 3365 E ANR_LOG : >>>Current msg List is:
19:45:45.142 3365 3365 E ANR_LOG : Current msg <1> = { when=-4s5ms what=13 target=android.view.ViewRootImpl$ViewRootHandler }
19:45:45.142 3365 3365 E ANR_LOG : Current msg <2> = { when=-4s1ms what=0 target=android.view.ViewRootImpl$ViewRootHandler callback=com.android.launcher3.util.ViewOnDrawExecutor }
19:45:45.143 3365 3365 E ANR_LOG : Current msg <3> = { when=-4s1ms what=0 target=android.view.ViewRootImpl$ViewRootHandler callback=android.widget.Editor$1 }
19:45:45.143 3365 3365 E ANR_LOG : Current msg <4> = { when=-3s23ms what=0 target=android.view.ViewRootImpl$ViewRootHandler callback=android.widget.ViewFlipper$2 }
19:45:45.143 3365 3365 E ANR_LOG : Current msg <5> = { when=-1s502ms what=6 target=android.view.inputmethod.InputMethodManager$H arg1=74627 obj=android.view.inputmethod.InputMethodManager$PendingEvent@561bec9 }
19:45:45.143 3365 3365 E ANR_LOG : Current msg <6> = { when=+14s859ms what=0 target=android.os.Handler callback=xxxUIEngineProguard.i.b$b }
19:45:45.143 3365 3365 E ANR_LOG : Current msg <7> = { when=+24d20h30m18s414ms what=0 target=android.view.ViewRootImpl$ViewRootHandler callback=android.widget.ViewFlipper$2 }
19:45:45.143 3365 3365 E ANR_LOG : >>>CURRENT MSG DUMP OVER<<<

这种问题一般是 SurfaceFlinger 的问题,可以转给对应的负责人继续分析

异常 Log 案例

主线程进行数据库读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"main" prio=5 tid=1 Native
held mutexes=
kernel: (couldn't read /proc/self/task/11003/stack)
native: #00 pc 000492a4 /system/lib/libc.so (nanosleep+12)
native: #01 pc 0002dc21 /system/lib/libc.so (usleep+52)
native: #02 pc 00009cab /system/lib/libsqlite.so (???)
native: #03 pc 00011119 /system/lib/libsqlite.so (???)
native: #04 pc 00016455 /system/lib/libsqlite.so (???)
native: #16 pc 0000fa29 /system/lib/libsqlite.so (???)
native: #17 pc 0000fad7 /system/lib/libsqlite.so (sqlite3_prepare16_v2+14)
native: #18 pc 0007f671 /system/lib/libandroid_runtime.so (???)
native: #19 pc 002b4721 /system/framework/arm/boot-framework.oat (Java_android_database_sqlite_SQLiteConnection_nativePrepareStatement__JLjava_lang_String_2+116)
at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:294)
at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:215)
at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:193)
at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:463)
at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:185)
at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:177)
at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:808)
locked <0x0db193bf> (a java.lang.Object)
at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:793)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:696)
at android.app.ContextImpl.openOrCreateDatabase(ContextImpl.java:690)
at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:299)
at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:223)
at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)
locked <0x045a4a8c> (a com.xxxx.video.common.data.DataBaseHelper)
at com.xxxx.video.common.data.DataBaseORM.<init>(DataBaseORM.java:46)
at com.xxxx.video.common.data.DataBaseORM.getInstance(DataBaseORM.java:53)
locked <0x017095d5> (a java.lang.Class<com.xxxx.video.common.data.DataBaseORM>)

Binder 数据量过大

1
2
3
4
5
07-2104:43:21.573 1000 148812756E Binder  : Unreasonably large binder replybuffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling1size388568(data:1,32,7274595)
07-2104:43:21.573 1000 148812756E Binder : android.util.Log$TerribleFailure: Unreasonably large binder replybuffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling1size388568(data:1,32,7274595)
07-2104:43:21.607 1000 1488 2951E Binder : Unreasonably large binder replybuffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling1size211848(data:1,23,7274595)
07-2104:43:21.607 1000 1488 2951E Binder : android.util.Log$TerribleFailure: Unreasonably large binder replybuffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling1size211848(data:1,23,7274595)
07-2104:43:21.662 1000 1488 6258E Binder : Unreasonably large binder replybuffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling1size259300(data:1,33,7274595)

Binder 通信失败

1
2
3
4
07-2106:04:35.580<6>[32837.690321] binder:1698:2362transaction failed29189/-3,size100-0line3042
07-2106:04:35.594<6>[32837.704042] binder:1765:4071transaction failed29189/-3,size76-0line3042
07-2106:04:35.899<6>[32838.009132] binder:1765:4067transaction failed29189/-3,size224-8line3042
07-2106:04:36.018<6>[32838.128903] binder:1765:2397transaction failed29189/-22,size348-0line2916

ANR 相关资料分享

  1. 西瓜视频稳定性治理体系建设一:Tailor 原理及实践
  2. 西瓜视频稳定性治理体系建设二:Raphael 原理及实践
  3. 西瓜视频稳定性治理体系建设三:Sliver 原理及实践
  4. 西瓜卡顿 & ANR 优化治理及监控体系建设
  5. 今日头条 ANR 优化实践系列 - 设计原理及影响因素
  6. 今日头条 ANR 优化实践系列 - 监控工具与分析思路
  7. 今日头条 ANR 优化实践系列分享 - 实例剖析集锦
  8. 今日头条 ANR 优化实践系列 - Barrier 导致主线程假死
  9. 今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
  10. Android ANR|原理解析及常见案例

参考资料

  1. https://duanqz.github.io/2015-10-12-ANR-Analysis#1-%E6%A6%82%E8%A7%88
  2. https://duanqz.github.io/2015-10-12-ANR-Analysis
  3. http://gityuan.com/2016/12/02/app-not-response/
  4. http://gityuan.com/2017/01/01/input-anr/
  5. https://xiaozhuanlan.com/topic/5097486132

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

🔲 ☆

Android ANR 系列 2 :ANR 分析套路和关键 Log 介绍

本文为 Android App ANR 系列的第二篇,主要分享 ANR 分析套路和关键 Log 介绍,系列文章目录如下

  1. Android App ANR 系列 1 :理解 Android ANR 设计思想
  2. Android App ANR 系列 2 :ANR 分析套路和关键 Log 介绍
  3. Android App ANR 系列 3 :ANR 案例分享

ANR(Application Not Responding),应用程序无响应,简单一个定义,却涵盖了很多 Android 系统的设计思想

首先,ANR 属于应用程序的范畴。这不同于 SNR(System Not Respoding),SNR 反映的问题是系统进程(system_server)失去了响应能力,而 ANR 明确将问题圈定在应用程序。SNR 由 Watchdog 机制保证,具体可以查阅 Watchdog 机制以及问题分析; ANR 由消息处理机制保证,Android 在系统层实现了一套精密的机制来发现 ANR,核心原理是消息调度和超时处理

其次,ANR 机制主体实现在系统层。所有与 ANR 相关的消息,都会经过系统进程(system_server)调度,然后派发到应用进程完成对消息的实际处理,同时,系统进程设计了不同的超时限制来跟踪消息的处理。 一旦应用程序处理消息不当,超时限制就起作用了,它收集一些系统状态,譬如 CPU/IO 使用情况、进程函数调用栈,并且报告用户有进程无响应了(ANR 对话框,部分 Rom 不显示 ANR 对话框,而是直接闪退到主界面)

然后,ANR 问题本质是一个性能问题。ANR 机制实际上对应用程序主线程的限制,要求主线程在限定的时间内处理完一些最常见的操作(启动服务、处理广播、处理输入), 如果处理超时,则认为主线程已经失去了响应其他操作的能力。主线程中的耗时操作,譬如密集 CPU 运算、大量 IO、复杂界面布局等,都会降低应用程序的响应能力

最后,部分 ANR 问题是很难分析的。有时候由于系统底层的一些影响,导致消息调度失败,出现问题的场景又难以复现。 这类 ANR 问题往往需要花费大量的时间去了解系统的一些行为,超出了 ANR 机制本身的范畴。有一些 ANR 问题很难调查清楚,因为整个系统不稳定的因素很多,例如 Linux Kernel 本身的 Bug 引起的内存碎片过多、硬件损坏等。这类比较底层的原因引起的 ANR 问题往往无从查起,并且这根本不是应用程序的问题,浪费了应用开发人员很多时间,如果你从事过整个系统的开发和维护工作的话会深有体会。所以我不能保证了解了本章的所有内容后能够解决一切 ANR 问题,如果出现了很疑难的 ANR 问题,我建议最好去和做 Framework、驱动和内核的朋友聊聊,或者,如果问题只是个十万分之一的偶然现象,不影响程序的正常运行,我倒是建议不去理它

– From duanqz

ANR 分析套路

ANR 问题主要有两种原因:应用自身的问题系统异常导致的问题。在分析 ANR 问题的时候,最主要的就是要确定是哪个原因导致的(当然也有一些中间地带,比如代码写的不好,在正常情况下不会暴露,在系统出问题的时候很快就暴露出来)。

ANR 问题一般的分析步骤如下:

  1. EventLog 看具体的 ANR 时间(搜索 am_anr),看看是否跟 ANR log 能够对上,以确定 ANR Log 是否有效,如果 ANR Log 有效,分析 ANR Log,提取有用信息:pid、tid、死锁等,遇到 ANR 问题,摆在我们面前的 trace 是不是第一案发现场,如果 ANR 发生的输出的信息很多,当时的 CPU 和 I/O 资源比较紧张,那么这段日志输出的时间点可能会延迟 10 秒到 20 秒都有可能,所以我们有时候需要提高警惕。不过正常情况下,EventLog 中的 am_anr 的输出时间是最早的,也是最接近 ANR 时间的。
  2. 看 MainLog(Android Log) 或者 SystemLog 查看 ANR 详细信息(搜索 ANR in),提取有效的信息。
    1. 发生 ANR 的时间
    2. 打印 ANR 日志的进程
    3. 发生 ANR 的进程
    4. 发生 ANR 的原因
    5. CPU 负载
    6. Memory 负载
    7. CPU 使用统计时间段
    8. 各进程的 CPU 使用率
      1. 总的 CPU 使用率
      2. 缺页次数 fault
        1. xxx minor 表示高速缓存中的缺页次数,可以理解为进程在做内存访问
        2. xxx major 表示内存的缺页次数,可以理解为进程在做 IO 操作
    9. CPU 使用汇总
  3. 配合 MainLog(Android Log) 和 EventLog 把 CPU 开始和结束的时间点内的所有有用信息提取出来到一个文件中.
    1. 收集关键操作,比如解锁、安装应用、亮灭屏、应用启动等
    2. 收集异常和系统关键 Log
      1. 系统变慢 :比如 Slow operation、Slow dispatch、Slow delivery、dvm_lock_sample
      2. 进程变化 :am_kill、am_proc_died、lowmemorykiller、ANR、应用启动关系等
      3. 系统信息 :cpu info、meminfo、binder info(是否满了) 、iowait (是否过高)
    3. 收集 ANR 进程的所有关键线程的运行情况、线程优先级等
    4. 根据第四步提取出来的关键信息文件,进一步理出系统当时的情况、状态((推荐 vscode 或者 notepad ++ ,有 线索就全局搜索)),比如
      1. 是处于低内存频繁杀进程?
      2. 重启第一次解锁系统繁忙
      3. 还是短时间内多个应用启动系统繁忙
      4. 还是应用自己的逻辑等待?
  4. 不行就加 Log 复现.

区分是应用的问题还是系统的问题

首先应该分析是否是应用的问题

分析应用的问题的关键是需要理清当时用户的操作是什么,应用在用户这个操作的过程中扮演了什么角色,然后再进行进一步的分析

  1. 分析应用是否关键组件中的生命周期中有耗时操作,可能平时没有暴露出来,一旦系统负载上来,就会暴露问题(建议在关键生命周期函数中加上对应的 Log,方便 Debug)。
  2. 分析是否出现极端情况,导致应用的逻辑耗时,比如大量的数据处理或者导入,同时运行线程过多等(看应用的 cpu \ io 使用情况)。
  3. 分析是否存在死锁。
  4. 分析是否是在等待 binder 返回。
  5. 分析 Trace 文件中 MainThread 和 RenderThread 是否存在异常。
  6. 分析 Trace 文件中 MainThread 跟 WorkerThread 是否存在等待关系。

分析系统的状态

  1. 查看 CPU 使用情况(cpu 使用率和 cpu 负载),看看 SystemServer、lowmemorykiller、HeapTaskDeamon、Audio、SurfaceFlinger 这些系统相关的进程或者线程是否占用高
  2. 查看是否存在大量 IO 的情况,看 io 负载
    1. faults: 118172 minor(高速缓存的缺页次数)。
    2. major(内存的缺页次数)。
  3. 查看系统是否是低内存
    1. 看 dumpsys meminfo 的结果,看看是否处于低内存。
    2. kernel log 是否有频繁的 lowmemorykiller。
    3. event log 是否有频繁的应用被系统低内存策略杀掉。
    4. kswapd0 。
  4. 应用是否被冻结:应用处于 D 状态,发生 ANR,如果最后的操作是 refriger,那么是应用被冻结了,正常情况下是功耗优化引起的,可以找一下前后是否有 xxxHansManager : unfreeze 这样的 Log;或者在 Systrace 中的 Kernel Callstack 显示 :{kernel callsite when blocked:: “__refrigerator+0xe4/0x198”}。
  5. 查看是否存在系统异常,比如自研功能导致系统繁忙,没有及时响应应用 Binder 之类的,这种需要分析 Log 中 SystemServer 的日志输出,查看是否有异常的 Log 输出。

继续分析:应用可以解决还是系统可以解决

  1. ANR 问题如果转给系统,大概率是无解的。
  2. 如果应用的代码是正常的,也没有极端的使用场景和数据处理,纯粹是由于系统或者其他的 App 造成的,那么可以转给系统处理。
  3. 如果应用代码本身有一定的问题,在非极端场景或者非系统异常的不会暴露出来,那么需要应用这边想一想规避方案。

ANR Log 分析

Log 可由测试使用 Log 工具抓取,可由线上平台获取,也可以自己抓取:

1
adb bugreport log.zip1

ANR Trace Log 所包含的内容如下

线程详细数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72c8bbf8 self=0xb400007b0ec10800
| sysTid=5991 nice=-10 cgrp=default sched=0/0 handle=0x7b95f61500
| state=S schedstat=( 807053249 267562324 1494 ) utm=63 stm=17 core=3 HZ=100
| stack=0x7fcccd9000-0x7fcccdb000 stackSize=8192KB
| held mutexes=
native: #00 pc 00000000000c6418 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 0000000000019a9c /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
native: #02 pc 000000000001997c /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
native: #03 pc 0000000000114310 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:339)
at android.os.Looper.loop(Looper.java:198)
at android.app.ActivityThread.main(ActivityThread.java:8142)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1006)

上面 Trace 中 部分字段的含义

线程堆栈

下面是该线程对应的调用堆栈

1
2
3
4
5
6
7
8
9
10
11
native: #00 pc 00000000000c6418 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 0000000000019a9c /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
native: #02 pc 000000000001997c /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
native: #03 pc 0000000000114310 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:339)
at android.os.Looper.loop(Looper.java:198)
at android.app.ActivityThread.main(ActivityThread.java:8142)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1006)

分为 Java 和 Native,一般来说,nativePollOnce 是在等待 Message,这是正常的

线程状态

Thread.java 中的状态和 Thread.cpp 中的状态是有对应关系的。可以看到前者更加概括,也比较容易理解,面向 Java 的使用者;而后者更详细,面向虚拟机内部的环境。traces.txt 中显示的线程状态都是 Thread.cpp 中定义的,完整的对应关系如下

art/runtime/thread_state.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// State stored in our C++ class Thread.
// When we refer to "a suspended state", or when function names mention "ToSuspended" or
// "FromSuspended", we mean any state other than kRunnable, i.e. any state in which the thread is
// guaranteed not to access the Java heap. The kSuspended state is merely one of these.
enum ThreadState {
// Java
// Thread.State JDWP state
kTerminated = 66, // TERMINATED TS_ZOMBIE Thread.run has returned, but Thread* still around
kRunnable, // RUNNABLE TS_RUNNING runnable
kTimedWaiting, // TIMED_WAITING TS_WAIT in Object.wait() with a timeout
kSleeping, // TIMED_WAITING TS_SLEEPING in Thread.sleep()
kBlocked, // BLOCKED TS_MONITOR blocked on a monitor
kWaiting, // WAITING TS_WAIT in Object.wait()
kWaitingForLockInflation, // WAITING TS_WAIT blocked inflating a thin-lock
kWaitingForTaskProcessor, // WAITING TS_WAIT blocked waiting for taskProcessor
kWaitingForGcToComplete, // WAITING TS_WAIT blocked waiting for GC
kWaitingForCheckPointsToRun, // WAITING TS_WAIT GC waiting for checkpoints to run
kWaitingPerformingGc, // WAITING TS_WAIT performing GC
kWaitingForDebuggerSend, // WAITING TS_WAIT blocked waiting for events to be sent
kWaitingForDebuggerToAttach, // WAITING TS_WAIT blocked waiting for debugger to attach
kWaitingInMainDebuggerLoop, // WAITING TS_WAIT blocking/reading/processing debugger events
kWaitingForDebuggerSuspension, // WAITING TS_WAIT waiting for debugger suspend all
kWaitingForJniOnLoad, // WAITING TS_WAIT waiting for execution of dlopen and JNI on load code
kWaitingForSignalCatcherOutput, // WAITING TS_WAIT waiting for signal catcher IO to complete
kWaitingInMainSignalCatcherLoop, // WAITING TS_WAIT blocking/reading/processing signals
kWaitingForDeoptimization, // WAITING TS_WAIT waiting for deoptimization suspend all
kWaitingForMethodTracingStart, // WAITING TS_WAIT waiting for method tracing to start
kWaitingForVisitObjects, // WAITING TS_WAIT waiting for visiting objects
kWaitingForGetObjectsAllocated, // WAITING TS_WAIT waiting for getting the number of allocated objects
kWaitingWeakGcRootRead, // WAITING TS_WAIT waiting on the GC to read a weak root
kWaitingForGcThreadFlip, // WAITING TS_WAIT waiting on the GC thread flip (CC collector) to finish
kNativeForAbort, // WAITING TS_WAIT checking other threads are not run on abort.
kStarting, // NEW TS_WAIT native thread started, not yet ready to run managed code
kNative, // RUNNABLE TS_RUNNING running in a JNI native method
kSuspended, // RUNNABLE TS_RUNNING suspended by GC or debugger
};

Trace 文件中,线程名的最后一行标识的就是当前线程的状态

正常主线程 Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72c8bbf8 self=0xb400007b0ec10800
| sysTid=5991 nice=-10 cgrp=default sched=0/0 handle=0x7b95f61500
| state=S schedstat=( 807053249 267562324 1494 ) utm=63 stm=17 core=3 HZ=100
| stack=0x7fcccd9000-0x7fcccdb000 stackSize=8192KB
| held mutexes=
native: #00 pc 00000000000c6418 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 0000000000019a9c /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
native: #02 pc 000000000001997c /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
native: #03 pc 0000000000114310 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:339)
at android.os.Looper.loop(Looper.java:198)
at android.app.ActivityThread.main(ActivityThread.java:8142)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1006)

异常主线程 Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x70f65400 self=0xe28dae00
| sysTid=22002 nice=-10 cgrp=default sched=0/0 handle=0xe9674474
| state=S schedstat=( 1943159901 290647362 1661 ) utm=159 stm=34 core=7 HZ=100
| stack=0xff041000-0xff043000 stackSize=8192KB
| held mutexes=
at com.facebook.cache.disk.DiskStorageCache.e(DiskStorageCache.java:3)
- waiting to lock <0x0e57c91f> (a java.lang.Object) held by thread 89
at com.xxx.community.util.imageloader.FrescoImageLoader.a(FrescoImageLoader.java:18)
at com.xxx.community.util.imageloader.FrescoImageLoader$2$1.run(FrescoImageLoader.java:2)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:254)
at android.app.ActivityThread.main(ActivityThread.java:8142)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1006)

CPU 使用率分析

搜索 ANR in 关键字可以看到 ANR 前一段时间内的 CPU 使用情况,其解析如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ActivityManager: ANR in com.xxx.launcher (com.xxx.launcher/.Launcher)(进程名)
ActivityManager: PID: 5991(进程 pid)
ActivityManager: Reason: Input dispatching timed out (6a6083a com.xxx.launcher/com.xxx.launcher.Launcher (server) is not responding. Waited 5001ms for FocusEvent(hasFocus=true))(原因)
ActivityManager: Parent: com.xxx.launcher/.Launcher
ActivityManager: Load: 15.29 / 5.19 / 1.87(Load 表明是 1 分钟,5 分钟,15 分钟 CPU 的负载)
ActivityManager: ----- Output from /proc/pressure/memory -----(内存压力)
ActivityManager: somavg10=1.35 avg60=0.31 avg300=0.06 total=346727
ActivityManager: full avg10=0.00 avg60=0.00 avg300=0.00 total=34803
ActivityManager: ----- End output from /proc/pressure/memory -----

// 13s 内的 cpu 使用情况
ActivityManager: CPU usage from 0ms to 13135ms later (2020-09-09 02:09:54.942 to 2020-09-09 02:10:08.077):
ActivityManager: 191%(CPU 的使用率) 1948/system_server: 72%(用户态的使用率) user + 119%(内核态的使用率) kernel / faults: 78816 minor 9 major
ActivityManager: 10% 2218/android.bg: 3.6% user + 6.6% kernel
ActivityManager: 30% 5991/com.xxx.launcher: 23% user + 6.4% kernel / faults: 118172 minor(高速缓存的缺页次数) 2 major(内存的缺页次数)
ActivityManager: 16% 6174/launcher-loader: 13% user + 2.8% kernel
ActivityManager: 3.9% 5991/m.xxx.launcher: 3.1% user + 0.8% kernel
ActivityManager: 20% 6549/com.xxx.xxx: 16% user + 3.7% kernel / faults: 3541 minor
ActivityManager: 10% 6889/DBCacheManager: 8.7% user + 1.2% kernel
ActivityManager: 9.4% 6942/DefaultDispatch: 7.1% user + 2.2% kernel

// 1s 内的 各个进程各个线程的 cpu 使用情况
ActivityManager: CPU usage from 246ms to 1271ms later (2020-09-09 02:09:55.188 to 2020-09-09 02:09:56.213):
ActivityManager: 290% 1948/system_server: 114% user + 176% kernel / faults: 9353 minor
ActivityManager: 32% 5159/LockSettingsSer: 29% user + 2.9% kernel
ActivityManager: 25% 8661/AnrConsumer: 8.8% user + 16% kernel
ActivityManager: 44% 5991/com.xxx.launcher: 37% user + 7.4% kernel / faults: 5756 minor
ActivityManager: 16% 6174/launcher-loader: 13% user + 3.7% kernel
ActivityManager: 14% 5991/m.xxx.launcher: 14% user + 0% kernel
ActivityManager: 37% 6549/com.xxx.xxx: 28% user + 9.3% kernel / faults: 153 minor
ActivityManager: 37% 6942/DefaultDispatch: 28% user + 9.3% kernel
ActivityManager: 20% 5962/com.android.phone: 14% user + 5.5% kernel / faults: 1345 minor
ActivityManager: 11% 5962/m.android.phone: 7.4% user + 3.7% kernel

CPU 负载

1
ActivityManager: Load: 15.29 / 5.19 / 1.871

Load 后面的三个数字的意思分别是 1 分钟、5 分钟、15 分钟内系统的平均负荷。当 CPU 完全空闲的时候,平均负荷为 0;当 CPU 工作量饱和的时候,平均负荷为 1,通过 Load 可以判断系统负荷是否过重

有一个形象的比喻:个= CPU 想象成一座大桥,桥上只有一根车道,所有车辆都必须从这根车道上通过,系统负荷为 0,意味着大桥上一辆车也没有,系统负荷为 0.5,意味着大桥一半的路段有车,系统负荷为 1.0,意味着大桥的所有路段都有车,也就是说大桥已经”满”了,系统负荷为 2.0,意味着车辆太多了,大桥已经被占满了 (100%),后面等着上桥的车辆还有一倍。大桥的通行能力,就是 CPU 的最大工作量;桥梁上的车辆,就是一个个等待 CPU 处理的进程(process)

经验法则是这样的

  1. 1.当系统负荷持续大于 0.7,你必须开始调查了,问题出在哪里,防止情况恶化
  2. 2.当系统负荷持续大于 1.0,你必须动手寻找解决办法,把这个值降下来
  3. 3.当系统负荷达到 5.0,就表明你的系统有很严重的问题

现在的手机是多核 CPU 架构,八核的多的是,意味着 Cpu 处理的能力就乘以了8,每个核运行的时间可以从下面的文件中得到,/sys/devices/system/cpu/cpu%d/cpufreq/stats/time_in_state 中读取的,%d 代表是 CPU 的核。文件中记录了 CPU 从开机到读取文件时,在各个频率下的运行时间,单位:10 ms

Memory 负载

1
2
3
4
ActivityManager: ----- Output from /proc/pressure/memory -----(内存压力)
ActivityManager: somavg10=1.35 avg60=0.31 avg300=0.06 total=346727
ActivityManager: full avg10=0.00 avg60=0.00 avg300=0.00 total=34803
ActivityManager: ----- End output from /proc/pressure/memory -----

Memory 负载是从 /proc/pressure/memory 中获取的

avg10、avg60、avg300 分别代表 10s、60s、300s 的时间周期内的阻塞时间百分比。total 是总累计时间,以毫秒为单位

some 这一行,代表至少有一个任务在某个资源上阻塞的时间占比,full 这一行,代表所有的非 idle 任务同时被阻塞的时间占比,这期间 cpu 被完全浪费,会带来严重的性能问题。我们以 IO 的 some 和 full 来举例说明,假设在 60 秒的时间段内,系统有两个 task,在 60 秒的周期内的运行情况如下图所示:

红色阴影部分表示任务由于等待 IO 资源而进入阻塞状态。Task A 和 Task B 同时阻塞的部分为 full,占比 16.66%;至少有一个任务阻塞(仅 Task B 阻塞的部分也计算入内)的部分为 some,占比 50%。

some 和 full 都是在某一时间段内阻塞时间占比的总和,阻塞时间不一定连续,如下图所示:

具体内容可以参考内核工匠的文章: https://blog.csdn.net/feelabclihu/article/details/105534140

IO 负载

1
ActivityManager:   30% 5991/com.xxx.launcher: 23% user + 6.4% kernel / faults: 118172 minor(高速缓存的缺页次数) 2 major(内存的缺页次数)
  1. Minor,高速缓存的缺页次数,指的是 Minor Page Fault(次要页错误,简称 MnPF),磁盘数据被加载到内存后,内核再次读取时,会发出一个 MnPF 信息。 一个文件第一次被读写时会有很多的 MPF,被缓存到内存后再次访问 MPF 就会很少,MnPF 反而变多,这是内核为减少效率低下的磁盘 I/O 操作采用的缓存技术的结果可以理解为进程在做内存访问
  2. Major,内存的缺页次数,指 Major Page Fault(主要页错误,简称 MPF),内核在读取数据时会先后查找 CPU 的高速缓存和物理内存,如果找不到会发出一个 MPF 信息,请求将数据加载到内存可以理解为进程在做 IO 操作

如果有大量的 major,那么说明当时 IO 操作负载比较高

进程负载

1
ActivityManager:   30% 5991/com.xxx.launcher: 23% user + 6.4% kernel / faults: 118172 minor(高速缓存的缺页次数) 2 major(内存的缺页次数)
  1. 1.23% user:用户态的 cpu 占用
  2. 2.6.4% kernel:内核态的 cpu 占用

CPU 异常进程

  1. SystemServer cpu 占用偏高,系统整体运行会缓慢
  2. kswapd0 cpu 占用率偏高,系统整体运行会缓慢,从而引起各种 ANR。把问题转给”内存优化”,请他们进行优化
  3. logd   CPU 占用率偏高,也会引起系统卡顿和 ANR,因为各个进程输出 LOG 的操作被阻塞从而执行的极为缓慢。
  4. Vold 占用 CPU 过高,会引起系统卡顿和 ANR,请负责存储的同学先调查
  5. 应用自身 CPU 占用率较高,高概率应用自身问题
  6. 应用处于 D 状态,发生 ANR,如果最后的操作是 refriger,那么是应用被冻结了,正常情况下是功耗优化引起的,可以找一下前后是否有 xxxHansManager : unfreeze 这样的 Log;或者在 Systrace 中的 Kernel Callstack 显示 :{kernel callsite when blocked:: “__refrigerator+0xe4/0x198”}
  7. CPU 使用率前面的 “+”。部分进程的 CPU 使用率前面有 “+” 号,譬如 cat 和 zygote64,表示在上一次 CPU 统计的时间片段内,还没有这些进程,而这一次 CPU 统计的时间片段内,运行了这些进程。 类似的还有 “-” 号,表示两次 CPU 统计时间片段时,这些进程消亡了

系统关键 Log 介绍

应用冻结

1
xxxHansManager : freeze uid: 10245 package: com.tencent.mm reason: LcdOff

如果冻结逻辑有 Bug,也会导致应用产生 ANR,这一行 Log 比较简单,主要是被冻结的进程信息和被冻结的原因

ActivityManager : Slow operation

AMS 在执行关键任务的时候,如果任务耗时超过 50 ms,则会打印对应的 Log

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

1
2
3
4
5
6
7
void checkTime(long startTime, String where) {
long now = SystemClock.uptimeMillis();
if ((now-startTime) > 50) {
// If we are taking more than 50ms, log about it.
Slog.w(TAG, "Slow operation: " + (now-startTime) + "ms so far, now at " + where);
}
}

对应的 Log 如下,如果系统中频繁打印这种 Log,说明系统目前处于一个比较卡的状态,分析的时候就得考虑到系统的因素

1
2
3
ActivityManager: Slow operation: 138ms so far, now at startProcess: done updating battery stats
ActivityManager: Slow operation: 138ms so far, now at startProcess: building log message
ActivityManager: Slow operation: 138ms so far, now at startProcess: starting to update pids map

Looper : Slow dispatch

1
Looper  : Slow dispatch took 418ms main h=android.app.ActivityThread$H c=android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA@e68bdc4 m=0

Looper : Slow delivery

1
Looper  : Slow delivery took 361ms android.ui h=com.android.server.am.ActivityManagerService$UiHandler c=null m=53

Looper:Slow Looper

1
W/Looper: Slow Looper main: Activity com.androidperformance.memoryfix/.MainActivity is 439ms late (wall=0ms running=0ms ClientTransaction{ callbacks=[android.app.servertransaction.TopResumedActivityChangeItem] }) because of 3 msg, msg 2 took 268ms (seq=2 running=207ms runnable=15ms late=1ms h=android.app.ActivityThread$H w=110), msg 3 took 171ms (seq=3 running=140ms runnable=5ms io=1ms late=268ms h=android.app.ActivityThread$H w=159)

onTrimMemory

1
ClockTag AlarmClockApplication: onTrimMemory: 80

dvm_lock_sample

当某个线程等待 lock 的时间 blocked 超过阈值(比如:500ms),则输出当前的持锁状态.

1
dvm_lock_sample:[system_server,1,Binder_9,1500,ActivityManagerService.java,6403,-,1448,0]
  1. system_server: Binder_9 执行到 ActivityManagerService.java 的 6403 行代码,一直在等待 AMS 锁
  2. “-“代表持锁的是同一个文件,即该锁被同一文件的 1448 行代码所持有, 从而导致 Binder_9 线程被阻塞 1500ms.

binder_sample

binder_sample: 监控每个进程的主线程的 binder transaction 的耗时情况, 当超过阈值(比如:500ms)时,则输出相应的目标调用信息.

1
6628  6628 I binder_sample: [android.view.accessibility.IAccessibilityManager,6,2010,com.xxx.community,100]
  1. 进程是 6628,主线程 6628
  2. 执行 android.view.accessibility.IAccessibilityManager 接口
  3. 所对应方法 code = 6 ( 即 TRANSACTION_addAccessibilityInteractionConnection )
  4. 所花费时间为 2010 ms
  5. 该 block 所在 package 为 com.xxx.community
  6. 最后一个参数是 sample 比例

查找对应的接口函数,比如上面例子里面 IAccessibilityManager 中 Code = 6 是对应的哪个函数,可以在 cs.android.com 里面 搜索 FIRST_CALL_TRANSACTION ,点击调用,然后查看 out 目录里面的对应的 IAccessibilityManager 文件(一直往下滑,直到可以搜索到 IAccessibilityManager )

其中对应的 static final int TRANSACTION_addAccessibilityInteractionConnection = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5)

Long monitor contention

1
16809 14216 W system_server: Long monitor contention with owner InputDispatcher (17039) at android.content.res.Configuration com.android.server.wm.ActivityTaskManagerService.getGlobalConfigurationForPid(int)(ActivityTaskManagerService.java:1066) waiters=0 in boolean com.android.server.wm.WindowProcessController.hasActivities() for 141ms

art/runtime/monitor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string Monitor::PrettyContentionInfo(const std::string& owner_name,
pid_t owner_tid,
ArtMethod* owners_method,
uint32_t owners_dex_pc,
size_t num_waiters) {
Locks::mutator_lock_->AssertSharedHeld(Thread::Current());
const char* owners_filename;
int32_t owners_line_number = 0;
if (owners_method != nullptr) {
TranslateLocation(owners_method, owners_dex_pc, &owners_filename, &owners_line_number);
}
std::ostringstream oss;
oss << "monitor contention with owner " << owner_name << " (" << owner_tid << ")";
if (owners_method != nullptr) {
oss << " at " << owners_method->PrettyMethod();
oss << "(" << owners_filename << ":" << owners_line_number << ")";
}
oss << " waiters=" << num_waiters;
return oss.str();
}

Binder Thread

当 system_server 等进程的线程池使用完, 无空闲线程时, 则 binder 通信都处于饥饿状态, 则饥饿状态超过一定阈值则输出信息.

1
E IPCThreadState: binder thread pool (31 threads) starved for 120 ms

am_kill

1
am_kill : [0,18555,com.sina.weibo.image,945,remove task]1

上面这句话里面,remove task 指的是这个应用被杀的原因

下面是各种被杀的情况所对应的 Reason,有时候我们需要看应用被杀的原因,来判断系统是否正常或者用户的操作步骤:

force-stop

异常杀进程

主动杀进程

调度杀进程

其他杀进程

以上介绍的所有杀进程都是调用 ProcessRecord.kill()方法, 必然会输出相应的 EventLog.那么还有哪些场景的杀进程不会输出 log 呢:

  1. Process.killProcess(int pid) //可杀任何指定进程,或者直接发 signal
  2. adb shell kill -9 pid //可杀任何指定的进程
  3. 直接 lmk 杀进程

也就是说进程被杀而无 log 输出,那么可能是通过直接调用 kill 或者发信号, 再或许是 lmk 所杀.

am_proc_died

1
am_proc_died: [0,13210,com.xxx.gallery3d,935,19]

am_anr

1
am_anr : [0,8769,com.android.updater,952680005,Broadcast of Intent { act=android.intent.action.BOOT_COMPLETED flg=0x9000010 cmp=com.android.updater/.BootCompletedReceiver (has extras) }]
  1. 进程 pid:8769
  2. 进程名:com.android.updater
  3. 发生 ANR 的类型是:BroadcastTimeout 的具体类或者原因:{ act=android.intent.action.BOOT_COMPLETED flg=0x9000010 cmp=com.android.updater/.BootCompletedReceiver (has extras) }

Lowmemorykiller

lowmemorykiller 的 log 在 kernel log 中

1
2
lowmemorykiller: Kill 'com.heytap.themestore' (15545), uid 10136, oom_adj 995 to free 105068kB
lowmemorykiller: Reclaimed 105068kB at oom_adj 606

通过 lowmemorykiller 机制杀应用:

  1. com.heytap.themestore 进程名
  2. 15545:PID
  3. 10136:UID
  4. 995:oom_adj
  5. 105068kB: 释放的内存
  6. 606:min_adj

Blocked msg

内部 加的 Log,当 Message 执行时间超过 1.5s 的时候,就会打印下面的 Log,从下面的 Log 可以看到:

  1. 执行超时的 Message,和这个 Message 执行耗时
  2. 被这个 Message 阻塞的 Message

可以从这个 Log 里面观察是自己的 Message 执行阻塞了别的 Message 还是自己的 Message 被其他的 Message 阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
E ANR_LOG : >>> msg's executing time is too long
E ANR_LOG : Blocked msg = { when=-32s683ms what=110 target=android.app.ActivityThread$H obj=AppBindData{appInfo=ApplicationInfo{bd8d51e com.android.contacts}} } , cost = 32436 ms
E ANR_LOG : >>>Current msg List is:
E ANR_LOG : Current msg <1> = { when=-32s672ms what=140 target=android.app.ActivityThread$H arg1=5 }
E ANR_LOG : Current msg <2> = { when=-32s671ms what=114 target=android.app.ActivityThread$H obj=CreateServiceData{token=android.os.BinderProxy@f7611ff className=com.android.contacts.xxxAppServicesManagerClient packageName=com.android.contacts intent=null} }
E ANR_LOG : Current msg <3> = { when=-32s671ms what=121 target=android.app.ActivityThread$H obj=BindServiceData{token=android.os.BinderProxy@f7611ff intent=Intent { cmp=com.android.contacts/.xxxAppServicesManagerClient }} }
E ANR_LOG : Current msg <4> = { when=-31s658ms what=1 target=android.os.AsyncTask$InternalHandler obj=android.os.AsyncTask$AsyncTaskResult@75e517c }
E ANR_LOG : Current msg <5> = { when=-29s750ms what=140 target=android.app.ActivityThread$H arg1=10 }
E ANR_LOG : Current msg <6> = { when=-29s103ms what=118 target=android.app.ActivityThread$H obj={1.0 460mcc3mnc [zh_CN] ldltr sw360dp w360dp h622dp 480dpi nrml long port finger -keyb/v/h -nav/h appBounds=Rect(0, 0 - 1080, 1920) s.10mThemeChanged = 0mThemeChangedFlags = 0mFlipFont = 0}
E ANR_LOG : Current msg <7> = { when=-28s370ms what=118 target=android.app.ActivityThread$H obj={1.0 460mcc11mnc [zh_CN] ldltr sw360dp w360dp h622dp 480dpi nrml long port finger -keyb/v/h -nav/h appBounds=Rect(0, 0 - 1080, 1920) s.11mThemeChanged = 0mThemeChangedFlags = 0mFlipFont = 0} }
E ANR_LOG : Current msg <8> = { when=-27s821ms what=122 target=android.app.ActivityThread$H obj=BindServiceData{token=android.os.BinderProxy@f7611ff intent=Intent { cmp=com.android.contacts/.xxxAppServicesManagerClient }} }
E ANR_LOG : Current msg <9> = { when=-27s821ms what=116 target=android.app.ActivityThread$H obj=android.os.BinderProxy@f7611ff }
E ANR_LOG : Current msg <10> = { when=-27s654ms what=114 target=android.app.ActivityThread$H obj=CreateServiceData{token=android.os.BinderProxy@e23cf1b className=com.android.contacts.xxxAppServicesManagerClient packageName=com.android.contacts intent=null} }
E ANR_LOG : >>>CURRENT MSG DUMP OVER<<<

通用场景 Log 介绍

灭屏 Log

关键是 power_screen_broadcast_done、screen_toggled 和 power_screen_state

1
2
3
4
5
6
7
8
9
10
11
12
1810  2470 I intercept_power: [ACTION_DOWN,1,0]
1810 2470 I intercept_power: [ACTION_UP,0,0]
1810 2470 I power_sleep_requested: 0

1810 1810 I power_screen_state: [0,2,0,0,1254]
1810 1810 I screen_toggled: 0 // 0 代表灭屏,1 代表亮屏
1810 1810 I power_screen_broadcast_send: 1
1810 1825 I wm_set_keyguard_shown: [1,0,0,setKeyguardShown] // (keyguardShowing|1),(aodShowing|1),(keyguardGoingAway|1),(Reason|3)

2768 2768 I sysui_status_bar_state: [1,1,0,0,0,1]

1810 1810 I power_screen_broadcast_done: [0,611,1] // 0 代表灭屏,611 是灭屏时间

亮屏 Log

关键是 power_screen_broadcast_done、screen_toggled 和 power_screen_state

其中
power_screen_state

  1. 1.offOrOn|1|5
  2. 2.becauseOfUser|1|5
  3. 3.totalTouchDownTime|2|3
  4. 4.touchCycles|1|1
  5. 5.latency|1|3
1
2
3
4
5
6
1810  2470 I intercept_power: [ACTION_DOWN,1,0]
1810 2470 I intercept_power: [ACTION_UP,1,0]
1810 1810 I screen_toggled: 1 // 1 代表亮屏
1810 1810 I power_screen_broadcast_send: 1
1810 1810 I power_screen_state: [1,0,0,0,319]
1810 1810 I power_screen_broadcast_done: [1,633,1] // 1 代表亮屏,633 是亮屏时间

解锁完整 Log

wm_set_keyguard_shown 的值

  1. (keyguardShowing|1) :Keyguard 是否在显示
  2. (aodShowing|1) :aodShowing 是否在显示
  3. (keyguardGoingAway|1) :Keyguard 消失
  4. (Reason|3) :原因
1
2
3
4
5
6
7
8
9
10
 1810  5711 I wm_set_keyguard_shown: [1,0,1,keyguardGoingAway]
// 桌面 Resume
1810 5711 I wm_set_resumed_activity: [0,com.xxx.launcher/.Launcher,resumeTopActivityInnerLocked]
1810 5711 I wm_resume_activity: [0,93093883,63,com.xxx.launcher/.Launcher]
20615 20615 I wm_on_restart_called: [93093883,com.xxx.launcher.Launcher,performRestartActivity]
20615 20615 I wm_on_start_called: [93093883,com.xxx.launcher.Launcher,handleStartActivity]
20615 20615 I wm_on_resume_called: [93093883,com.xxx.launcher.Launcher,RESUME_ACTIVITY]
20615 20615 I wm_on_top_resumed_gained_called: [93093883,com.xxx.launcher.Launcher,topWhenResuming]

1810 5711 I wm_set_keyguard_shown: [0,0,1,setKeyguardShown] // keyguard 不显示,aod 不显示,keyguardGoingAway,原因是调用了 setKeyguardShown 显示桌面

通知栏下拉完整 Log

1
2
3
4
5
6
2768  2768 I sysui_multi_action: [757,803,799,panel_open,802,1]
2768 2768 I sysui_multi_action: [757,1328,758,4,1326,29,1327,0,1329,0]
1810 3004 I notification_visibility: [-1|android|55|null|1000,1,457483196,457483196,0,4]
1810 3004 I notification_visibility: [0|com.android.systemui|10005|null|10132,1,97168545,97168545,0,2]
1810 3004 I notification_visibility: [0|com.android.systemui|2147483647|ranker_group|10132|ranker_group,1,97168236,97168236,0,1]
1810 3004 I notification_visibility: [-1|com.android.systemui|10006|null|10132,1,82586063,82586063,0,0]

通知栏折叠完整 Log

1
2
3
4
5
2768  2768 I sysui_multi_action: [757,111,758,2]
1810 5473 I notification_visibility: [-1|android|55|null|1000,0,457546516,457546516,0,4]
1810 5473 I notification_visibility: [0|com.android.systemui|10005|null|10132,0,97231865,97231865,0,2]
1810 5473 I notification_visibility: [0|com.android.systemui|2147483647|ranker_group|10132|ranker_group,0,97231557,97231557,0,1]
1810 5473 I notification_visibility: [-1|com.android.systemui|10006|null|10132,0,82649384,82649384,0,0]

应用启动完整 Log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建 Task
1810 5473 I wm_task_created: [100,-1]
1810 5473 I wm_stack_created: 100
1810 5473 I wm_create_task: [0,100]

// 创建 Activity
1810 5473 I wm_create_activity: [0,231348670,100,com.androidperformance.memoryfix/.MainActivity,android.intent.action.MAIN,NULL,NULL,270532608]

// 桌面走 pause 流程
1810 5473 I wm_pause_activity: [0,93093883,com.xxx.launcher/.Launcher,userLeaving=true]
0615 20615 I wm_on_top_resumed_lost_called: [93093883,com.xxx.launcher.Launcher,topStateChangedWhenResumed]
0615 20615 I wm_on_paused_called: [93093883,com.xxx.launcher.Launcher,performPause]
1810 3720 I wm_add_to_stopping: [0,93093883,com.xxx.launcher/.Launcher,makeInvisible]

//启动 Activity
1810 2045 I am_proc_start: [0,18803,10263,com.androidperformance.memoryfix,pre-top-activity,{com.androidperformance.memoryfix/com.androidperformance.memoryfix.MainActivity}]
1810 3428 I am_proc_bound: [0,18803,com.androidperformance.memoryfix]
1810 3428 I wm_restart_activity: [0,231348670,100,com.androidperformance.memoryfix/.MainActivity]
1810 3428 I wm_set_resumed_activity: [0,com.androidperformance.memoryfix/.MainActivity,minimalResumeActivityLocked]
8803 18803 I wm_on_create_called: [231348670,com.androidperformance.memoryfix.MainActivity,performCreate]
8803 18803 I wm_on_start_called: [231348670,com.androidperformance.memoryfix.MainActivity,handleStartActivity]
8803 18803 I wm_on_resume_called: [231348670,com.androidperformance.memoryfix.MainActivity,RESUME_ACTIVITY]
8803 18803 I wm_on_top_resumed_gained_called: [231348670,com.androidperformance.memoryfix.MainActivity,topStateChangedWhenResumed]
1810 2034 I wm_activity_launch_time: [0,231348670,com.androidperformance.memoryfix/.MainActivity,471]

// 桌面走 stop 流程
1810 1978 I wm_stop_activity: [0,93093883,com.xxx.launcher/.Launcher]
0615 20615 I wm_on_stop_called: [93093883,com.xxx.launcher.Launcher,STOP_ACTIVITY_ITEM]

Window Focus 相关的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 从桌面启动 App,focus 变化 :Launcher => null => App
WindowManager: Changing focus from Window{b0416d7 u0 com.xxx.launcher/com.xxx.launcher.Launcher} to null,diplayid=0
WindowManager: Changing focus from null to Window{10f5145 u0 com.android.settings/com.android.settings.Settings},diplayid=0

// 从 App 返回桌面,focus 变化 :App => null => Launcher
WindowManager: Changing focus from Window{10f5145 u0 com.android.settings/com.android.settings.Settings} to null,diplayid=0
WindowManager: Changing focus from null to Window{b0416d7 u0 com.xxx.launcher/com.xxx.launcher.Launcher},diplayid=0

// 从 App 界面进入锁屏:focus 变化 :App => null => 锁屏
WindowManager: Changing focus from Window{10f5145 u0 com.android.settings/com.android.settings.Settings} to null,diplayid=0
WindowManager: Changing focus from null to Window{82e5f30 u0 NotificationShade},diplayid=0

// 从锁屏界面解锁进入 App,focus 变化 :锁屏 => App
WindowManager: Changing focus from Window{82e5f30 u0 NotificationShade} to Window{10f5145 u0 com.android.settings/com.android.settings.Settings},diplayid=0

ANR 相关资料分享

  1. 西瓜视频稳定性治理体系建设一:Tailor 原理及实践
  2. 西瓜视频稳定性治理体系建设二:Raphael 原理及实践
  3. 西瓜视频稳定性治理体系建设三:Sliver 原理及实践
  4. 西瓜卡顿 & ANR 优化治理及监控体系建设
  5. 今日头条 ANR 优化实践系列 - 设计原理及影响因素
  6. 今日头条 ANR 优化实践系列 - 监控工具与分析思路
  7. 今日头条 ANR 优化实践系列分享 - 实例剖析集锦
  8. 今日头条 ANR 优化实践系列 - Barrier 导致主线程假死
  9. 今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
  10. Android ANR|原理解析及常见案例

参考资料

  1. https://duanqz.github.io/2015-10-12-ANR-Analysis#1-%E6%A6%82%E8%A7%88
  2. https://duanqz.github.io/2015-10-12-ANR-Analysis
  3. http://gityuan.com/2016/12/02/app-not-response/
  4. http://gityuan.com/2017/01/01/input-anr/
  5. https://xiaozhuanlan.com/topic/5097486132

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

🔲 ☆

Android ANR 系列 1 :理解 Android ANR 设计思想

本文为 Android App ANR 系列的第一篇,主要是从系统的角度来剖析 Android ANR 的设计思想,系列文章目录如下

  1. Android App ANR 系列 1 :理解 Android ANR 设计思想
  2. Android App ANR 系列 2 :ANR 分析套路和关键 Log 介绍
  3. Android App ANR 系列 3 :ANR 案例分享

一、ANR 的普遍性与复杂性

在 Android 生态系统中,应用无响应(ANR,Application Not Responding)不仅是开发者面临的常见挑战,更是系统设计哲学的核心体现。虽然 ANR 常被简化为“主线程耗时操作”的代名词,但这种表面化的理解远远不足以揭示问题的本质。实际上,ANR 的根本原因在于 Android 多进程架构、事件分发和资源调度机制之间复杂的协同作用,其实质是系统层面对应用行为实施严格约束与监控的综合体现。

Android 明确将 ANR 定性为应用层问题,这与 SNR(System Not Responding)形成了鲜明对比。SNR 指的是系统进程(如 system_server)失去响应能力,通常依靠 Watchdog 机制通过监控关键系统线程状态来实现;而 ANR 则依托于消息调度机制,系统进程利用精心设计的超时模型追踪应用主线程的响应能力。这种区别反映了系统针对不同层级问题所采取的治理策略:

  • SNR 侧重于确保系统核心服务的生存,采用主动轮询的监控方式;
  • ANR 则聚焦于应用进程的实时响应,通过事件驱动的异步检测机制来进行判断。

从系统架构的角度来看,ANR 机制主要实现于系统层(即 system_server 进程中),其核心在于构建跨进程的事件监控体系。当应用进程通过 Binder 向系统服务发起操作请求(如启动 Activity、处理广播等)时,系统会同步启动超时计时器;而对于输入事件这类异步操作,InputDispatcher 会通过 socket 与窗口建立事件通道,并在事件派发后启动超时检测。这种分层监控设计充分体现了 Android 针对不同任务类型所采用的差异化处理策略。

ANR 机制的深层意义在于平衡开放性与系统可控性。作为一个开放平台,Android 允许应用自由申请硬件资源(如 CPUIO内存 等),但必须通过严格的规则防止单个应用的异常行为蔓延至整个系统。当检测到超时事件时,系统会启动多维度熔断机制:首先,通过强制终止问题进程来释放关键系统资源(例如防止其占用 Binder 线程池或文件描述符),从而避免级联故障;同时,系统会冻结进程状态,并采集 CPU 使用率、线程堆栈、内存快照等关键信息,将这些数据写入 /data/anr/traces.txt,为后续问题分析保留现场。更巧妙的是,系统还通过用户可见的弹窗将最终操作权交还给用户,从而既避免了自动化处理可能带来的误判风险,又保持了人机交互的连续性。这种“故障隔离—现场保护—用户决策”相结合的设计,充分展现了 Android 在技术严谨性与用户体验友好性之间的平衡智慧。


二、ANR 的核心设计哲学

ANR 的本质:系统级监控与强制干预

ANR 机制构成了 Android 系统架构中一套深层次的稳定性防御体系,其核心在于通过 跨层协同监控异步决策隔离 构建一个独立于应用状态的全局安全网。这种设计远非简单的超时检测,而是深深植根于 Linux 进程沙箱机制和 Android 组件架构的有机结合中。系统进程(如 system_server)通过 ActivityManagerService (AMS) 与 InputManagerService (IMS) 这两大核心模块,分别对组件生命周期和输入事件流实施全方位监控。正因这种分层架构,监控逻辑得以与业务逻辑解耦,即使目标应用的主线程完全阻塞,系统依然可以依靠独立线程进行超时裁决,从根本上避免了“监控者反被监控对象拖垮”的风险。

在实现层面,ANR 充分体现了事件驱动型系统设计的精髓。例如,在组件类 ANR 的场景中,当 AMS 通过 Binder 向应用进程派发跨进程任务时,系统会同步启动一个倒计时器(例如针对 Service 启动的 20 秒阈值),这一“埋炸弹”机制实质上将异步任务转化为带有超时约束的同步契约。应用进程在完成任务后必须通过 Binder 回调主动“拆弹”,否则系统将介入收集现场信息(如主线程堆栈)并触发用户交互。整个过程由系统进程主导,应用仅作为事件响应方存在,从而确保监控的绝对权威性。

任务派发依赖 Binder 的同步调用来确保原子性,同时 AMS 通过专门的 Handler 将超时检测消息推入消息队列,从而在规定时间内监控任务的执行情况。这种设计既保证了跨进程通信期间任务的完整性,也能在超时后迅速触发熔断处理。


组件类 ANR:异步任务的全局防护逻辑

组件类 ANR 的监控逻辑围绕 ActivityManagerService (AMS) 展开,其本质是通过 任务派发–回调–熔断 的三阶段模型,实现对异步任务生命周期的全链路追踪。当系统通过 Binder 跨进程通信向应用派发任务(例如启动 Service)时,AMS 会同步启动超时检测机制:利用 MainHandler 发送延迟消息实现精确计时。以 Service 启动为例,当 AMS 调用 IApplicationThreadscheduleCreateService() 后,会启动对应的超时监控(默认 20 秒)。如果应用在规定时间内未通过 serviceDoneExecuting() 回调通知 AMS,则触发 ANR 判定。

开发者需要特别注意 跨进程回调的时序陷阱:即使异步任务在子线程完成,但若主线程因消息队列堵塞(例如过度调用 runOnUiThread)导致 Binder 回调延迟,系统依然会判定为 ANR。

Android 14+ 引入的 ProcessStateRecord 对进程状态进行了更细粒度的划分,不仅详细记录了主线程消息处理的状态,还实时监控后台任务和挂起状态,从而降低了误判率,并为开发者提供了更丰富的调试信息。

这一设计的关键在于 同步事务与异步熔断的解耦。任务派发依靠 Binder 的同步调用确保原子性,而超时检测则通过 Handler 消息机制异步执行,避免阻塞系统主线程。

当 ANR 触发时,系统会执行多维度的熔断策略:

  1. 现场采集
    系统会收集主线程堆栈信息、CPU 使用情况、进程状态等关键数据,并将这些数据写入 /data/anr/traces.txt 文件。同时,系统还会利用 ProcessCpuTracker 记录详细的 CPU 使用统计,为后续问题分析提供依据。

  2. 资源隔离
    系统通过 ProcessRecord 的调度优先级调整机制确保熔断决策的实时性,从而保证即使在系统负载较高的情况下,ANR 处理流程也能得到及时执行。

  3. 诊断数据收集
    系统提供 ApplicationExitInfo API,允许开发者查询历史 ANR 记录,包括发生时间、进程状态、异常堆栈等详细信息,这些数据对于问题复现和根因分析极为重要。

值得注意的是,Android 15 对后台服务施加了更严格的约束:前台服务必须在 3 秒内完成初始化并调用 startForeground(),否则系统将直接触发 ANR。具体而言,系统通过内部属性(例如 persist.sys.fgs_timeout)以及 API 参数(如 AMS 内部控制前台服务启动超时的参数)来管理这一超时机制。开发者可参照最新的 API 文档了解这些变更,从而在设计服务时确保满足严格的响应时限要求。

系统还提供了多种工具来支持 ANR 问题的诊断和分析:

  • 系统日志收集:开发者可通过 adb 命令获取 ANR 堆栈信息和系统报告,其中包含了问题发生时的详细系统状态。
  • 性能分析工具:Android Studio 的 CPU Profiler 能够实时监控应用性能,帮助开发者发现潜在的性能问题。
  • 系统级分析:Perfetto 提供了强大的系统级性能分析能力,帮助开发者理解复杂的性能问题。

通过这种多层次的监控和防护机制,Android 系统确保了应用响应性能的可靠性,并为开发者提供了完整的问题诊断工具链。开发者需深入理解这些机制,在应用设计中充分考虑性能因素,遵循系统生命周期契约,合理管理主线程负载,确保关键回调的及时响应。

这种设计哲学体现了 Android 平台对应用质量的严格要求:通过明确的超时限制和完善的监控机制,推动开发者构建更可靠、响应更及时的应用。同时,丰富的诊断工具也为开发者提供了必要的支持,帮助他们在遇到问题时能快速定位并解决问题。


Input 类 ANR:输入事件分发的动态熔断体系

输入类 ANR 的监控机制更为复杂,其核心挑战在于如何在 高实时性要求资源高效利用 之间取得平衡。从硬件事件产生到应用主线程处理,输入系统通过 EventHubInputReaderInputDispatcher 三大组件协同构建了一条高效且可控的事件分发链路。

  • 事件读取层 (EventHub)
    EventHub 利用 Linux 的 epoll 机制监听 /dev/input 设备节点,支持多设备并发监听,并通过事件驱动模型(而非轮询)实现零空闲 CPU 消耗。当硬件中断触发时,系统通过 inotify 接收原始输入数据,并将其封装为 RawEvent

  • 事件预处理 (InputReader)
    InputReader 通过特定的 InputMapper 对原始数据进行设备相关的预处理(如触摸校准),并将其转换为标准的输入事件(如 MotionEventKeyEvent)。同时,根据设备类型和配置进行必要的事件过滤,确保数据质量。

  • 事件分发层 (InputDispatcher)
    InputDispatcher 的核心职责是确定当前焦点窗口,并通过基于 Unix Domain Socket 的 InputChannel 将事件推送至应用进程。它采用 multiplexing 机制高效管理多个 InputChannel,并依赖 WindowManagerService 获取最新窗口焦点信息,确保事件准确送达目标窗口。

输入 ANR 机制依赖于对事件状态的持续追踪与超时判定,其核心在于 队列状态管理跨线程协作模型 的设计:

  • inboundQueue:存储从 InputReader 接收的待分发事件
  • outboundQueues:为每个连接维护的输出队列,同时通过 waitQueue 跟踪已分发但未收到完成响应的事件
  • waitQueue:记录已经分发出去但尚未收到应用端处理确认的事件。

当事件被分发后,系统通过 MonitoredTimeout 机制跟踪其处理状态。默认超时时间为 5000 毫秒(可通过系统属性调整),超时检测采用事件驱动模式,在新事件到达、应用回调完成或周期性心跳检查时触发。一旦检测到超时,系统会通过 WindowManagerService 通知 ActivityManagerService,并收集包括 InputDispatcher 状态及应用进程信息在内的诊断数据,随后可能触发 ANR 弹窗及进程重启流程。

整个输入系统采用了优化的线程模型设计:

  • InputReaderThread 专注于事件读取与预处理
  • InputDispatcherThread 负责事件分发与超时监控

两者通过无锁队列实现高效的线程间通信,使得即使某个应用进程的主线程阻塞,系统层面的输入处理依然能够正常运行,从而有效防止问题扩散。

对于开发者而言,应特别关注主线程的响应性,避免在输入事件处理回调中执行耗时操作。同时,理解输入系统的分层设计有助于在性能优化时从整体角度提高事件处理链路的效率。


No Focused Window 类 ANR

No Focused Window ANR 是输入系统中另一类重要的无响应场景,其本质在于窗口焦点状态异常,导致输入事件无法正确分发。与常规输入超时不同,这类 ANR 反映的是 WindowManager 子系统与输入系统之间的协同问题。

WindowManagerService (WMS) 的设计中,窗口焦点管理是一个独立而复杂的子系统。当用户界面发生变化(如 Activity 切换或对话框弹出)时,系统会触发一系列窗口事务操作:首先对旧窗口执行 relayoutWindow 以解除焦点标记,然后为新窗口执行 addWindow 并授予焦点。这些状态变化会通过 WindowManagerPolicy 实时同步至 InputDispatcher,确保输入事件能够路由到当前的焦点窗口。

焦点的获得与丢失由多种系统行为触发。例如:

  • 焦点获得:新 Activity 启动完成并显示第一帧、DialogPopupWindow 弹出、分屏模式下触碰窗口区域、从后台任务切换器中恢复应用、解锁后前台应用恢复。
  • 焦点丢失:Activity 被全屏 Activity 覆盖、用户按下 Home 键、系统弹出权限请求等关键级别 Dialog、应用进入后台、设备锁屏等情况。

No Focused Window ANR 往往与窗口生命周期管理异常有关。最常见的情况是在 Activity 切换过程中,由于目标 Activity 的 handleResumeActivity 执行延迟,系统在一定时间内无法确定合法的焦点窗口。与输入超时 ANR 不同,输入超时是目标窗口存在但未及时处理事件,而 No Focused Window ANR 则是系统无法找到合适的事件接收者。基于这一区别,系统对这两类情况采取不同的防护策略:对于输入超时,系统会在默认 5 秒后触发 ANR;而对于无焦点窗口情况,如果连续多次事件分发找不到目标窗口,系统会更快地启动 ANR 流程。

从应用开发角度来看,影响焦点切换的代码路径较为有限,主要涉及 Activity 生命周期回调、窗口添加/移除以及输入事件处理等环节。即使这些环节出现问题(如主线程阻塞),通常也会触发常规 ANR,而非 No Focused Window ANR。因此,在遇到此类问题时,更应关注系统整体资源使用状态、 system_server 进程的 CPU 负载以及系统服务间 Binder 调用延迟等系统级指标,而不是单纯聚焦于某个应用的代码优化。这也是为什么 No Focused Window ANR 常被视为系统性能问题而非应用质量问题的根本原因。


系统设计的统一性原则

无论是组件类 ANR 还是 Input 类 ANR,其监控机制均遵循以下核心原则:

  1. 状态可追踪性
    通过队列(如 waitQueue)和定时器(例如 AMS 中的 Handler)精确追踪任务进度,确保系统始终掌握应用行为的最新状态。
  2. 故障隔离性
    在超时后迅速终止问题进程,防止局部故障扩散成系统级雪崩。
  3. 用户控制权兜底
    通过弹窗提示与进程终止机制,确保用户始终拥有最终的操作权,即使应用内部已完全失控。
  4. 开发者约束性
    强制要求主线程保持轻量与异步设计,促使应用架构更贴合系统设计哲学。

从架构角度来看,ANR 机制是 Android 系统对 开放生态可控性 的最终回答——它既允许开发者自由创新,又通过刚性规则划定行为边界。这种平衡不仅体现在技术实现上,也深刻影响了整个 Android 应用的性能优化文化。

ANR 问题的全局解析与主动防御

ANR 问题的复杂性要求我们在分析框架中同时具备 技术纵深感系统全局观,并通过递进逻辑将碎片化的现象转化为一个可演进的认知体系。这种方法不仅仅是简单的目录分层,而是利用多维视角的交叉验证,建立从微观代码缺陷到宏观系统约束的完整映射关系。


从现象到根源:逐层解剖 ANR 问题

构建纵向分析路径遵循 “现象 → 机制 → 支撑 → 资源” 的链条式逻辑,其目标在于厘清从用户看到的 ANR 弹窗到硬件资源问题之间的完整链条:

  1. 机制表象(ANR 弹窗)
    作为用户可见的最外层现象,ANR 弹窗实际上是系统对故障的最终裁决——它并不揭示具体根因,而只是展示结果。开发者往往仅停留在查看堆栈日志、寻找主线程阻塞点的层面,但这就如同只观察火山喷发而忽略了地壳运动的根本驱动因素。

  2. 系统实现(AMS/InputDispatcher)
    深入系统服务层,ANR 弹窗背后隐藏着 AMS 的 appNotResponding 触发流程。AMS 通过 Binder 事务状态机追踪组件的生命周期,而 InputDispatcher 则利用 socket 事件流监控输入响应。此层分析揭示了 超时判定逻辑的差异性:AMS 采用同步阻塞式检测(例如 BroadcastQueue 的超时计算),而 InputDispatcher 则利用基于 epoll 的异步非阻塞模型实现事件循环监控。

  3. 底层支撑(Binder/调度器)
    系统服务的高效运行依赖于 Linux 内核的核心机制。Binder 驱动通过内存映射实现跨进程通信,其线程池调度策略(例如 BINDER_MAX_POOL_THREADS 阈值限制)直接影响事务处理能力;而系统的公平调度机制则通过动态分配 CPU 时间片决定主线程是否能够及时获得执行资源。此层的关键在于解析资源分配公平性与实时性之间的矛盾——例如,为了保障多任务的公平性,系统可能允许后台进程的 CPU 密集型任务抢占前台应用的响应时间。

  4. 硬件资源(CPU/IO/Memory)
    最终,所有软件行为都受限于物理硬件。CPU 的乱序执行可能导致锁竞争问题的随机性;磁盘 I/O 延迟会放大主线程在 SharedPreferences 写入时的阻塞;内存带宽争抢则可能使 RenderThread 无法及时获取纹理数据。此层要求建立 硬件指标与软件行为的关联模型,例如利用 perf 工具分析 CPU 缓存命中率与 ANR 触发频率的相关性。

这种纵向深入并非线性递进,而是一个 循环验证 的过程:当硬件层分析发现内存带宽瓶颈时,需要回溯到 Binder 驱动层,检查是否因频繁跨进程通信引发内存拷贝风暴,最终在系统服务层进行数据传输机制的优化。


从被动应对到主动防御:ANR 治理的三步走

方法论的演进路径——“诊断 → 追踪 → 预测 → 设计”,反映了技术认知成熟度的跃迁,具体步骤包括:

  1. 堆栈分析
    传统 ANR 分析依赖于 traces.txt 中的线程堆栈,这本质上是故障发生时的静态快照。当问题由偶发竞争条件(如 Binder 线程池瞬时饱和)引起时,堆栈可能显示为正常的 NativePollOnce 状态,而无法揭示真实的资源争抢过程。此时,需要引入 多时间点堆栈对比技术,通过比较 ANR 前后 5 秒内的堆栈变化,识别线程状态迁移模式。

  2. 动态追踪
    利用 systraceperfetto 等工具提供的毫秒级事件追踪能力,可以监控主线程 Looper 的事件处理周期,量化 dispatchMessage 的执行耗时;同时结合 Binder 驱动中的 binder_transaction 事件,可以绘制跨进程调用的热力图。动态追踪的核心价值在于 揭示隐藏的时间相关性,例如发现输入事件延迟往往紧随 SharedPreferences 磁盘写入操作出现。

  3. 机器学习预测
    当 ANR 的根因涉及多个子系统交互(如 CPU 调度、内存回收和 I/O 负载的耦合效应)时,传统方法难以处理高维数据。通过收集线程状态、Binder 交互数据以及 CPU 争夺情况等 20 多项指标,利用机器学习算法建立分析模型,可以自动识别 ANR 类型(例如主线程阻塞、IPC 死锁或资源竞争)。Google 已在 Android Vitals 中应用类似技术,实现了 ANR 根因的云端聚合分析。

  4. 架构预防性设计
    终极目标是从代码设计阶段就内化系统约束,例如:

    • 通信拓扑约束:限制跨进程调用层级,避免 A → B → C 的链式调用,改用事件总线广播模式。
    • 资源预算管理:为每个业务模块分配 Binder 事务配额,超出阈值时自动降级。
    • 异步边界强化:利用 HandlerThreadExecutor 严格隔离同步与异步操作,防止线程模型出现混乱。

这种从被动应对到主动防御的方法论进化路径,不仅为系统从根源上预防 ANR 提供了有效策略,也为开发者提供了丰富的诊断工具和优化思路。

ANR 相关资料分享

  1. 反思|Android 输入系统 & ANR机制的设计与实现
  2. 西瓜视频稳定性治理体系建设一:Tailor 原理及实践
  3. 西瓜视频稳定性治理体系建设二:Raphael 原理及实践
  4. 西瓜视频稳定性治理体系建设三:Sliver 原理及实践
  5. 西瓜卡顿 & ANR 优化治理及监控体系建设
  6. 今日头条 ANR 优化实践系列 - 设计原理及影响因素
  7. 今日头条 ANR 优化实践系列 - 监控工具与分析思路
  8. 今日头条 ANR 优化实践系列分享 - 实例剖析集锦
  9. 今日头条 ANR 优化实践系列 - Barrier 导致主线程假死
  10. 今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
  11. Android ANR|原理解析及常见案例

参考资料

  1. https://duanqz.github.io/2015-10-12-ANR-Analysis#1-%E6%A6%82%E8%A7%88
  2. https://duanqz.github.io/2015-10-12-ANR-Analysis
  3. http://gityuan.com/2016/12/02/app-not-response/
  4. http://gityuan.com/2017/01/01/input-anr/
  5. https://xiaozhuanlan.com/topic/5097486132

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

🔲 ⭐

Android Weekly 2025-04 期

Android Weekly 是一份专注于 Android 技术生态的周刊,每周一更新。本周刊深入挖掘 Android 系统架构、性能优化、跨平台开发、AI 等领域的高质量技术内容,为开发者提供持续的知识更新与技术洞察。

本周刊可以通过微信公众号、知乎专栏、掘金专栏、个人博客、竹白等平台订阅和阅读。

技术文章

  1. 深入 Flutter 和 Compose 的 PlatformView 实现对比,它们是如何接入平台控件 : 本文深入对比了 Flutter 和 Compose 在 PlatformView 实现上的差异,重点分析了它们接入平台控件的方式、技术实现和适配场景的不同。文章详细展示了 Flutter 的三种模式(VD、HC、TLHC)的演变及其优缺点,同时探讨了 Compose 如何通过 AndroidView 将传统 View 集成到其 UI 渲染树中,并分析了两者在 SurfaceView 支持上的关键区别。
  1. Now In Android 精讲 5 - Data Layer : 文章主要精讲了 Android 中的 Data Layer,包括学习前需解决的一系列问题,通过代码示例讲解了 Repository 的使用及设计原则,阐述了 Repository 与 DataSource 的关系,介绍了离线优先业务,如模块设计、读写策略、同步设计等,最后总结鼓励对照代码学习并应用。

  2. DeepSeek-R1 发布,性能对标 OpenAI o1 正式版 : DeepSeek-R1 发布,性能媲美 OpenAI o1 正式版,并开源模型权重。DeepSeek-R1 采用 MIT License,支持模型蒸馏,API 和应用同步上线,性能在数学、代码、自然语言推理等领域表现优异。小模型蒸馏版本也已开源,协议调整为更宽松的 MIT License,明确支持用户进行模型蒸馏。API 服务定价透明,用户可通过官网或 App 体验新功能。

  3. 内核空间内存 profiler: memprofiling : 内核空间内存使用情况剖析器 memprofiling 由 Kent Overstree 和 Google 的 Suren Baghdasaryan 开发,能够直观展示内核内存的分配情况,包括分配者及分配对象数量。

  4. Android 15 内存追踪利器:ProfilingManager! : Android 15 内存追踪利器:ProfilingManager

  5. 两个 display driver 导致的问题 : 两个 display driver 导致的问题,第一个问题是 MTK 平台的 PQ 功能存在 bug,关闭 PQ 后解决了 settings 界面滑动花屏的问题。第二个问题是项目在开启 doze_suspend 后出现黑屏和 SF 的 DMA Buffer 泄露,原因是驱动在适配 doze suspend 功能时屏幕上下电逻辑存在 bug。

  6. 案例分享:数据库 sqlite3 访问 SIGBUS 崩溃问题 : 案例分享:文章探讨了一个与数据库 sqlite3 访问相关的 SIGBUS 崩溃问题,分析了其发生原因、定位方法及解决过程,最终确认是由于多线程并发操作导致文件锁丢失引发的数据库访问错误。

  7. AGI 前夜的思考 [译] : 这篇文章探讨了人工通用智能(AGI)即将到来的影响,分析了技术发展、社会变革、潜在风险及应对策略。作者认为我们正处于历史性时刻,未来将充满惊人的可能性,也伴随着巨大的挑战。文章呼吁每个人发挥作用,共同塑造一个积极的未来。

  8. 快让 Appium 自动化测试你的 App 吧 : 文章主要介绍了 Appium 自动化测试流程,包括环境配置(安装 appium、驱动,配置环境变量,选择 Python 语言等),使用方法如打开目标 Activity、查看布局元素、尝试点击事件,还提及相关工具及可能遇到的问题。

  9. 一些“小模型”的使用案例 : 在 Hacknews 上有一个讨论很火,就是大家都用小参数的语言模型做什么,有没有什么好的使用案例。我把这些案例整理汇总了一下,大约有六类:(1)文本分类与信息提取、(2)办公与生产力辅助、(3)对话/消息处理与辅助回复、(4)网页/应用集成与自动化、(5)娱乐、创作与游戏、(6)模型部署、技术瓶颈与思考。

  10. Episode 212: Happy birthday, Android Studio! : In this episode Chet, Romain and Tor chat with Xav and Jamal from the Android Studio team to talk about the history of Android’s IDE.

  11. Android 车机 Car 模式原理 : 文章主要介绍了 Android 车机的 Car 模式原理,包括车机系统 Android Automotive OS 的特点,Car 模式中的各个模块如 Car API、Car Service、Vehicle HAL 的功能、使用方法、类图等,还讲解了 MCU 报文通讯流程及相关开发维护工作。

  12. Android 字节码处理-ASM 入门开胃菜 : 文章主要介绍了 Android 字节码处理相关内容,包括编译流程、字节码修改阶段、ASM 框架(简介、两种模式、执行模型、核心组件)、类结构及描述符等,还提到 ASM 学习曲线陡峭,建议查阅官网和示例代码。

  13. Now in Android: 113 - Android 16 Developer Preview 2, Android XR, Android Studio Ladybug, and more! : Welcome to Now in Android, your ongoing guide to what’s new and notable in the world of Android development. In this episode, we’ll cover updates on the Second Developer Preview of Android 16, Android XR, Spotlight Week on Android Camera and Media, Android Studio Ladybug Feature Drop and more!

  14. Android WebView 中网页被劫持的原因及解决方案 :这篇文章探讨了 Android WebView 中网页被劫持的原因及解决方案。原因包括 JavaScript 重定向、恶意网页等多种情况。解决方案有使用 HTTPS、验证 URL 等措施,并提供了相应代码案例。还通过案例深入分析了问题,得出网页被劫持可能由多种因素导致的结论。

  15. Kotlin 技术月报 | 2025 年 1 月 :2025 年 1 月的 Kotlin 技术月报涵盖了最新动态、精选博客和社区活动等。最新动态包括 IDE 支持、Kotlin K2 编译器新特性、Gradle 支持情况等。精选博客有 KMP 发展回顾、KMM 跨平台原理、MMKV 封装思路等。社区活动是 Kotlin 中文开发者大会视频回放。

  16. 带着问题学,Compose 附带效应(Side Effect)一探究竟 :文章以“Jetpack Compose 附带效应(Side Effect)”为主题,介绍了其定义,包括在组合函数中对输入输出范围外状态或系统产生影响的操作,如网络请求等。还列举了处理副作用的 API 及其用途场景、特别之处,如 SideEffect、LaunchedEffect 等。探讨了如何确保重组时应用状态与外部系统一致,以及副作用 API 的最佳实践和常见误区,最后总结了实际开发中的关键要点。

非技术文章

  1. SP05.愿意主动承受痛苦,才是热爱的证明 : 文章探讨了“热爱”与“痛苦”之间的关系,指出真正的热爱往往藏在愿意主动承受的痛苦背后。通过作者自身的经历以及身边人物的故事,阐述了如何辨别兴趣与热爱,并强调主动选择承受痛苦的重要性。

  2. 读《吸引力法则:如何利用心理暗示实现愿望》的记录与思考 : 这篇文章是作者对《吸引力法则:如何利用心理暗示实现愿望》一书的读后感和思考。文章从吸引力法则的定义、理论体系到具体实践方法,进行了系统的梳理和个人化的解读。作者强调吸引力法则的核心是“相似者互相吸引”,通过关注感受和调整磁场,达到与愿望和谐的状态,并分享了多种提升想象力和正向思维的具体方法。

  3. 月刊(第 28 期):AI 没有体验世界的能力 : 本篇是对二〇二四年十一月至十二月的记录与思考。

  4. EBook - 自洽的程序员 : 但这本书的真正用意是想解决工作过程中碰到的焦虑、倦怠、迷茫、抑郁等情绪,聚焦于解决具体问题,通过改变认知将我们从负面情绪的泥淖中走出来,做到更坦然,真诚的面对自己的内心,成为一个自洽的程序员。

  5. 33 岁在重庆,程序员,在 2024 年拥有的面试经历 : 这篇文章记录了一位 33 岁的程序员在 2024 年于重庆的求职经历及反思。他分享了多次面试的过程、心得以及对未来的展望,同时总结了求职过程中需要注意的事项和心态调整的重要性。

  6. 《奔跑吧,程序员:从零开始打造产品、技术和团队》 : 这篇文章是对创业公司、技术选择、设计原则、招聘策略以及学习技巧等主题的深入探讨。通过实际案例和理论分析,文章介绍了如何在不确定的环境中快速增长,并提供了许多实用建议。

  7. 技术简报 2025 第一期 : 本文章主要整理了一系列技术相关的文章和主题,包括搜索引擎架构、Linux 上下文切换、GPU 计算、数据库分区、创造性思维等内容,旨在分享编程技术洞察和经验。

  8. 读《黄仁勋:英伟达之芯》 : 这篇文章探讨了黄仁勋及其领导下的英伟达如何通过创新和坚持,成为人工智能领域的领导者。文章详细描述了黄仁勋的管理风格、他对技术的深刻理解,以及他如何利用第一性原理重新定义 GPU 的应用场景,推动 AI 革命。

  9. 一个伪独立开发者的独白——4399 小游戏开发 : 作者回顾了自己作为伪独立开发者的经历,分享了开发 4399 小游戏的经验和心得。

  10. 【Netflix】极简主义:记录生命中的重要事物 官方双语字幕 Minimalism : 这部纪录片访问的对象,都努力抗拒认为事物能带来幸福的美式理念,展现少即是多的真谛。

  11. 被动收入的一些思考与实践(1) : 这篇文章讲述了作者在构建被动收入过程中的实践经验、心得体会以及一些关键洞见。通过一次小的尝试,作者逐步摸索出如何将自己的技能和资源转化为可持续的收入来源,同时也总结了在过程中学到的核心理念。

工具

  1. 个人工具箱 : 自己一直是个工具控,也一直信奉着“工欲善其事,必先利其器”的理念,总是不断折腾和优化自己的硬件与软件,针对自己的一个特定需求会试图找到最优解,现在也慢慢找到了最适合自己使用习惯的解决方案。因为工作、学习和个人兴趣,设备经过很多次迭代,在这个时间节点作一下记录,后续也会不断更新,希望能够对其他人有所参考。
  2. 对标 Cursor 和 Windsurf,Trae 如何成为中文开发者的首选? : Trae 是字节跳动推出的一款专为中文开发者优化的 AI IDE,旨在解决现有工具在中文语言支持上的不足,同时通过整合 Claude 3.5 和 GPT-4o 等主流大模型,提升开发效率并提供智能化支持。Trae 通过本地化设计、便捷的功能迁移和多样化的 AI 功能,重新定义了中文开发者友好型 IDE 的标准。

杂记

关于作者

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 掘金 - Gracker:https://juejin.cn/user/1816846860560749
  2. 知乎 - Grackerhttps://www.zhihu.com/people/gracker
  3. 个人博客 - Android Performance : 写东西的地方
  4. 个人介绍 - 欢迎加微信群组多多交流 :里面有个人的微信和微信群链接。
  5. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  6. 本周刊 Newsletter 订阅https://androidweekly.zhubai.love/ ,支持微信和邮箱订阅
  7. 微信公众号 Android Performance
  8. Android 性能优化知识星球 : 个人运营的一个知识星球,欢迎加入,多谢支持~
☑️ ⭐

Android Perfetto 系列 3:熟悉 Perfetto View

本篇是 Perfetto 系列文章的第三篇,前两篇介绍了 Perfetto 是什么以及 Perfetto Trace 怎么抓,本篇主要是在网页端打开 Perfetto Trace 之后,面对复杂的 Perfetto 信息该怎么看。

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

本文目录

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

本系列旨在通过 Perfetto 这个工具,从一个新的视角审视 Android 系统的整体运作方式。此外,它还旨在提供一个不同的角度来学习 App 、 Framework、Linux 等关键模块。尽管你可能已经阅读过许多关于 Android Framework、App 、性能优化的文章,但或许因为难以记住代码或不明白其运行流程,你仍感到困惑。通过 Perfetto 这个图形化工具,你可能会获得更深入的理解。

Perfetto 系列目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争
  12. 视频(B站) - Android Perfetto 基础和案例分享
  13. 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

Perfetto View 界面

抓到 Perfetto Trace 之后,一般是在 ui.perfetto.dev 中打开(如果用官方提供的脚本,则会在抓去结束后自动在这个网站上打开,想看看怎么实现的话可以去看看脚本的源码)。打开后界面如下:

Perfetto View 界面

可以通过 Open trace file 或者直接把 Perfetto Trace 拖到白色区域来打开 Trace。

Perfetto Trace 界面

打开 Perfetto Trace 之后界面如下:

Trace 操作区

大致上 Perfetto Trace 界面可以分为四个区域:

  1. 最右边的操作区:这里最主要的是 Current Trace 这一栏下面的那几个会经常用到。
    1. Show timeline :显示当前 Trace,切到了别的界面之后,再点这个就会回到 Trace View 界面
    2. Query:写 SQL 查询语句和查看执行结果的地方
    3. Metrics:官方默认帮你写好的一些分析结果,可以选择直接打开
    4. Info and stats :当前 Trace 和手机 App 的一些信息
  2. 上方的信息和操作区域:最主要就是看时间。
  3. 中间的 Trace 内容区:操作最多的区域,Trace 内容都在这部分,最上面的几部分是从功能的角度来划成一个区域的,比如 CPU 区(可以查看当前 Task 跑在哪个核心上,频率是多少,跑了多长时间、被谁唤醒)、Ftrace event 区等;下面的就是以 App Process 为单位展示的(包括 App 的各种线程、Input 事件、Binder Call、Memory、Buffer 等信息)。
  4. 最下方的信息区:这个区域主要是展示各种信息、我们选中了某个 Task 段之后,这里就会展示这个 Task 相关的信息(如果你加了 Log,这里也会显示 Log;ftrace event 同理)。

Perfetto 界面最初看的时候会觉得很乱,花里胡哨的,但是用习惯了之后,真香~

基本操作

Perfetto Trace 界面的操作是非常顺滑的,这是相比 Systrace 的一个巨大的优势,Systrace 打开稍大的 Trace 就会卡卡的,但是 Perfetto Trace 打开 500Mb 的 Trace 依然操作很顺滑。

操作看 Trace 的快捷键跟 Systrace 很像,w/s 是放大/缩小,a/d 是左右移动,鼠标点击是选择。官方左下角的文档有详细的操作说明,忘记了的话可以随时去看看,熟能生巧:

基本操作

SQL 相关的操作

其他快捷键

其他快捷键里面用的比较多的:

  1. f 是放大选中

  2. m 是临时 Mark 一段区域(与 Systrace 一样), 用来上下看时间、看其他进程信息等。临时的意思就是你如果按 m 去 mark 另外一个区域,那么上一个用 m mark 出来的 Mark 区域就会消失。退出临时选中:esc ,或者选择其他的 Slice 按 m,当前这个 Slice 的选中效果就会消失

  3. shift + m 是持续 Mark 一段区域(如果你不点,他就不会消失),主要是用来长时间 Mark 住一段信息,比如你把一份 Trace 中所有的掉帧点都 Mark 出来,就可以用 shift + m,这样就不会丢失。

    点击小旗子,就可以看到这段区间内的执行信息

  1. 删除持续 Mark

    1. 点击你选中的那个 Slice 的最上面那个三角
    2. 下面选择 Tab:Current Selection
    3. 点击最后边的 Remove ,就可以把他 Remove 掉了
  2. q :隐藏和显示信息 Tab,由于 Perfetto 非常占屏幕,熟练使用 q 键很重要,看的时候快速打开,看完后快速关闭。

  3. 插旗子:Perfetto 还可以通过插旗子的方法来在 Trace 上做标记,Perfetto 支持你把鼠标放到 Trace 最上面,就会出现一个旗子,点击左键即可插一个旗子在上面,方便我们标记某个事件发生,或者某个时间点

  1. 取消插的旗子:与退出持续选中一样,点击旗子,右下角有个 Remove ,点击就可以把这个旗子干掉了,就不插图了

Perfetto 使用技巧

查看唤醒源

我们可以通过查看某一个 Task 的唤醒源,来了解 App 和 Framework 的运转流程,Systrace 和 Perfetto 都可以查看唤醒源,不过 Perfetto 在这方面做的更丝滑一些。

Android Systrace 响应速度实战 3 :响应速度延伸知识 这篇文章中,有讲 Systrace 是如何查看唤醒源的,其实略微还是有些麻烦的。 Perfetto 中查看唤醒源则非常方便且操作很顺滑:

比如我们想看下图中, RenderThread 是被谁唤醒的,我们可以有好几种方法:

点击 Runnable 状态

与 Systrace 操作一样,直接点击 Running 前面的 Runnable,就可以在下面的信息区看到 Related thread states:

  1. Waker:唤醒源
  2. Previous state:这个 Task 的前一个状态
  3. Next state:这个 Task 的后一个状态

点击他上方的 Running 状态,查看连续唤醒信息:

或者我们可以点击 Running 状态,点击小箭头直接跳到对应的 CPU Info 区域,这里可以看到更详细的信息,也可以连续点击 Task,来追踪唤醒源,并可以通过信息区的小箭头来回在 CPU Info 区域和 Task 区域跳转

点击 RenderThread 上方的 Running 状态,通过小箭头跳转到 CPU Info 区域

RenderThread 是被 MainThread 唤醒的

再点击 MainThread 可以看到他是被 SurfaceFlinger 唤醒的,下方信息区还有对应的唤醒延迟分析

查看 Critical Path(Task)

Critical Task 指的是与当前我们选中的 Task 有依赖关系的 Task,比如我们的 Task 是 e,e 要等 d 执行结束后才能执行,d 要等 c,c 要等 b,b 要等 a,那么 e 的 Critical Task 就是 a、b、c、d。

Perfetto 上就可以查看某一个 Task 的 Critical Task,鉴于 Critical path lite 是 Critical path 的子集,我们这里只介绍 Critical path:

点击 Running 状态,然后点击在下面的信息区点击 Critical path

稍等片刻就可以看到我们选择的 MainThread 对应的 Critical path:

放大来看,可以看到我们选择的 MainThread 的边缘,第一个 Critical Task 是唤醒他的 sf 的 app 线程

再往左看 sf 的 app 线程是被 sf 的 TimerDispatch 线程唤醒的,这里就不贴了。

其实可以看到,Perfetto 提供的 Critical Path 其实就是把连续唤醒的 Task 都聚集到一起了,方便我们来看各个 Task 之间的关系。

Pin (固定到最上面)

在每个 Thread 的最左边,有一个图钉一样的按钮,点击之后,这个 Thread 就会被固定到最上面,方便我们把自己最关注的 Thread 放到一起。

比如下面是我 Pin 的从 App 到 SF 的流程图,放到一起的话就会清晰很多,看掉帧的话也会更方便。

CPU Info 区域 Task 高亮

在 CPU Info 区域,鼠标放到某一个 Task 上,就会这个 Task 对应的 Thread 的其他 Task 都会高亮。

我们经常会用这个方法来初步看某些 Thread 的摆核信息

查看 Buffer 消耗情况

App 的 Buffer 消费者是 SurfaceFlinger,通过 App Process 这边的 Actual Timeline 这行,我们可以看到 Buffer 具体是被 SurfaceFlinger 的哪一框消费了。

快速查看 App 执行超时

由于 Android 多 Buffer 机制的存在,App 执行超时不一定会卡顿,但是超时是需要我们去关注的。

通过 Perfetto 提供给的 Expected Timeline 和 Actual Timeline 这两行,可以清楚看到执行超时的地方。

点击 Actial Timeline 红色那一段,底下的信息栏会显示掉帧原因:

在 Perfetto 上查看 Log

在信息栏上切换到 Android Logs 这个 Tab,鼠标放倒某一行上,Perfetto 就会把对应的 Timeline 拉一条直线,可以看到这个 Log 所对应的时间

同样切换到 Ftrace events tab 也可以查看对应的 ftrace 的 event 和对应的时间线

分析 Thread 的 Running 信息

可以通过鼠标左键按住滑动,选中一段区域来进行分析,比如选中 CPU State 这一栏的话,就可以看到这一段时间对应的 Running、Runnable、Sleep、Uninterruptible Sleep 的占比。

这在分析 App 启动的时候经常会用到。

总结

上面分享了 Perfetto 基本的界面和操作,以及分享了一些比较常用的 Perfetto 的技巧。Google 目前在积极推广和维护 Perfetto,很多新功能指不定哪天就蹦出来了,到时候觉得有用我也会更新上来。

至此 Perfetto 基础篇就结束了,后续就是通过 Perfetto 这个工具,来了解 Android 系统运行的基本流程,以及使用 Perfetto 以及 Perfetto SQL 来分析遇到的性能、功耗等问题。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

❌