阅读视图

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

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 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 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 系列目录

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

所以就有了这个系列,我也有在星球里面写了几条为什么要更新 Perfetto 系列的原因(之前一直觉得 Systrace 系列就够了):

  1. 目前 Oppo、Vivo 这些手机厂商内部,都已经切换成 Perfetto 了,不管是抓 Trace 还是看 Trace,都在使用 Perfetto ,很多新人接触的都是 Perfetto 而不是 Systrace ,守着之前的老 Systrace 系列会流失这部分读者
  2. 之前的 Systrace 系列,对应的 Code 已经比较老了,全新的 Perfetto 系列可以使用 Android 14 的 Code 来进行更新
  3. 个人对 Perfetto 的使用也没有很深入,有些高阶功能目前还只是浅尝辄止。可以通过重写 Perfetto 系列来进行这部分内容的强化
  4. Perfetto 是个很强大的工具,他的背后是整个 Android + Linux 系统,所以在写这个系列的时候,应该是以他背后的这个 Android + Linux 为主,而不是仅仅局限于 Perfetto 这个工具。工具只是我们观测 Android + Linux 的方式,理解整个 Android 系统运行的规律,思考其运行的原理,通过工具挖掘问题,通过问题思考本质,这才是对开发者来说有意义的
  5. 很多 Android 系统运行相关的内容,Perfetto 的官方文档还是没有讲,这部分我这边可以作为补足;另外官方文档是英文版本的,中文博客可以补充这方面。
  6. Perfetto 系列写好了,可以拿到 Google Dev Fest 上作为演讲内容~。

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 系统优化分享

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

Systrace 系列

另外 Systrace 工具尽管已经不更新了,但是之前的 Systrace 系列文章,内容依然没有过时,还是有很多公司在使用 Systrace 来分析各种系统问题,Systrace 工具是分析 Android 性能问题的利器,它可以从一个图形的角度,来展现整机的运行情况。Systrace 工具不仅可以分析性能问题,用它来进行 Framework 的学习也是很好的。

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - Input 解读
  6. Systrace 基础知识 - Vsync 产生与工作机制解读
  7. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  8. Systrace 基础知识 - MainThread 和 RenderThread 解读
  9. Systrace 基础知识 - Binder 和锁竞争解读
  10. Systrace 基础知识 - Triple Buffer 解读
  11. Systrace 基础知识 - CPU Info 解读
  12. Systrace 基础知识 - SystemServer 解读
  13. Systrace 基础知识 - SurfaceFlinger 解读
  14. Systrace 流畅性实战 1 :了解卡顿原理
  15. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  16. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  17. Systrace 响应速度实战 1 :了解响应速度原理
  18. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  19. Systrace 响应速度实战 3 :响应速度延伸知识
  20. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  22. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

关于我 && 博客

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

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

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

微信扫一扫

🔲 ☆

2023 年的方方面面

今年偷个懒,找了个模版,主要从以下几个方面回顾过去一年:健康 / 锻炼、工作 / 职业、友情 / 社交、个人生活 / 家庭、学习 / 知识管理、旅游 / 文化、兴趣 / 创造、情绪 / 精神状况、财务状况。

内容更多是自己对于 2023 年的一个记录,算不上总结,文采也不好。不过很多事情,如果你不记录,就慢慢消失了。希望每次我翻看这篇记录的时候,都会感慨 2023 年真是丰富多彩的一年:有难忘的瞬间、有低谷、有朋自远方来、有去看大山大川;也会感慨有些事情做得很糟糕,如此这般其实可以做得更好;也会责备自己的懒惰,明明知道怎么做是对的,却因为懒惰没有去坚持。

各位看官一笑而过即可~

健康 / 锻炼

今年由于公司有骑行活动比赛,持续一整年,再加上成都环城绿道去年贯通,今年一整年的活动就是骑行。年初在闲鱼收了一个美利达瑞克多 4000,准备今年好好骑车。年初的目标是一圈(97km)骑到 3h 内,挑战目标是 2h 50min,没想到最终都达到了(总共骑了 35 圈绿道,最快用时是 2h 47min),如愿以偿在公司内部比赛中拿了第一名。同时把媳妇也带进了坑,一起骑了几圈。

自身叠加的那些 Debuff:哮喘、鼻炎,并没有明显的改善;不过最值得开心的是年底由于肺炎+甲流,住了 4 天院,打了四天吊瓶之后,鼻炎导致的味觉消失也被治好了,同时血氧也恢复到了 96% 以上(之前 always 在 94 以下,给我焦虑得不行)。成都的空气质量真是一言难尽,连华西的医生都直摇头,这一点想移居成都的同学可以慎重考虑一下。

新的一年还是得 骑车+跑步 一起抓,报了个都江堰半马,希望能完赛。减肥依然是主旋律,管住嘴迈开腿很重要,睡前保持饥饿感~

2023 年骑行数据

工作 / 职业

工作方面,今年不算很顺利,做的东西很杂,很多东西也没有最终落地,自己总结了一下,还是因为缺乏规划能力和执行力。不过也算积累了很多知识和做事方法,调研了很多技术方案,与客户也合作完成了几个项目。另外公司内部卧虎藏龙,需要虚心向每一个人学习。

AI 今年算是整了个大新闻,不管是公司还是个人,工作流中都插入了 AI 相关的内容:利用 AI 提升工作效率、创造 AI 相关的工具。不过由于公司性质,AI 还没有产生那种颠覆性的影响,不过我们都知道这一天很快就会来临。

“一种新技术一旦开始流行,你要么坐上压路机,要么成为铺路石。 —— Stewart Brand”

友情 / 社交

今年最重要的一次社交可能就是从成都去深圳参加 深圳 GDG DevFest 技术嘉年华,见到了几位老朋友和各位大佬,吃到了心心念念的砂锅粥和牛肉火锅,跟群里的小伙伴面了基,听了各位 GDE 的主题演讲,可谓是收获满满。

剩下的时间就是跟公司的几个小伙伴骑车了,能让大家周五晚上 12 点还在绿道上疯狂拉扯的,只有骑完之后那一顿火锅/麻辣烫了。@James @Hanzo @天宇 @宣总 @王 @陈大 @祥大 @果哥 @鹏哥 新的一年继续拉扯~

@福滕 和 @小陈 有了小宝宝,@周岩 的新房居然提前交房了(不过依然没有女朋友,狗头~)~,@老崔 跟 @和泉 都在深圳安家了。

交流最多的还是跟微信群里的小伙伴们,基本都是通过微信公众号或者博客添加的,基本都是 Android App 开发者或者国内的系统开发者,5 个群大概 2000 多人吧(虽然 90% 都是资深潜水员)。通过微信群认识了很多业内的大佬,手机圈本身就小,多个朋友多个家(狗头~),同时也感叹真是山外有山,人外有人,啥都别说了学就是了。

最大的感受是:最好跟在同一个频道上的人沟通,会减少很多沟通的成本。

个人生活 / 家庭

小宝宝橘橘快三岁了,橘橘妈也换了新的组(跟我做相同的工作:系统性能优化),一家人开开心心平平安安,就是最大的心愿了。今年由于各种原因,在家里陪宝宝和家人的时间有点少,出游计划也多被搁置。新的一年要提升效率,把时间多用在陪伴家人。

今天读到一个博主的 2023 年终总结,其中一段话让我感触很深,这才是生活的最终目标,不要本末倒置:

我希望多丰富一些体验,做一些没做过的事情,看一些没看过的风景。我希望 有一天可以把时间用在自己身上,而不是用在工作上,不是在那些不重要的人,或者事身上。更具体一点,我希望家人身体健康、平平安安,有爱,有陪伴,我也希望有一天可以走遍中国、走遍世界,看那些没看过的风景,吃那些没吃过的美食。

当我想到这里,我豁然开朗,我知道我终其一生追求的是什么,剩余的,都不重要的。那么,我怎样才可以?我需要时间,我需要钱。

从此开始,我所有的事情都围绕着这个目的展开,只要某件事情,对我的人生最终目的是有推进作用,无论多么艰难,我都必须做,是的,必须,没有任何商量的余地

学习 / 知识管理

今年学习方面一败涂地,英语学习基本上停滞,最后这个月才重新拾起来;技术学习更多停留在读,写和思考总结都比较少;文化学习,比如阅读,书架上的书很多,读完的很少;博客也只更新了几篇文章,费曼学习法那种输出倒逼输入,道理都懂,还是没有克服拖延症。

工作中倒是养成了一个习惯:在公司内部使用 Typora 记录每天的工作内容、技术专项、技术调研、Bug 记录等;新的一年切换到 Loop 之后,我会在上面更详细地记录和思考,以及组内分享,带动组内的技术氛围(今年年初吹下的牛皮年底了最终还是没有实现)。

从我自己的学习经历来看,学习有三大障碍:

  1. 如何获取优秀的输入源:互联网如此发达的年代,如何获取优秀的输入源很重要,这就需要在长期的学习过程中,记录那些优秀的人的博客、优质的 Github 库、高质量的周报、专注在技术输出上的技术团队等(话说我这篇文章:Android 性能优化必知必会 本意也是搜集这些内容的,欢迎大家推荐和自荐)。
  2. 静下心来把一个知识点吃透:短视频类的快消品在争夺人的注意力的同时,也把我们的耐心消耗殆尽,静下心来阅读和思考一篇文章、一个知识点、一本书,都成了一个很不容易的事情。这个只能自己锻炼自己,远离社交网络和短视频,重新夺回注意力,把心思放在真正需要的地方。
  3. 克服拖延症:我们常说,道理都懂,依然过不好,拖延症在里面功不可没。今天看到张朝阳分析治疗拖延症的方法,分析出来与君共勉:拖延症的关键关键就在于拖延本质上是因为不熟悉这件事情想要逃避,因而产生恐惧和焦虑,那战胜它的方法就是在想要拖延时,先在脑子里过一遍要做的事情,梳理好脉络,构想出细节,让自己的神经元兴奋,立即行动。这样可以战胜拖延,并感受到正反馈

年初看了李自然的一个视频:人生如逆旅,我亦是行人,视频主要是讲了下面几个点,当时感触很深,也分享给大家:【李自然说】人生如逆旅,我亦是行人

  1. 不给自己设限,能快速融入到更有潜力的行业
  2. 向顶级的人拼命地学习
  3. 拼命地打开自己的视野和格局
  4. 拥抱变化,甚至主动去求变
  5. 一直坚持做,就会有一个长期的口碑
  6. 提高容错率

旅游 / 文化

今年去了四个地方:四姑娘山,厦门,烟台+威海,乐山。

  1. 厦门:团队旅游目的地,看海,吃沙茶面,吃姜母鸭,不得不说厦门这天气太让人羡慕了,环海路也可以骑车,市内还有步道(可惜没去)。
  2. 烟台+威海:烟台+威海,先去参加 @小婉 的婚礼,然后回母校去看看见了几位老师和老同学,韩餐和海鲜都吃到了,环海路依然风景漂亮,学校后面的沙滩落日很美~
  3. 四姑娘山:开了一天车去到处于川西的四姑娘山,路上要经过海拔 4800 的垭口,宝宝和媳妇高反,没有怎么玩,很快就回来了。我们去的那天晚上下了雪,第二天到了景区后拍照很漂亮,我录了视频在这里:https://www.bilibili.com/video/BV1H84y1Q7oM ,漂亮是真漂亮,值得再去一趟~
  4. 乐山:看乐山大佛和吃小吃~甜皮鸭和炸串真好吃~

带着宝宝出去玩虽然很折腾,但是还是很值的,宝宝在一路上会认识很多新的东西,交新的朋友,吃到新的食物。2024 依然会继续带着宝宝折腾~

兴趣 / 创造

终于来到轻松一点的环节了,今年主要兴趣是骑车,购置了一台大疆 Action4 来拍骑车时候的视频,要技术没有,都是一镜到底,创造力这玩意是真得有天赋才行。

情绪 / 精神状况

自我感觉良好,除了稍微有一些焦虑。自我感觉良好很大程度上得益于家庭的支持,有爸妈在家帮忙看宝宝,我和媳妇双 it,很多决定媳妇都会坚定支持。成都美食比较多,比较杂,想吃哪种都可以吃到,玩的地方也比较多,川西、古镇、都江堰青城山,去重庆也不远;天府新区高新区这边也不怎么排外,呆着很舒服(除了雾霾,这个真的是负分)

2024 继续保持吧,心态要好,睡眠要好。

财务状况

房贷还剩余比较多,每个月工资大半都交给银行了,除了夫妻两个的工资基本没有其他的收入,这也是我目前比较焦虑的点,一旦遇到被辞退或者其他变故,周转就会有困难。所以最终目标还是要管好自己的钱袋子,控制花销。看了一个说法,分别是穷人、中产阶级、富人的资产负债情况:

  • 穷人疲于奔命,收入被衣食住行消耗殆尽。
  • 中产阶级深陷「老鼠赛跑」的陷进,为各种账单、贷款奋斗半辈子。
  • 富人买入并持有优质资产,形成「钱 → 资产 → 钱」的良性循环。

所以最终还是要持有 优质资产,我理解这里的优质资产可以是房产,也可以是知识。结合个人的情况,我思索再三,又重新开启了 知识星球,简介在这里 The Performance 知识星球简介,我并没有做宣传,只在博客中有介绍。我觉得目前最有效的两种学习方法:费曼学习法和公开学习法,知识星球都可以满足我,我会把日常的一些思考、案例、学习的进度、工具等分享到星球上;如果在这个过程中,有些知识能帮助到你,那么我觉得就值了。

最后三项

最骄傲的成就

今年可能最值得拿出来说的就是去了 电子科技大学 ,给研究生们上了三天的课,蹭的当然是公司跟企业合作这个便利,不过过程还是很值得一说的:从接到这个任务,到开始备课(其实就是学习 @鹏哥 去年准备的 PPT),再到每个知识点的学习,再到真正去讲课,还是充满了挑战的,毕竟是第一次,还好没有翻车。

今年好像还是我,这次就没那么紧张了,不过去年的内容要好好更新一下,讲课的方式需要再优化优化,知识点该深入的还是要继续深入,毕竟 Android 从最上层到最底层都讲清楚也不是那么容易的.

最大的挑战

工作方面的就不说了,除了工作方面,最大的挑战应该还是英语口语了,今年有好几次需要用到英语口语的时候,结果都不是那么好,这也刺激我要好好学英语。这个感觉要放到 2024 年最重要的 Task 里面,目前也有一些小的规划在实施中。

明年的目标和愿望

  1. 多陪陪家人,做一些之前没有做过的事情,去一些之前没有去过的地方。
  2. 至少做一次 Google Dev Fest 级别的公开分享
  3. 出一次国
  4. 坚持把知识星球做大做强
  5. 坚持锻炼身体,一次马拉松完赛
  6. 多看书
  7. 坚持每 2 周更新博客,提早把 Perfetto 系列写完
  8. 录视频

文笔有限,借用 aoxiang 的话来做个结尾吧:

很感谢对我的认可,你花时间看到这里,我很希望这篇文章对你有价值。同时,我也很希望你能想清楚「你想要过什么样的人生」,当你有了这个判定标准,你任何一个抉择,都会无比轻松。

关于我 && 博客

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

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

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

微信扫一扫

🔲 ⭐

当 App 有了系统权限,真的可以为所欲为?

前一段时间有个 App 很火,是 Android App 利用了 Android 系统漏洞,获得了系统权限,做了很多事情。想看看这些个 App 在利用系统漏洞获取系统权限之后,都干了什么事,于是就有了这篇文章。由于准备仓促,有些 Code 没有仔细看,感兴趣的同学可以自己去研究研究,多多讨论,对应的文章和 Code 链接都在下面:

  1. 深蓝洞察:2022 年度最 “不可赦” 漏洞
  2. XXX apk 内嵌提权代码,及动态下发 dex 分析
  3. Android 反序列化漏洞攻防史话

关于这个 App 是如何获取这个系统权限的,Android 反序列化漏洞攻防史话,这篇文章讲的很清楚,就不再赘述了,我也不是安全方面的专家,但是建议大家多读几遍这篇文章

序列化和反序列化是指将内存数据结构转换为字节流,通过网络传输或者保存到磁盘,然后再将字节流恢复为内存对象的过程。在 Web 安全领域,出现过很多反序列化漏洞,比如 PHP 反序列化、Java 反序列化等。由于在反序列化的过程中触发了非预期的程序逻辑,从而被攻击者用精心构造的字节流触发并利用漏洞从而最终实现任意代码执行等目的。

这篇文章主要来看看 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个库里面提供的 Dex ,看看 App 到底想知道用户的什么信息?总的来说,App 获取系统权限之后,主要做了下面几件事(正常 App 无法或者很难做到的事情),各种不把用户当人了。

  1. 自启动、关联启动相关的修改,偷偷打开或者默认打开:与手机厂商斗智斗勇。
  2. 开启通知权限。
  3. 监听通知内容。
  4. 获取用户的使用手机的信息,包括安装的 App、使用时长、用户 ID、用户名等。
  5. 修改系统设置。
  6. 整一些系统权限的工具方便自己使用。

另外也可以看到,这个 App 对于各个手机厂商的研究还是比较深入的,针对华为、Oppo、Vivo、Xiaomi 等终端厂商都有专门的处理,这个也是值得手机厂商去反向研究和防御的。

最好我还加上了这篇文章在微信公众号发出去之后的用户评论,以及知乎回答的评论区(问题已经被删了,但是我可以看到:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317,目前为止是 2471 个赞)可以说是脑洞大开(关于 App 如何作恶)。

0. Dex 文件信息

本文所研究的 dex 文件是从 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个仓库获取的,Dex 文件总共有 37 个,不多,也不大,慢慢看。这些文件会通过后台服务器动态下发,然后在 App 启动的时候进行动态加载,可以说是隐蔽的很,然而 Android 毕竟是开源软件,要抓你个 App 的行为还是很简单的,这些 Dex 就是被抓包抓出来的,可以说是人脏货俱全了。

由于是 dex 文件,所以直接使用 https://github.com/tp7309/TTDeDroid 这个库的反编译工具打开看即可,比如我配置好之后,直接使用 showjar 这个命令就可以

showjar 95cd95ab4d694ad8bdf49f07e3599fb3.dex

默认是用 jadx 打开,就可以看到反编译之后的内容,我们重点看 Executor 里面的代码逻辑即可

打开后可以看到具体的功能逻辑,可以看到一个 dex 一般只干一件事,那我们重点看这件事的核心实现部分即可

1. 通知监听和通知权限相关

1.1 获取 Xiaomi 手机通知内容

  1. 文件 : 95cd95ab4d694ad8bdf49f07e3599fb3.dex
  2. 功能 :获取用户的 Active 通知
  3. 类名 :com.google.android.sd.biz_dynamic_dex.xm_ntf_info.XMGetNtfInfoExecutor

1. 反射拿到 ServiceManager

一般我们会通过 ServiceManager 的 getService 方法获取系统的 Service,然后进行远程调用

2. 通过 NotificationManagerService 获取通知的详细内容

通过 getService 传入 NotificationManagerService 获取 NotificationManager 之后,就可以调用 getActiveNotifications 这个方法了,然后具体拿到 Notification 的下面几个字段

  1. 通知的 Title
  2. 发生通知的 App 的包名
  3. 通知发送时间
  4. key
  5. channelID :the id of the channel this notification posts to.

可能有人不知道这玩意是啥,下面这个图里面就是一个典型的通知

其代码如下

可以看到 getActiveNotifications 这个方法,是 System-only 的,普通的 App 是不能随便读取 Notification 的,但是这个 App 由于有权限,就可以获取

当然微信的防撤回插件使用的一般是另外一种方法,比如辅助服务,这玩意是合规的,但是还是推荐大家能不用就不用,它能帮你防撤回,他就能获取通知的内容,包括你知道的和不知道的

1.2. 打开 Xiaomi 手机上的通知权限(Push)

  1. 文件 :0fc0e98ac2e54bc29401efaddfc8ad7f.dex
  2. 功能 :可能有的时候小米用户会把 App 的通知给关掉,App 想知道这个用户是不是把通知关了,如果关了就偷偷打开
  3. 类名 :com.google.android.sd.biz_dynamic_dex.xm_permission.XMPermissionExecutor

这么看来这个应该还是蛮实用的,你个调皮的用户,我发通知都是为了你好,你怎么忍心把我关掉呢?让我帮你偷偷打开吧

App 调用 NotificationManagerService 的 setNotificationsEnabledForPackage 来设置通知,可以强制打开通知
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

然后查看 NotificationManagerService 的 setNotificationsEnabledForPackage 这个方法,就是查看用户是不是打开成功了
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

还有针对 leb 的单独处理~ 细 !

1.3. 打开 Vivo 机器上的通知权限(Push)

  1. 文件 :2eb20dc580aaa5186ee4a4ceb2374669.dex
  2. 功能 :Vivo 用户会把 App 的通知给关掉,这样在 Vivo 手机上 App 就收不到通知了,那不行,得偷偷打开
  3. 类名 :com.google.android.sd.biz_dynamic_dex.vivo_open_push.VivoOpenPushExecutor

核心和上面那个是一样的,只不过这个是专门针对 vivo 手机的

1.4 打开 Oppo 手机的通知权限

  1. 文件 :67c9e686004f45158e94002e8e781192.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.oppo_notification_ut.OppoNotificationUTExecutor

没有反编译出来,看大概的逻辑应该是打开 App 在 oppo 手机上的通知权限

1.5 Notification 监听

  1. 文件 :ab8ed4c3482c42a1b8baef558ee79deb.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.ud_notification_listener.UdNotificationListenerExecutor

这个就有点厉害了,在监听 App 的 Notification 的发送,然后进行统计

监听的核心代码

这个咱也不是很懂,是时候跟做了多年 SystemUI 和 Launcher 的老婆求助了....@史工

1.6 App Notification 监听

  1. 文件 :4f260398-e9d1-4390-bbb9-eeb49c07bf3c.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.notification_listener.NotificationListenerExecutor

上面那个是 UdNotificationListenerExecutor , 这个是 NotificationListenerExecutor,UD 是啥?

这个反射调用的 setNotificationListenerAccessGranted 是个 SystemAPI,获得通知的使用权,果然有权限就可以为所欲为

1.7 打开华为手机的通知监听权限

  1. 文件 :a3937709-b9cc-48fd-8918-163c9cb7c2df.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_notification_listener.HWNotificationListenerExecutor

华为也无法幸免,哈哈哈

1.8 打开华为手机通知权限

  1. 文件 :257682c986ab449ab9e7c8ae7682fa61.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_permission.HwPermissionExecutor

2. Backup 状态

2.1. 鸿蒙 OS 上 App Backup 状态相关,保活用?

  1. 文件 :6932a923-9f13-4624-bfea-1249ddfd5505.dex
  2. 功能 :Backup 相关

这个看了半天,应该是专门针对华为手机的,收到 IBackupSessionCallback 回调后,执行 PackageManagerEx.startBackupSession 方法

查了下这个方法的作用,启动备份或恢复会话

2.2. Vivo 手机 Backup 状态相关

  1. 文件 :8c34f5dc-f04c-40ba-98d4-7aa7c364b65c.dex
  2. 功能 :Backup 相关

3. 文件相关

3.1 获取华为手机 SLog 和 SharedPreferences 内容

  1. 文件 : da03be2689cc463f901806b5b417c9f5.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_get_input.HwGetInputExecutor

拿这个干嘛呢?拿去做数据分析?

获取 SharedPreferences

获取 slog

4. 用户数据

4.1 获取用户使用手机的数据

  1. 文件 : 35604479f8854b5d90bc800e912034fc.dex
  2. 功能 :看名字就知道是获取用户的使用手机的数据
  3. 类名 :com.google.android.sd.biz_dynamic_dex.usage_event_all.UsageEventAllExecutor

看核心逻辑是同 usagestates 服务,来获取用户使用手机的数据,难怪我手机安装了什么 App、用了多久这些,其他 App 了如指掌

那么他可以拿到哪些数据呢?应有尽有~,包括但不限于 App 启动、退出、挂起、Service 变化、Configuration 变化、亮灭屏、开关机等,感兴趣的可以看一下:

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
frameworks/base/core/java/android/app/usage/UsageEvents.java
private static String eventToString(int eventType) {
switch (eventType) {
case Event.NONE:
return "NONE";
case Event.ACTIVITY_PAUSED:
return "ACTIVITY_PAUSED";
case Event.ACTIVITY_RESUMED:
return "ACTIVITY_RESUMED";
case Event.FOREGROUND_SERVICE_START:
return "FOREGROUND_SERVICE_START";
case Event.FOREGROUND_SERVICE_STOP:
return "FOREGROUND_SERVICE_STOP";
case Event.ACTIVITY_STOPPED:
return "ACTIVITY_STOPPED";
case Event.END_OF_DAY:
return "END_OF_DAY";
case Event.ROLLOVER_FOREGROUND_SERVICE:
return "ROLLOVER_FOREGROUND_SERVICE";
case Event.CONTINUE_PREVIOUS_DAY:
return "CONTINUE_PREVIOUS_DAY";
case Event.CONTINUING_FOREGROUND_SERVICE:
return "CONTINUING_FOREGROUND_SERVICE";
case Event.CONFIGURATION_CHANGE:
return "CONFIGURATION_CHANGE";
case Event.SYSTEM_INTERACTION:
return "SYSTEM_INTERACTION";
case Event.USER_INTERACTION:
return "USER_INTERACTION";
case Event.SHORTCUT_INVOCATION:
return "SHORTCUT_INVOCATION";
case Event.CHOOSER_ACTION:
return "CHOOSER_ACTION";
case Event.NOTIFICATION_SEEN:
return "NOTIFICATION_SEEN";
case Event.STANDBY_BUCKET_CHANGED:
return "STANDBY_BUCKET_CHANGED";
case Event.NOTIFICATION_INTERRUPTION:
return "NOTIFICATION_INTERRUPTION";
case Event.SLICE_PINNED:
return "SLICE_PINNED";
case Event.SLICE_PINNED_PRIV:
return "SLICE_PINNED_PRIV";
case Event.SCREEN_INTERACTIVE:
return "SCREEN_INTERACTIVE";
case Event.SCREEN_NON_INTERACTIVE:
return "SCREEN_NON_INTERACTIVE";
case Event.KEYGUARD_SHOWN:
return "KEYGUARD_SHOWN";
case Event.KEYGUARD_HIDDEN:
return "KEYGUARD_HIDDEN";
case Event.DEVICE_SHUTDOWN:
return "DEVICE_SHUTDOWN";
case Event.DEVICE_STARTUP:
return "DEVICE_STARTUP";
case Event.USER_UNLOCKED:
return "USER_UNLOCKED";
case Event.USER_STOPPED:
return "USER_STOPPED";
case Event.LOCUS_ID_SET:
return "LOCUS_ID_SET";
case Event.APP_COMPONENT_USED:
return "APP_COMPONENT_USED";
default:
return "UNKNOWN_TYPE_" + eventType;
}
}

4.2 获取用户使用数据

  1. 文件:b50477f70bd14479a50e6fa34e18b2a0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.usage_event.UsageEventExecutor

上面那个是 UsageEventAllExecutor,这个是 UsageEventExecutor,主要拿用户使用 App 相关的数据,比如什么时候打开某个 App、什么时候关闭某个 App,6 得很,真毒瘤

4.3 获取用户使用数据

  1. 文件:1a68d982e02fc22b464693a06f528fac.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.app_usage_observer.AppUsageObserver

看样子是注册了 App Usage 的权限,具体 Code 没有出来,不好分析

5. Widget 和 icon 相关

经吃瓜群众提醒,App 可以通过 Widget 伪造一个 icon,用户在长按图标卸载这个 App 的时候,你以为卸载了,其实是把他伪造的这个 Widget 给删除了,真正的 App 还在 (不过我没有遇到过,这么搞真的是脑洞大开,且不把 Android 用户当人)

5.1. Vivo 手机添加 Widget

  1. 文件:f9b6b139-4516-4ac2-896d-8bc3eb1f2d03.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_widget.VivoAddWidgetExecutor

这个比较好理解,在 Vivo 手机上加个 Widget

5.2 获取 icon 相关的信息

  1. 文件:da60112a4b2848adba2ac11f412cccc7.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.get_icon_info.GetIconInfoExecutor

这个好理解,获取 icon 相关的信息,比如在 Launcher 的哪一行,哪一列,是否在文件夹里面。问题是获取这玩意干嘛???迷

5.3 Oppo 手机添加 Widget

  1. 文件:75dcc8ea-d0f9-4222-b8dd-2a83444f9cd6.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppoaddwidget.OppoAddWidgetExecutor

5.4 Xiaomi 手机更新图标?

  1. 文件:5d372522-b6a4-4c1b-a0b4-8114d342e6c0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.xm_akasha.XmAkashaExecutor

小米手机上的桌面 icon 、shorcut 相关的操作,小米的同学来认领

6. 自启动、关联启动、保活相关

6.1 打开 Oppo 手机自启动

  1. 文件:e723d560-c2ee-461e-b2a1-96f85b614f2b.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_boot_perm.OppoBootPermExecutor

看下面这一堆就知道是和自启动相关的,看来自启动权限是每个 App 都蛋疼的东西啊

6.2 打开 Vivo 关联启动权限

  1. 文件:8b56d820-cac2-4ca0-8a3a-1083c5cca7ae.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_association_start.VivoAssociationStartExecutor

看名字就是和关联启动相关的权限,vivo 的同学来领了

直接写了个节点进去

6.3 关闭华为耗电精灵

  1. 文件:7c6e6702-e461-4315-8631-eee246aeba95.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.hw_hide_power_window.HidePowerWindowExecutor

看名字和实现,应该是和华为的耗电精灵有关系,华为的同学可以来看看

6.4 Vivo 机型保活相关

  1. 文件:7877ec6850344e7aad5fdd57f6abf238.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_get_loc.VivoGetLocExecutor

猜测和保活相关,Vivo 的同学可以来认领一下

7. 安装卸载相关

7.1 Vivo 手机回滚卸载

  1. 文件:d643e0f9a68342bc8403a69e7ee877a7.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_rollback_uninstall.VivoRollbackUninstallExecutor

这个看上去像是用户卸载 App 之后,回滚到预置的版本,好吧,这个是常规操作

7.2 Vivo 手机 App 卸载

  1. 文件:be7a2b643d7e8543f49994ffeb0ee0b6.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_official_uninstall.OfficialUntiUninstallV3

看名字和实现,也是和卸载回滚相关的

7.3 Vivo 手机 App 卸载相关

  1. 文件:183bb87aa7d744a195741ce524577dd0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_official_uninstall.VivoOfficialUninstallExecutor

同上

其他

SyncExecutor

  1. 文件:f4247da0-6274-44eb-859a-b4c35ec0dd71.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.sync.SyncExecutor

没看懂是干嘛的,核心应该是 Utils.updateSid ,但是没看到实现的地方

UdParseNotifyMessageExecutor

  1. 文件:f35735a5cbf445c785237797138d246a.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.ud_parse_nmessage.UdParseNotifyMessageExecutor

看名字应该是解析从远端传来的 Notify Message,具体功能未知

6.3 TDLogcatExecutor

  1. 文件
    1. 8aeb045fad9343acbbd1a26998b6485a.dex
    2. 2aa151e2cfa04acb8fb96e523807ca6b.dex
  2. 类名
    1. com.google.android.sd.biz_dynamic_dex.td.logcat.TDLogcatExecutor
    2. com.google.android.sd.biz_dynamic_dex.td.logcat.TDLogcatExecutor

没太看懂这个是干嘛的,像是保活又不像,后面有时间了再慢慢分析

6.4 QueryLBSInfoExecutor

  1. 文件:74168acd-14b4-4ff8-842e-f92b794d7abf.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.query_lbs_info.QueryLBSInfoExecutor

获取 LBS Info

6.5 WriteSettingsExecutor

  1. 文件:6afc90e406bf46e4a29956aabcdfe004.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.write_settings.WriteSettingsExecutor

看名字应该是个工具类,写 Settings 字段的,至于些什么应该是动态下发的

6.6 OppoSettingExecutor

  1. 文件:61517b68-7c09-4021-9aaa-cdebeb9549f2.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.opposettingproxy.OppoSettingExecutor

Setting 代理??没看懂干嘛的,Oppo 的同学来认领,难道是另外一种形式的保活?

6.7 CheckAsterExecutor

  1. 文件:561341f5f7976e13efce7491887f1306.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.check_aster.CheckAsterExecutor

Check aster ?不是很懂

6.8 OppoCommunityIdExecutor

  1. 文件:538278f3-9f68-4fce-be10-12635b9640b2.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_community_id.OppoCommunityIdExecutor

获取 Oppo 用户的 ID?要这玩意干么?

6.9 GetSettingsUsernameExecutor

  1. 文件:4569a29c-b5a8-4dcf-a3a6-0a2f0bfdd493.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_get_settings_username.GetSettingsUsernameExecutor

获取 Oppo 手机用户的 username,话说你要这个啥用咧?

6.10 LogcatExecutor

  1. 文件:218a37ea-710d-49cb-b872-2a47a1115c69.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.logcat.LogcatExecutor

配置 Log 的参数

6.11 VivoBrowserSettingsExecutor

  1. 文件:136d4651-df47-41b4-bb80-2ec0ab1bc775.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_browser_settings.VivoBrowserSettingsExecutor

Vivo 浏览器相关的设置,不太懂要干嘛

评论区比文章更精彩

微信公众号评论区

image-20230514203931411

image-20230514203940833

image-20230514203951666

image-20230514204055973

image-20230514204002395

image-20230514204022808

image-20230514204042836

image-20230514204123412

image-20230514204200492

知乎评论区

知乎回答已经被删了,我通过主页可以看到,但是点进去是已经被删了:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317

image-20230514205638861

image-20230514205909534

image-20230514205857945

image-20230514205937705

image-20230514205947268

image-20230514210010062

image-20230514210020926

image-20230514210040479

image-20230514210107839

image-20230514210122906

image-20230514210141653

image-20230514210152755

image-20230514210226176

image-20230514210235233

image-20230514210255912

image-20230514210344475

iOS 和 Android 哪个更安全?

这里就贴一下安全大佬 sunwear 的评论

img

关于我 && 博客

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

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

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

微信扫一扫

🔲 ☆

The Performance 星球茶话会 - 第一期

2022.3.25 ,周五晚上九点,The Performance 知识星球(付费版本)举办了第一次线上茶话会,3 位星球主理人 + 5 位星球嘉宾,50 多位球友参加,非常感谢各位!

第一次茶话会没有预设主题,预计 1 个小时就可以结束,结果聊了 2 个半小时,光是主理人和嘉宾的个人介绍就花了一个小时。平时大家在群里和星球里交流很多,但是通过语音在线交流还是第一次,再者大家的公司基本上涵盖了 Android 上下游:既有 App 大厂的大佬,也有一线手机厂商的系统大咖,也有芯片公司和造车新势力的资深专家。所以在每个人自我介绍的时候,会聊一些公司相关的东西,再发散一下,其他人再问一些问题,时间就过去了。

后续会不定期举办星球茶话会,主题会更加明确,也会邀请更多嘉宾来一起聊聊,不局限于技术,欢迎大家多多参与和提问。考虑到隐私问题,

本次茶话会没有录屏,下面的内容也不会涉及到各位的隐私,文字稿是根据聊天内容部分还原的。

关于职业发展

领域的应届生真的被庞大的知识体系压的有点喘不过气

  1. 改变心态:学校跟专业领域是不一样的。学校是有课本,学完一门课就算结业了。从事专业事项的时候,两个不一样,而且很不一样。
  2. 从手边的问题开始学习:把工作中遇到的不懂的问题弄懂,做到 100 分甚至 110 分,平时储备自己不具备的知识。对于新人来说,把工作做好有很多好处。一是增加经验,二是获得更多学习与接触新东西的机会。切忌主业没做好,做另外一个方向,除非你确定目前的主业不适合自己干。
  3. 面向解决问题编程:工作中很重要的一个技能就是解决问题,老板发钱就是让你解决问题的。掌握好度,以经济学来说就是控制好边际收益,投入与产出应最大化是比较理想的情况。
  4. 掌握学习的方法和技巧:爬山的时候直接看山顶,更让人容易放弃。更好的方式是,头朝下一步一步走,偶尔看看地图是否走错,或者走着走着发现更适合自己的路。

刚从 App 开发转到安卓系统性能优化的小白

  1. 跟踪公司的项目走:如果有人带那就好,跟着公司项目走,这是最快的。
  2. 自驱力:如果愿意平时学习,会的多了争取到的机会也多一些。还是那个观点,对于新人来说,把手头工作做好的前提下努力学习跟主业相关的新知识。当你做手头工作的时候你的主观能动性会变强、周边同事与领导也会支持你,而且给你的奖励也是不错的。
  3. 不要给自己设限:知识都是融会贯通的,不要把自己局限在自己的一亩三分地,尤其是做性能的,App、Framework、Kernel、Hardware 这些,都可以去看去学习。努力使自己处于「知道自己不知道」的状态,最糟糕的是「不知道自己不知道」还觉得优化没啥可做,从经验与常识来看,这都是错误的。
  4. 知识体系化:性能优化涉及到的知识非常广,平时分析和解决问题的时候,要多看代码、多记录、总结和归纳,把学到的知识体系化,不要想着一蹴而就,要厚积薄发。
  5. 利用好现有的资源:跟着星球内容、博客内容、大厂的优秀文章这些学习,多提问,不管有多初级。

关于提问

  1. 星球是一个很好的平台,很多让你困惑的问题别人也经历过。但是你要利用好他的前提是能问出准确的问题。如果问问题,是个技术活。
  2. 可以参考这篇文章:到底如何提问?https://mp.weixin.qq.com/s/l_Iz5pZ5yXhBAoPzNi4m-Q
    • 决定探讨、思考、回答的质量和效率。「提出好的问题已经解决了问题的一半」,说得没什么科学依据,但也不妨理解好问题的重要性。
    • 决定做事方向。曾经有人问爱因斯坦:「如果您有一个小时来拯救世界,您将如何使用?」他回答说:「我将花 55 分钟确定问题,然后花 5 分钟解决问题。」
    • 提问是认识世界和人的桥梁。好问题能激发优秀者的教学欲望,将宝贵信息倾囊相授。
    • 其实可以从一个人提问的能力来判断其段位。就像杨澜说的,「一个人的提问力,彰显在与外界互动质量的高低。」
  3. 尽量使用 Google 来搜索答案,百度和 Google 的答案差异很大。答案不区分中文资料和英文资料,能解决疑惑就是好资料,值得保存和分享(到星球或者星球微信群)。
  4. 需要提供日志分析问题时
    • 如果涉及到敏感内容,比如 Systrace 中涉及到敏感信息,可以使用文本软件,比如 VSCode 打开 Systrace 文件,一键将敏感信息(比如 package name)替换。
    • Log 中如果涉密,尽量还是不要发出来。
    • 如果是单独发给三个主理人,那么不脱敏也无所谓,我们承诺不会公开这些内容,只会做分析使用。
  5. 一个好的闭环:大家伙提问 -> 一起分析原因 -> 尝试各种方法解决 -> 通过数据验证有效果 -> 把结果分享给大家。

关于面试

有时候需要出去跟友商、新赛道的人沟通下,可以看看外部环境都在发生怎样的变化。首要目的不是为了跳槽(如果有好机会,跳也无妨),而是为了让自己在人力资源市场上有比较高的竞争力。

这里首先要区分职业与工作的区别。我是软件工程师,这是我的职业。我在 A 工作任职负责某个模块,这是我的工作。这两个是不一样的,如果你能把两者区分开(前提是能区分得开),那在择业、面对公司变化的时候都能从容应对。

最理想情况下,应该追求自己喜欢的职业,因为它伴随你绝大部分时间,如果这个职业本身不让你感到兴奋,你不太可能做出比较好的成绩,进而无法获得比较高的回报。

遇到好的工作,那就看天意了。这很复杂,与很多很多因素有关系。当你遇到面试不顺利,只能说这个工作不适合你,没有缘分,无法进一步说明更多的事情。

关于公司环境

不同的公司在不同的行业赛道、竞争格局上所采用的策略是不一样的。微观上,你的部门领导的风格以及同事们的结构,都影响到了你具体做事的环境。不同环境所追求的「回报最优值」是不一样的。

有的是需要你做到行业 5%,得到组织认可。有的是能把问题搞定就行而不关心如何搞定的,得到组织认可。

要么直接硬刚这种环境,按照你认为对的方式行事,要么就适应它。无论选择哪种,要知道局面是什么局面,而不是遇到与自己想象不一样的时候要么怀疑自己,要么以为这个世界就是这样。

关于学习新技术

对于优化业务来说,它是有底层逻辑可寻的。而这些底层逻辑遇到不同的技术栈、不同的局面的时候会藏的比较深。平时学习中,要努力找出这种底层不变的东西,将可变的东西套进去,一是增强对新技术的判断力,二是可以举一反三提出更进一步的优化方案。

不要在新技术的表面上游荡,一是自己觉得累,二是这没啥意义。

优化做到一定深度,肯定是往底层走。比如编译器、硬件特性,甚至硬件整合。因为这些东西直接决定了程序的速度,所以是绕不开的一道坎。

关于 GPU

  • 讲 GPU 架构相关:下面是一些 PDF,各时代、各厂商的 GPU 架构都不一样,当然也有相似之处,建议都看看。初学 GPU 架构的人会遇到很多专业名词,遇到了不懂的词要去搜索,然后弄懂,会有文章介绍,看完后会更加拓展你的视野,发现新的一片天地。
    1. GPU Architectures
    2. Introdution to GPU architecture
    3. Introduction to Modern GPU Architecture
    4. Graphics and Computing GPUs
    5. GPU Architecture and Function
  • 讲 GPU 架构相关的书,几乎没啥书
    • Mobile 3D Graphics SoC From Algorithm to Chip 这本书网上只有付费的,免费的 PDF 可以在我们星球上搜。
  • 讲 GPU 相关的博客,很多我找不到了,就列举了一些我能找到的
    1. 深入 GPU 硬件架构及运行机制 - 0 向往 0 - 博客园,这个论坛讲的内容比较成体系,作者面向的读者应该是游戏开发的人群,不过学习图形的人都可以看看。
    2. Tile Based 架构下的性能调校,关于 Tile base 如果性能调优。 厂商一般都没有 OpenGL 实现库源码,如果又对源码感兴趣,可以下载下面两个库代码看
    3. 谷歌的 swiftshader,在谷歌 android 源码根目录 external/swiftshader 下,一个完全用 CPU 实现的 OpenGL 3.0 的版本,代码可读性非常高,可以帮助大家理解 OpenGL 的各种原如:状态机、资源管理、API 实现等。
    4. Mesa 3D,一个开源的图形驱动库,网上有一些文档介绍可以搜搜。
  • 如果没有图形开发经验,想学习 Vulkan 开发个人推荐先学习 OpenGL:因为 GPU 底层原理和机制都一样,学习了 OpenGL 入门 Vulkan 会更加简单,因为 Vulkan API 更加底层如果直接学习 Vulkan 估计会比较困难。如果有了 OpenGL 开发基础,想学 Vulkan,我分享一下我的学习之路(可能不是最优的,大家参考一下即可):
    1. Vulkan Tutoria l 学习最简单的 Vulkan 例子,如:如何绘制一个三角形。
    2. 《Vulkan Programming Guide》By Graham Sellers and John Kessenich,网上有 PDF,也有中文版,这本是官方出版的书,一本没有感情介绍 Vulkan API 的书,比较赤裸裸的介绍 API,几乎不讲原理,我学习它的目的是要熟悉 Vulkan 的 API。
    3. 《Vulkan 学习指南》作者帕敏德·辛格,也是一本入门的书,我是微信读书看的中文版,不过翻译得很一般,能看英文的尽量看原版,这本书比较注重渲染的流程和原理的讲解,不过 API 的介绍不全,需要配合《Vulkan Programming Guide》一起看。
    4. 看了入门的书,那就要动手写代码了,所以我推荐大家看一本用 Vulkan 来介绍游戏开发的书,我个人看的吴亚峰的《Vulkan 开发实践指南》,这本书个人觉得写的很一般,我看它的主要原因是,我大学的时候看过这本书 OpenGL 版本的,这本书基本就是 OpenGL 版本直译过来,讲的比较冰冷。
  • 为什么都在推 Vulkan,Vulkan 比 OpenGL 有什么优势?可以看
    • 苹果开发官网的Bringing OpenGL Apps to Metal视频,Metal 和 Vulkan API 基本一样,目的也是一样,都是来自 AMD 的 Mantle,把视频中 Metal 当做 Vulkan 就可以了
    • 上面书中《Vulkan 学习指南》作者帕敏德·辛格,开编就有讲 OpenGL 和 Vulkan 差异

最后就是:学习图形开发的人一定要沉得住气耐得住寂寞,多看书多练习,不然很难学得精通,这是一个长期的过程,不是看几本书就能精通的。学习 GPU 架构的人,国内基本没有一家像样的 GPU 供应商,GPU 方面资料会很少,所以一定要多到外网去收集学习资料,可以多用 GPU 调试工具调试去分析应用程序,去了解 GPU 内部的 Counters,如 DS5 Streamline、Snapdragon Profiler 等,另外在没有很全面的学习资料的前提下,去学习一门更加底层的图形开发技术对学习和了解 GPU 架构也很重要,如 Vulkan,这会有助于自顶而下的去了解 GPU 内部的工作原理

关于提到的文章、书

文章:十年创业者万字长文分享招人

文章地址:https://presence.feishu.cn/docs/doccn71hTTKbaRGF8RvD2XzLJEK,里面列举了作者总结的 S、A、B、C 类人才,大家可以对照一下自己做事的方式,看看更高级别的人才是怎么做事的

书籍:程序员的自我修养 by 俞甲子 / 石凡 / 潘爱民

豆瓣地址:https://book.douban.com/subject/3652388/

这本书主要介绍系统软件的运行机制和原理,涉及在 Windows 和 Linux 两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。每个技术专题都配备了大量图、表和代码实例,力求将复杂的机制以简洁的形式表达出来。本书最后还提供了一个小巧且跨平台的 C/C++运行库 MiniCRT,综合展示了与运行库相关的各种技术。

对装载、链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书。同时,还可作为各行业从事软件开发的工程师、研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材。

书籍:软件调试(第 2 版) by 张银奎

本书堪称是软件调试的“百科全书”。作者围绕软件调试的“生态”系统(ecosystem)、异常(exception)和调试器 3 条主线,介绍软件调试的相关原理和机制,探讨可调试性(debuggability)的内涵、意义以及实现软件可调试性的原则和方法,总结软件调试的方法和技巧。

卷 1:硬件基础

豆瓣地址:https://book.douban.com/subject/30379453/

第 1 卷主要围绕硬件技术展开介绍。全书分为 4 篇,共 16 章。第一篇“绪论”(第 1 章),介绍了软件调试的概念、基本过程、分类和简要历史,并综述了本书后面将详细介绍的主要调试技术。第二篇“CPU 及其调试设施”(第 2 ~ 7 章),以英特尔和 ARM 架构的 CPU 为例系统描述了 CPU 的调试支持。第三篇“GPU 及其调试设施”(第 8 ~ 14 章),深入探讨了 Nvidia、AMD、英特尔、ARM 和 Imagination 这五大厂商的 GPU。第四篇“可调试性”(第 15 ~ 16 章),介绍了提高软件可调试性的意义、基本原则、实例和需要注意的问题,并讨论了如何在软件开发实践中实现可调试性。

本书理论与实践紧密结合,既涵盖了相关的技术背景知识,又针对大量具有代表性和普遍意义的技术细节进行了讨论,是学习软件调试技术的宝贵资料。本书适合所有从事软件开发工作的读者阅读,特别适合从事软件开发、测试、支持的技术人员,从事反病毒、网络安全、版权保护等工作的技术人员,以及高等院校相关专业的教师和学生学习参考。

卷 2:Windows 平台调试

书籍信息:https://book.douban.com/subject/35233332/

第 2 卷分为 5 篇,共 30 章,主要围绕 Windows 系统展开介绍。第一篇(第 1- 4 章)介绍 Windows 系统简史、进程和线程、架构和系统部件,以及 Windows 系统的启动过程,既从空间角度讲述 Windows 的软件世界,也从时间角度描述 Windows 世界的搭建过程。第二篇(第 5-8 章)描述特殊的过程调用、垫片、托管世界和 Linux 子系统。第三篇(第 9-19 章)深入探讨用户态调试模型、用户态调试过程、中断和异常管理、未处理异常和 JIT 调试、硬错误和蓝屏、错误报告、日志、事件追踪、WHEA、内核调试引擎和验证机制。第四篇(第 20-25 章)从编译和编译期检查、运行时库和运行期检查、栈和函数调用、堆和堆检查、异常处理代码的编译、调试符号等方面概括编译器的调试支持。第五篇(第 26-30 章)首先纵览调试器的发展历史、工作模型和经典架构,然后分别讨论集成在 Visual Studio 和 Visual Studio(VS)Code 中的调试器,最后深度解析 WinDBG 调试器的历史、结构和用法。

本书理论与实践结合,不仅涵盖了相关的技术背景知识,还深入研讨了大量具有代表性的技术细节,是学习软件调试技术的珍贵资料。

这本书应当还有卷 3,此卷里会讲基于 Linux 平台的调试方法

关于 The Performance 知识星球

The Performance 是一个分享 Android 开发领域性能优化相关的圈子,主理人是三个国内一线手机厂商性能优化方面的一线开发者,有多年性能相关领域的知识积累和案例分析经验,可以提供性能、功耗分析知识的一站式服务,涵盖了基础、方法论、工具使用和最宝贵的案例分析

星球 免费版,定位是知识分享和交流,也可以微信扫码加入

微信扫一扫

关于我 && 博客

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

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

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

微信扫一扫

🔲 ☆

Android 性能优化的术、道、器

黑客与画家

Paul Graham 在其著作 <黑客与画家> 中断言:“不同语言的执行效率差距正变得越来越大,所以性能分析器(profiler)将变得越来越重要。目前,性能分析并没有受到重视。许多人好像仍然相信,程序运行速度提升的关键在于开发出能够生成更快速代码的编译器。代码效率与机器性能的差距正在不断加大,我们将会越来越清楚地看到,应用软件运行速度提升的关键在于有一个好的性能分析器帮助指导程序开发。”
by Paul Graham 黑客与画家

谷歌搜索 「Android 优化工具」,你会找到很多与此相关的内容。他们的问题在于要么是内容高度重复、要么是直接讲使用方法,很少会给你介绍整体性的架构,一不小心就会让人会种「一个工具搞定一切」的错误认知。以笔者团队的多年经验来看,在性能分析领域这种银弹级别的工具是不存在的。工具在发展,老问题会以新的方式变样出现,不掌握核心逻辑的话始终会让你浮于技术的表面。

本文首先系统性的梳理性能分析中的可观测性技术,它涵盖数据类型、抓取方法以及分析方法等三部分内容,之后是介绍谷歌提供的「三大件」分析工具。目的是想让你了解不变的理论性的知识,以及与之对应的在安卓环境中可用的工具,这些可以让你少走一些弯路,直接复用前辈们的经验。

需要特别说明的是,对于性能优化肯定不止有这三个工具可用,但这个三个工具是我们平时用到的「第一手工具」。进行进一步分析之前,你都需要依赖这三个工具进行瓶颈定位,之后才应不同领域特性选择对应的工具进行下钻分析。

1 性能分析中的可观测性技术

  • 这个操作到底有没有被执行?执行时间有多长?
  • 为什么两个版本的前后差异这么大?
  • 当 CPU 使用量变高的时候系统都在执行什么操作?
  • 为什么启动速度变慢了?
  • 为什么这个页面滑动总是会卡一下?

相信你不止一次被同事、被老板问到过类似的问题。最原始的想法应该是,首先是拿到相关的日志进行逐个分析。根据以往经验,通过查找关键字寻找蛛丝马迹。如果没有想看的信息,那就加上日志尝试本地复现。费时费力不说,也还费研发资源。但你有没有想过行业里有没有更高效的方法?可以提高一个数量级的那种,把我们的时间花在问题解决上而不是无聊的重复性体力活儿上?

答案当然是有的(否则就不会有这篇文章了),我们称他为可观测性技术。

计算机行业发展至今,计算机前辈们捣鼓出了所谓的「可观测性技术」的类别。它研究的是通过工具,来观测复杂系统的运行细节,内容越细越好。 移动操作系统之前是由嵌入式发展而来的,现在的中高端安卓手机算力都能赶得上二十几年前的一个主机的算力,在此算力基础上所带来的软件复杂度也是非常巨大的。

如果你的程序部署了一个精心设计且运行良好的可观测性技术,可以大大加快研发软件的效率,因为即使我们使用了各种各样的前置性静态代码检测、人工代码审查,也无法 100% 拦截软件的问题。只有在真实环境里运行之后才知道是否真正发生了问题,即使这个环境可能是一个你的自动化测试用例。即使这样,你还需要翻阅你的日志,重读代码来找出问题。出于这些原因,每个工程团队都需要有一个功能完备的可观测性工具作为他们的基础设施之一。

可观测性技术是一个系统性工程,它能够让你更深入的了解软件里发生的事情。可用于了解软件系统内部运行过程(特别是对于业务逻辑或者交互关系复杂的系统)、排查问题甚至通过寻找瓶颈点优化程序本身。对于复杂的系统来说,你通过阅读代码来了解整个运行过程其实是很困难的事情,更高效的方法就是借助此类工具,以最直观的的方式获取软件运行的状态。

下面将从 数据类型、数据获取方法、分析方法 这三个主题来帮助你了解可观测性技术。

1.1 数据类型

日志的形式可能是键值对(key=Value),JSON、CSV,关系型数据库或者其他任何格式。其次我们通过日志还原出系统当时运行的整个状态,目的是为了解决某个问题,观察某个模块的运行方式,甚至刻画系统使用者的行为模式。在可观测性技术上把日志类型分类为 Log 类型、Metric 类型,以及 Trace 类型。

数据类型

Log 类型

Log 是最朴素的数据记录方式,一般记录了什么模块在几点发生了什么事情,日志等级是警告还是错误。 绝大部分系统,不管是嵌入式设备还是汽车上的计算机,他们所使用的日志形式几乎都是这种形式。这是最简单,最直接也最好实现的一种方式。几乎所有的 Log 类型是通过 string 类型的方式存储,数据呈现形式是一条一条的文本数据。Log 是最基本的类型,因此通过转换,可以将 Log 类型转换成 Metric 或者 Trace 类型,当然成本就是转换的过程,当数据量非常巨大的时候这可能会成为瓶颈。

为了标识出不同的日志类型等级,一般使用错误、警告、调试等级别来划分日志等级。显然,错误类型的是你首要关注的日志等级。不过实践中也不会严格按照这种方式划分,因为很多工程师不会严格区分他们之间的差异,这可能是他们的工程开发环境中不太会对不同等级的日志进行分类分析有关。总之,你可以根据你的目的,将 Log 类型进行等级划分,它就像一个索引一样,可以进一步可以提高分析问题、定位目标信息的效率。

Metric 类型

Metric 类型相比 Log 类型使用目的上更为聚焦,它记录的是某个维度上数值的变化。知识点是「维度」与「数值」的变化。维度可能是 CPU 使用率、CPU Cluster 运行频率,或者上下文切换次数。数值变化既可以是采样时候的瞬时值(成为快照型)、与前一次采样时的差值(增或减)、或者某个时段区间的统计聚合值。实践中经常会使用统计值,比如我想看问题发生时刻前 5 分钟的 CPU 平均使用量。这时候需要将这五分钟内的所有数值做算数平均计算,或者是加权平均(如: 离案发点越近的样本它的权重就越高)。Log 类型当然可以实现 Metric 类型的效果,但是操作起来非常麻烦而且其性能损耗可能也不小。

聚合是非常有用的工具,因为人不可能逐个分析所有的 Metric 值,因此借助聚合的方式判断是否出了问题之后再进行详细的分析是更为经济高效的方法。

Metric 类型的另外一个好处是它的内容格式是比较固定的,因此可以通过预编码的方式进行数据存储,空间的利用率会更紧凑进而占用的磁盘空间就更少。最简单的应用就是数据格式的存储上,如果使用 Log 类型,一般采用的是 ASCII 编码,而 Metric 使用的是整数或者浮点等固定 byte 数的数据,当存储较大数值时显然 ASCII 编码需要的字节数会多于数字型数据,并且在进行数据处理的时候你可以直接使用 Metric 数据,而不需要把 Log 的 ASCII 转换成数字型后再做转换。

除了是具体的数值之外,也可以存储枚举值(某种程度上它的本质就是数值)。不同的枚举值代表不同的意义,可能是开和关、可能是不同的事件类型。

Trace 类型

Trace 类型标识了事件发生的时间、名称、耗时。多个事件通过关系,标识出了是父与子还是兄弟。当分析多个线程间复杂的调用关系时 Trace 类型是最方便的数据分析方式。

Trace 类型特别适用于 Android 应用与系统级的分析场景,因为用它可以诊断:

  1. 函数调用链
  2. Binder 调用时的调用链
  3. 跨进程事件流跟踪

Android 的应用程序运行环境的设计中,一个应用程序是无法独自完成所有的功能的,它需要跟 SystemServer 有大量的交互才能完成它的很多功能。与 SystemServer 间的通讯是通过 Binder 完成,它的通讯方式后面的文章再详细介绍,到目前为止你只需要知道它的调用关系是跨进程调用即可。这需要本端与远端的数据才能准确还原出调用关系,Trace 类型是完成这种信息记录的最佳方式。

Trace 类型可以由你手动添加开始与结束点,在一个函数里可以添加多个这种区间。通过预编译技术或者编程语言的特性,在函数的开头与结尾里自动插桩 Trace 区间。理想情况下后者是最好的方案,因为我们能知道系统中运行的所有的函数是哪些、执行情况与调用关系是什么。可以拿这些信息统计出调用次数最多(最热点)的函数是什么,最耗时的函数又是什么。可想而知这种方法带来的性能损耗非常大,因为函数调用的频次跟量级是非常大的,越是复杂的系统量级就越大。

因此有一种迂回的方法,那就通过采样获取调用栈的方式近似拟合上面的效果。采样间隔越短,就越能拟合真实的调用关系与耗时,但间隔也不能太小因为取堆栈的操作本身的负载就会变高因为次数变多了。这种方法,业界管他叫 Profiler,你所见过的绝大部分编程语言的 Profiler 工具都是基于这个原理实现的。

1.2 数据获取方法

数据获取方法

静态代码与动态跟踪

静态代码的采集方式是最原始的方式,优点是实现简单缺点是每次新增内容的时候需要重新编译、安装程序。当遇到问题之后你想看的信息恰好没有的话,就没有任何办法进一步定位问题,只能重新再来一遍整个过程。更进一步的做法是预先把所有可能需要的地方上加入数据获取点,通过动态判断开关的方式选择是否输出,这既可以控制影响性能又能够在需要日志的时候可以动态打开,只不过这种方法的成本非常高。

动态跟踪技术其实一直都存在,只是它的学习成本比较高,被誉为调试跟踪领域里的屠龙刀。它需要你懂比较底层的技术,特别是编译、ELF 格式、内核、以及熟悉代码中的预设的探针、动态跟踪所对应的编程语言。对,你没看错,这种技术甚至还有自己的一套编程语言用于「动态」的实现开发者需求。这种方式兼具性能、灵活性,甚至线上版本里遇到异常后可以动态查看你想看的信息。

Android 应用开发、系统级开发中用的比较少,内核开发中偶尔会用一些。只有专业、专职的性能分析人员才可能会用上这类工具。它有两个关键点,探针与动态语言,程序运行过程中需要有对应的探针点将程序执行权限交接到动态跟踪框架,框架执行的逻辑是开发者使用动态语言来编写的逻辑。

所以,你的程序里首先是要有探针,好在 Linux 内核等框架埋好了对应的探针点,但是 android 应用层是没有现成的。所以目前 Android 上能用动态框架,如 eBPF 基本都是内核开发者在使用。

无条件式抓取与有条件式抓取

无条件式抓取比较好理解,触发抓取之后不管发生任何事情,都会持续抓取数据。缺点是被观测对象产生的数据量非常大的时候可能会对系统造成比较大的影响,这种时候只能通过降低数据量的方式来缓解。需要做到既能满足需求,性能损失又不能太大。

有条件式抓取经常用在可以识别出的异常的场景里。比如当系统的某个观测值超过了预先设定的阈值时,此时触发抓取日志并且持续一段时间或者达到另外一种阈值之后结束抓取。这相比于前面一个方法稍微进步了一些,仅在出问题的时候对系统有影响,其他时候没有任何影响点。但它需要你能够识别出异常,并且这种异常是不需要异常发生之前的历史数据。当然你可以通过降低阈值来更容易达到触发点,这可能会提高触发数据抓取的概率,这时候会遇到前面介绍的无条件式抓取遇到的同样的问题,需要平衡性能损失。

落盘策略

持续落盘是存储整个数据抓取过程中的所有数据,代价是存储的压力。如果能知道触发点,比如能够检测到异常点,这时候可以选择性的落盘。为了保证历史数据的有效性,因此把日志先暂存储到 RingBuffer 中,只有接受到落盘指令后再进行落盘存储。这种方式兼顾了性能与存储压力,但成本是运行时内存损耗与触发器的准确性。

1.3 分析方式

分析方式

数据可视化分析

随着问题分析的复杂化,出现了要解决多个模块间交互的性能问题需求,业界就出现了以时间为横轴把对应事件放到各自泳道上的数据可视化分析方法,可以方便的看到所关心事件什么时候发生、与其他系统的交互信息等等。在 Android 里我们常用的 Systrace/Perfetto 以及更早之前的 KernelShark 等工具本质上都是这一类工具。在「数据类型」提到的 「Trace 类型」,经常采用这种可视化分析方法。

Systrace 的可视化框架是基于 Chrome 的一个叫 Catapult 的子项目构建。Trace Event Format 讲述了 Catapult 所支持的数据格式,如果你有 Trace 类型的数据,完全可以使用此框架来展示可视化数据。AOSP 编译系统,安卓应用的编译过程,也都有相应的 Trace 文件输出,它们也都基于 Catapult 实现了可视化效果。

数据库分析

面对大量数据分析的分析,通过对数据进行格式化,把他们转换成二维数据表,借助 SQL 语言可实现高效的查询操作。在服务器领域中 ELK 等技术栈可以实现更为灵活的格式化搜索与统计功能。借助数据库与 Python,你甚至可以实现一套自动化数据诊断工具链。

从上面的讨论可知,从文本分析到数据库分析他们要面对的分析目的是不一样的。单纯的看一个模块的耗时用文本分析就够用了,多个系统间的交互那就要用可视化工具,复杂的数据库分析就要用到 SQL 的工具。无论哪种分析方式,本质上都是针对数据的分析,在实战中我们经常会通过其他工具对数据进行转换以支持不同的分析方式,比如从文本分析方式改成数据库分析方式。

根据自己的目的,选择合适的分析方式才会让你的工作事倍功半。

对于 Android 开发者来说,Google 提供了几个非常重要的性能分析工具,帮助系统开发者、应用开发者来优化他们的程序。

2 谷歌提供的 Andorid 性能分析工具

从实践经验来看最常用的工具有 Systrace,Perfetto 与 Android Studio 中的 Profiler 工具。通过他们定位出主要瓶颈之后,你才需要用到其他领域相关工具。因此,会重点介绍这三个工具的应用场景,它的优点以及基本的使用方法。 工具之间的横向对比,请参考下一个「综合对比」这一章节的内容。

2.1 初代系统性能分析工具 - Systrace

Systrace 是 Trace 类型的可视化分析工具,是第一代系统级性能分析工具。Trace 类型所支持的功能它都有支持。在 Perfetto 出现之前,基本上是唯一的性能分析工具,它将 Android 系统和 App 的运行信息以图形化的方式展示出来,与 Log 相比,Systrace 的图像化方式更为直观;与 TraceView 相比,抓取 Systrace 时候的性能开销基本可以忽略,最大程度地减少观察者效应带来的影响。

Systrace

Systrace 的设计思路

系统的一些关键操作(比如 Touch 操作、Power 按钮、滑动操作等)、系统机制(input 分发、View 绘制、进程间通信、进程管理机制等)、软硬件信息(CPU 频率信息、CPU 调度信息、磁盘信息、内存信息等)的关键流程上,插入类似 Log 的信息,我们称之为 TracePoint(本质是 Ftrace 信息),通过这些 TracePoint 来展示一个核心操作过程的执行时间、某些变量的值等信息。然后 Android 系统把这些散布在各个进程中的 TracePoint 收集起来,写入到一个文件中。导出这个文件后,Systrace 通过解析这些 TracePoint 的信息,得到一段时间内整个系统的运行信息。

Android 系统中,一些重要的模块都已经默认插入了一些 TracePoint,通过 TraceTag 来分类,其中信息来源如下

  1. Framework Java 层的 TracePoint 通过 android.os.Trace 类完成
  2. Framework Native 层的 TracePoint 通过 ATrace 宏完成
  3. App 开发者可以通过 android.os.Trace 类自定义 Trace

这样 Systrace 就可以把 Android 上下层的所有信息都收集起来并集中展示,对于 Android 开发者来说,Systrace 最大的作用就是把整个 Android 系统的运行状态,从黑盒变成了白盒。全局性和可视化使得 Systrace 成为 Android 开发者在分析复杂的性能问题的时候的首选。

实践中的应用情况

解析后的 Systrace 由于有大量的系统信息,天然适合分析 Android App 和 Android 系统的性能问题, Android 的 App 开发者、系统开发者、Kernel 开发者都可以使用 Systrace 来分析性能问题。

  1. 从技术角度来说,Systrace 可覆盖性能涉及到的 响应速度卡顿丢帧ANR 这几个大类。
  2. 从用户角度来说,Systrace 可以分析用户遇到的性能问题,包括但不限于:
    1. 应用启动速度问题,包括冷启动、热启动、温启动
    2. 界面跳转速度慢、跳转动画卡顿
    3. 其他非跳转的点击操作慢(开关、弹窗、长按、选择等)
    4. 亮灭屏速度慢、开关机慢、解锁慢、人脸识别慢等
    5. 列表滑动卡顿
    6. 窗口动画卡顿
    7. 界面加载卡顿
    8. 整机卡顿
    9. App 点击无响应、卡死闪退

在遇到上述问题后,可以使用多种方式抓取 Systrace ,将解析后的文件在 Chrome 打开,然后就可以进行分析

2.2 新一代性能分析全栈工具 - Perfetto

谷歌在 2017 年开始了第一笔提交,随后的 4 年(截止到 2021.12)内总共有 100 多位开发者提交了近 3.7W 笔提交,几乎每天都有 PR 与 Merge 操作,是一个相当活跃的项目。 除了功能强大之外其野心也非常大,官网上号称它是下一代面向可跨平台的 Trace/Metric 数据抓取与分析工具。应用也比较广泛,除了 Perfetto 网站,Windows Performance ToolAndroid Studio,以及华为的 GraphicProfiler 也支持 Perfetto 数据的可视化与分析。 我们相信谷歌还会持续投入资源到 Perfetto 项目,可以说它应该就是下一代性能分析工具了,会完全取代 Systrace。

提供的亮点功能

Perfetto 相比 Systrace 最大的改进是可以支持长时间数据抓取,这是得益于它有一个可在后台运行的服务,通过它实现了对收集上来的数据进行 Protobuf 的编码并存盘。从数据来源来看,核心原理与 Systrace 是一致的,也都是基于 Linux 内核的 Ftrace 机制实现了用户空间与内核空间关键事件的记录(ATRACE、CPU 调度)。Systrace 提供的功能 Perfetto 都支持,由此才说 Systrace 最终会被 Perfetto 替代。

Perfetto

Perfetto 所支持的数据类型、获取方法,以及分析方式上看也是前所未有的全面,它几乎支持所有的类型与方法。数据类型上通过 ATRACE 实现了 Trace 类型支持,通过可定制的节点读取机制实现了 Metric 类型的支持,在 UserDebug 版本上通过获取 Logd 数据实现了 Log 类型的支持。

你可以通过 Perfetto.dev 网页、命令行工具手动触发抓取与结束,通过设置中的开发者选项触发长时间抓取,甚至你可以通过框架中提供的 Perfetto Trigger API 来动态开启数据抓取,基本上涵盖了我们在项目上能遇到的所有的情境。

在数据分析层面,Perfetto 提供了类似 Systrace 操作的数据可视化分析网页,但底层实现机制完全不同,最大的好处是可以支持超大文件的渲染,这是 Systrace 做不到的(超过 300M 以上时可能会崩溃、可能会超卡)。在这个可视化网页上,可以看到各种二次处理的数据、可以执行 SQL 查询命令、甚至还可以看到 logcat 的内容。Perfetto Trace 文件可以转换成基于 SQLite 的数据库文件,既可以现场敲 SQL 也可以把已经写好的 SQL 形成执行文件。甚至你可以把他导入到 Jupyter 等数据科学工具栈,将你的分析思路分享给其他伙伴。

比如你想要计算 SurfaceFlinger 线程消耗 CPU 的总量,或者运行在大核中的线程都有哪一些等等,可以与领域专家合作,把他们的经验转成 SQL 指令。如果这个还不满足你的需求, Perfetto 也提供了 Python API,将数据导出成 DataFrame 格式近乎可以实现任意你想要的数据分析效果。

这一套下来供开发者可挖掘的点就非常多了,从笔者团队的实践来看,他几乎可以覆盖从功能开发、功能测试、CI/CD 以及线上监控、专家系统等方方面面。本星球的后续系列文章中,也会重点介绍 Perfetto 的强大功能与基于它开发的专家系统,可以帮助你「一键解答」性能瓶颈。

实践中的应用情况

性能分析首要用到的工具就是 Perfetto,使用 Systrace 的场景是越来越少了。所以,你首要掌握的工具应该是 Perfetto,学习它的用法以及它提供的指标。

不过 Perfetto 也有一些边界,首先它虽然提供了较高的灵活性但本质上还是静态数据收集器,不是动态跟踪工具,跟 eBPF 还是有本质上的差异。其次运行时成本比较高,因为涉及到在手机中实现 Ftrace 数据到 Perfetto 数据的转换。最后他不提供文本分析方式,只能通过网页可视化或者操作 SQLite 来进行额外的分析了。综合来看 Perfetto 是功能强大,几乎涵盖了可观测性技术的方方面面,但是使用门槛也比较高。值得挖掘与学习的知识点比较多,我们后续的文章中也会重点安排此部分的内容。

2.3 Android Studio Profiler 工具

Android 的应用开发集成环境(官方推荐)是 Android Studio (之前是Eclipse,不过已经淘汰了) ,它自然而然也需要把开发和性能调优集成一起。非常幸运的是,随着 Android Studio 的迭代、演进,到目前,Android Studio 有了自己的性能分析工具 Android Profiler,它是一个集合体,集成了多种性能分析工具于一体,让开发者可以在 Android Studio 做开发应用,也不用再下载其它工具就能让能做性能调优工作。

目前 Android Studio Profiler 已经集成了 4 类性能分析工具: CPU、Memory、Network、Battery,其中 CPU 相关性能分析工具为 CPU Profiler,也是本章的主角,它把 CPU 相关的性能分析工具都集成在了一起,开发者可以根据自己需求来选择使用哪一个。可能很多人都知道,谷歌已经开发了一些独立的 CPU 性能分析工具,如 Perfetto、Simpleperf、Java Method Trace 等,现在又出来一个 CPU Profiler,显然不可能去重复造轮子,CPU Profiler 目前做法就是:从这些已知的工具中获取数据,然后把数据解析成自己想要的样式,通过统一的界面展示出来。

提供的亮点功能

CPU Profiler 集成了性能分析工具:Perfetto、Simpleperf、Java Method Trace,它自然而然具备了这些工具的全部或部分功能,如下:

  1. System Trace Recording,它是用 Perfetto 抓取的信息,可用于分析进程函数耗时、调度、渲染等情况,但是它一个精简版,只能显示进程强相关的信息且会过滤掉耗时短的事件,建议将 Trace 导出文件后在 https://ui.perfetto.dev/ 上进行分析。
  2. Java Method Trace Recording,它是从虚拟机获取函数调用栈信息,用于分析 Java 函数调用和耗时情况。
  3. C/C++ Function Trace,它是用 Simpleperf 抓取的信息,Simpleperf 是从 CPU 的性能监控单元 PMU 硬件组件获取数据。 C/C++ Method Trace 只具备 Simpleperf 部分功能,用于分析 C/C++ 函数调用和耗时情况。

CPU Profiler

实践中的应用情况

应用的性能问题主要分为两类:响应慢、不流畅。

  • 响应慢问题常有:应用启动慢、页面跳转慢、列表加载慢、按钮响应慢等
  • 不流畅问题常有:列表滑动不流畅、页面滑动不跟手、动画卡顿等

CPU Profiler 在这些场景中要如何使用呢?基本的思路是:首先就要抓 System Trace,先用System Trace 分析、定位问题,如果不能定位到问题,再借助 Java Method Trace 或 C/C++ Function Trace 进一步分析定位。

以一个性能极差的应用为例,在系统的关键位置插了 Systrace TracePoint,假设对代码不熟悉,那要怎么找到性能瓶颈呢?我们先把应用跑起来,通过 CPU Profiler 录制一个 System Trace (后面文章会介绍工具的使用方法)如下:

通过上面 Trace 可以知道是在 egl_core 线程中的 onDrawFrame 操作耗时,如果发现不了问题,建议导出到 https://ui.perfetto.dev/ 进一步分析,可以查找源代码看看 onDrawFrame 是什么东西, 我们通过查找发现 onDrawFrame 是 Java 函数 onDrawFrame 的耗时,要分析 Java 函数耗时情况,我们要录制一个 Java Method Trace,如下:

通过上面 Trace 很容易发现是一个叫做 Utils.onDraw 的 native 函数耗时,因为涉及到C/C++ 代码,所以要再录制一个 C/C++ Function Trace 进一步分析,如下:

可以发现在 native 的 Java_com_gl_shader_Utils_onDraw 中代码执行了 sleep,它就是导致了性能低下的罪魁祸首!

AS 中的 CPU Profiler 最大优势是集成了各种子工具,在一个地方就能操作一切,对应用开发者来说是非常方便的,不过对系统开发者来说可能没那么幸运。

2.4 综合对比

工具名称应用场景数据类型获取方法分析方式
SystraceAndroid 系统与应用性能分析Trace 类型无条件抓取 持续落盘可视化分析
PerfettoAndroid 系统与应用性能分析Metric 类型 Trace 类型无条件抓取 持续落盘可视化分析 数据库分析
AS ProfilerAndroid 系统与应用性能分析Trace 类型无条件抓取 持续落盘可视化分析
SimplePerfJava/C++ 函数执行耗时 分析 PMU 计数器Trace 类性无条件抓取 持续落盘可视化分析 文本分析
Snapdragon Profiler Tools & Resources主要是高通 GPU 性能分析器Trace 类型 Metric 类型无条件抓取 持续落盘可视化分析
Mali Graphics DebuggerARM GPU 分析器(MTK、麒麟芯片)Trace 类型 Metric 类型无条件抓取 持续落盘可视化分析
Android Log/dumpsys综合分析Log 类型有条件抓取 持续抓取但不落盘文本分析
AGI(Android GPU Inspector)Android GPU 分析器Trace 类型 Metric 类型无条件抓取 持续落盘可视化分析
eBPFLinux 内核行为动态跟踪Metric 类型动态跟踪 有条件抓取 持续抓取但不落盘文本分析
FTraceLinux 内核埋点Log 类型静态代码 有条件抓取 持续抓取但不落盘文本分析

3 关于「器、术、道」

技术上的变革、改进更多是体现在「器」层面,Linux 社区以及谷歌所开发的工具发展方向朝着提高工具的集成化使得在一个地方可以方便查到所需的信息、或者是朝着获取更多信息的方向发展。总之,器层面他们的发展轨迹是可寻的,可总结出发展规律。 我们需要在工具快速迭代的时候准确的认识到他们能力以及应用场景,其目的是提高解决问题的效率,而不是把时间花在学习新工具上。

「术」层面依赖具体的业务知识,知道一帧是如何被渲染的、CPU 是如何选择进程调度的、IO 是如何被下发的等等。只有了解了业务知识才能正确的选择工具并正确的解读工具所提供的信息。随着经验的丰富,有时候你都不需要看到工具提供的详细信息,也可以查到蛛丝马迹,这就是当你业务知识丰富到一定程度,大脑里形成了复杂的关联性信息之后凌驾于工具之上的一种能力。

「道」层面思考的是要解决什么问题,问题的本质是什么?做到什么程度以及需要投入什么样的成本达成什么样的效果。为了解决一个问题,什么样的路径的「投入产出比」是最高的?整体打法是什么样?为了完成一件事,你首先要做什么其次是做什么,前后依赖关系的逻辑又是什么?

后续的文章中,会依照「器、术、道」方式讲解一个技术、一个功能,我们不止想让你学习到一个知识点,更想激发你举一反三的能力。遇到类似的工具或者类似的问题、更进一步是完全不同的系统,都能够从容应对。牢牢抓住本质,通过评估「投入产出比」选择合适的工具或信息,高效解决问题。

4 关于「The Performance 知识星球」

为了更好地交流与输出高质量文章,我们创建了名为 「The Performance」的知识星球,主理人是三个国内一线手机厂商性能优化方面的一线开发者,有多年性能相关领域的工作经验,提供Android 性能相关的一站式知识服务,涵盖了基础、方法论、工具使用和最宝贵的案例分析。

目前星球的内容规划如下(两个 ## 之间的是标签,相关的话题都会打上对应的标签,方便大家点击感兴趣的标签查看对应的知识)

  • #The Performance# — 可以提早阅读「Android 性能优化 - 系统性课程」的电子书,每周会放出已经写好的章节。「Android 性能优化 - 系统性课程」是我们规划的一本讲 Android 性能优化的电子书,目前开发者社区有相当多高质量的性能优化理论知识和实践文章和开源库,但是目前市面上缺乏一个完整的、系统性的、包含了性能优化原理、工具、实践等内容、面向初级开发中和中级开发者、面向 App 开发者和系统开发者,且持续更新的 Android 性能优化工具书。书的大纲 (暂定) 我们已经基本上列好了,预计会花费一年左右的时间来完成,在星球中会放出写好的章节,让大家提前看到。
    • Part 1: → 性能工程
    • Part 2: → 以性能角度分析 Android 交互与核心系统
    • Part 3: → 以性能角度分析 Linux 内核核心子系统设计与实现
    • Part 4: → 问题场景分析思路
    • Part 5: → 分析与调试工具
    • Part 6: → 质量守护 - 性能监控方法与工具
  • #性能工具# — 分享 Android 开发中使用到的性能分析工具以及其使用方法,同时也提供 1V1 的 Systrace、Perfetto 等性能工具的视频指导。性能工具的使用,最好还是以视频的方式展示会直观很多,文章是静态的,很多地方比较难讲清楚,1V1 的视频会议指导也算是一个学习的方法
  • #案例分析# — 典型案例分析思路总结、球友提供的案例分析与讨论。案例分析是学习的一个很重要的途径,阅读大量的实际性能案例对以后自己分析和解决性能问题是非常有帮助的,同时也欢迎大家提供案例和解决方法,怕泄露信息的话,我们会对关键信息进行打码
  • #经典解读# — 经典方案、课程重读,例如优秀的三方库解析、Android 开发高手课重读等。比如可以对方案进行深度的剖析,横向对比等;对 Android 开发高手课进行重读和查漏补缺
  • #知识分享# — 优秀文章、博客、工具分享。业界有很大大牛的博客、经过实际业务考验的开源方案、各种性能工具等,我们会寻找这些优秀的内容,分享给大家
  • #知识沉淀# — 微信群聊精华、微信问答、博客留言解答等
  • #性能面试# — Android 性能相关的面试题搜集和解答,也算是刚需了吧
  • #编程语言# — 编程语言相关的使用技巧分享
  • #效能提升# — 效能提升分享,包括开发者开发效能、工作效能提升方法、工程效率、工具推荐等,磨刀不误砍柴工嘛
  • #行业动态# — 性能相关新技术第一时间解读报告,包括但不限于下面的内容
    • 行业峰会、学术峰会新思路解读报告
    • 论文、行业、书籍介绍、视频
    • Android 大版本性能相关介绍
    • Android 新硬件性能相关内容介绍
    • Android 性能相关开源项目解读
  • #大咖分享# — 每月定期邀请行业大咖进行经验分享、案例分析
  • #工作内推# — 各大厂商内推工作机会介绍

TeamWork - 付费知识星球

注意: iOS 手机用户不要直接在星球里面付款,在微信界面长按图片扫描二维码加入即可,否则苹果会收取高昂的手续

5 附录

关于我 && 博客

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

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

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

微信扫一扫

❌