Files
xianyan/docs/flutter-debug-workflow.md
Developer 63a0559721 refactor: 重构项目路由与模块结构,统一发现页命名与路径
1. 全局替换tool_center/inspiration为discover模块,统一路由路径
2. 调整AppRoutes路由常量,将discover作为主Tab页,inspiration作为子页面
3. 更新页面注册表与路由配置,修正跳转目标
4. 调整启动页可选配置项,修正路由ID对应关系
5. 新增翻译服务、内容发现、热搜相关工具类与数据模型
6. 修复缓存清理后未刷新统计的问题,调整x86_64架构注释
7. 更新AGENTS.md文档约束规则
8. 新增一批调试用截图资源文件
2026-05-28 06:42:20 +08:00

889 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Flutter AI 全流程闭环调试指南:看 → 操作 → 分析 → 修复
> 创建时间: 2026-05-28 | 版本: 1.1 | 测试环境: Windows + Android 真机 + Flutter 3.11.5
> 上次更新: 2026-05-28 | 更新内容: 补充实测结果,修正 API 参数,新增关键发现和注意事项
---
## 目录
- [0. 前置准备](#0-前置准备)
- [1. 看 — 观察应用状态](#1-看--观察应用状态)
- [1.1 获取 Widget 树](#11-获取-widget-树)
- [1.2 截取屏幕截图](#12-截取屏幕截图)
- [1.3 检测布局溢出和错误](#13-检测布局溢出和错误)
- [1.4 获取 Widget 属性详情](#14-获取-widget-属性详情)
- [2. 操作 — 模拟用户交互](#2-操作--模拟用户交互)
- [2.1 模拟点击](#21-模拟点击)
- [2.2 模拟滑动](#22-模拟滑动)
- [2.3 模拟文字输入](#23-模拟文字输入)
- [2.4 模拟按键](#24-模拟按键)
- [2.5 组合操作:导航到指定页面](#25-组合操作导航到指定页面)
- [3. 分析 — 诊断性能和问题](#3-分析--诊断性能和问题)
- [3.1 帧率分析Android 层)](#31-帧率分析android-层)
- [3.2 内存分析Android 层)](#32-内存分析android-层)
- [3.3 Dart 堆内存分析VM Service](#33-dart-堆内存分析vm-service)
- [3.4 ANR 检测](#34-anr-检测)
- [3.5 Flutter 日志分析](#35-flutter-日志分析)
- [3.6 GC 分析](#36-gc-分析)
- [4. 修复 — 修改代码并验证](#4-修复--修改代码并验证)
- [4.1 Hot Reload 热重载](#41-hot-reload-热重载)
- [4.2 修复布局溢出](#42-修复布局溢出)
- [4.3 修复内存泄漏](#43-修复内存泄漏)
- [4.4 修复 ANR 问题](#44-修复-anr-问题)
- [4.5 验证修复结果](#45-验证修复结果)
- [5. 完整闭环示例](#5-完整闭环示例)
- [6. VM Service API 速查表](#6-vm-service-api-速查表)
- [7. adb 命令速查表](#7-adb-命令速查表)
- [8. 故障排除](#8-故障排除)
---
## 0. 前置准备
### 0.1 启动应用
```bash
flutter run -d <device_id>
```
### 0.2 获取 VM Service URI
应用启动后,从日志中获取 VM Service 地址:
```bash
adb logcat -d | Select-String "Dart VM service is listening"
```
输出示例:
```
I/flutter: The Dart VM service is listening on http://127.0.0.1:43079/quZORSmVYqk=/
```
### 0.3 设置端口转发
```bash
adb forward tcp:<local_port> tcp:<remote_port>
# 示例:
adb forward tcp:62562 tcp:43079
```
### 0.4 保存关键变量
在后续所有命令中需要用到以下变量:
```powershell
$VM_WS = "ws://127.0.0.1:62562/quZORSmVYqk=/ws" # WebSocket 地址
$ISOLATE_ID = "isolates/6655121260625127" # Isolate ID
$PACKAGE = "apps.xy.xianyan" # 包名
```
### 0.5 WebSocket 通信模板
所有 VM Service 调用都通过 WebSocket 发送 JSON-RPC 请求:
```powershell
function Call-VMService {
param([string]$Method, [hashtable]$Params = @{})
$ws = New-Object System.Net.WebSockets.ClientWebSocket
$cts = New-Object System.Threading.CancellationTokenSource
$uri = [System.Uri]::new($VM_WS)
$ws.ConnectAsync($uri, $cts.Token).Wait()
$body = @{jsonrpc="2.0"; method=$Method; params=$Params; id=[guid]::NewGuid().ToString()} | ConvertTo-Json -Depth 5
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)
$segment = New-Object System.ArraySegment[byte] -ArgumentList @(,$bytes)
$ws.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $cts.Token).Wait()
$buffer = New-Object byte[] 262144
$recvSegment = New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)
$result = $ws.ReceiveAsync($recvSegment, $cts.Token).Result
$response = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $result.Count)
$ws.Dispose()
return $response
}
```
> ✅ **测试结果**: VM Service WebSocket 连接成功,可正常收发 JSON-RPC 消息。
---
## 1. 看 — 观察应用状态
### 1.1 获取 Widget 树
**用途**: 查看当前页面的完整 Widget 层级结构,定位 UI 组件。
**命令**:
```powershell
Call-VMService -Method "ext.flutter.inspector.getRootWidgetTree" -Params @{
isolateId = $ISOLATE_ID
groupName = "debug_group"
withPreferencedSemantics = $false
}
```
**返回示例** (实测):
```json
{
"result": {
"description": "[root]",
"type": "_ElementDiagnosticableTreeNode",
"hasChildren": true,
"children": [
{
"description": "View",
"children": [
{
"description": "RawView",
"children": [
{
"description": "_RawViewInternal",
"children": [
{
"description": "MediaQuery",
"children": [
{
"description": "FocusTraversalGroup",
"children": [
{
"description": "Focus",
"children": [
{
"description": "Navigator",
"children": [
{ "description": "你的业务Widget..." }
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
}
```
**关键字段**:
| 字段 | 说明 |
|------|------|
| `description` | Widget 类型名称 |
| `hasChildren` | 是否有子 Widget |
| `valueId` | Widget 引用 ID用于后续查询属性 |
| `locationId` | 代码位置 ID |
| `creationLocation.file` | 创建该 Widget 的源文件路径 |
| `creationLocation.line` | 创建该 Widget 的行号 |
> ✅ **测试结果**: 成功获取 16KB Widget 树数据,包含完整的 Widget 层级。
### 1.2 截取屏幕截图
#### 方法一adb screencap推荐速度快
```bash
adb shell screencap -p /sdcard/screenshot.png
adb pull /sdcard/screenshot.png ./debug_screenshot.png
```
#### 方法二Android CLI支持 UI 标注)
```bash
android screen capture -a -o=./debug_annotated.png # 带标注
android screen capture -o=./debug_screenshot.png # 不带标注
```
**标注模式说明**: `-a` 参数会在截图中为每个可交互 UI 元素绘制编号边框,配合 `android screen resolve` 可解析坐标。
> ✅ **测试结果**: adb screencap 约 0.03s 完成Android CLI 截屏约 1-2s。两者均成功。
### 1.3 检测布局溢出和错误
**用途**: 实时捕获 Flutter 的布局溢出、RenderFlex overflow 等结构化错误。
**步骤一:启用结构化错误监听**
```powershell
Call-VMService -Method "ext.flutter.inspector.structuredErrors" -Params @{
isolateId = $ISOLATE_ID
enabled = $true
}
```
**步骤二:触发错误后查看**
错误会通过 VM Service 的事件流推送。也可以直接查看 Flutter 日志:
```bash
adb logcat -s flutter | Select-String "overflow|RenderFlex|Exception|Error"
```
**常见溢出错误类型**:
| 错误 | 说明 | 修复方法 |
|------|------|---------|
| `RenderFlex overflowed` | Flex 容器溢出 | 添加 `flexible`/`Expanded`,或 `SingleChildScrollView` |
| `Bottom overflowed by X pixels` | 底部溢出 | 包裹 `SingleChildScrollView` |
| `A RenderFlex overflowed by X pixels` | 水平/垂直溢出 | 调整约束或使用 `overflow: Clip.hardEdge` |
| `Incorrect use of ParentDataWidget` | Widget 父子关系错误 | 检查 Widget 嵌套层级 |
> ✅ **测试结果**: structuredErrors 已成功启用,返回 `{"enabled":"true"}`。
### 1.4 获取 Widget 属性详情
**用途**: 查看指定 Widget 的详细属性(大小、约束、颜色等)。
> ⚠️ **关键发现**: `getDetailsSubtree` 和 `getProperties` 必须在**同一 WebSocket 连接**内调用,
> 且必须使用与 `getRootWidgetTree` 相同的 `groupName/objectGroup`。
> 跨连接调用会返回 `null` 或空数组。
**正确用法(同一连接内)**:
```powershell
# 步骤1建立 WebSocket 连接
$ws = New-Object System.Net.WebSockets.ClientWebSocket
$cts = New-Object System.Threading.CancellationTokenSource
$uri = [System.Uri]::new($VM_WS)
$ws.ConnectAsync($uri, $cts.Token).Wait()
# 步骤2获取 Widget 树(创建对象组)
$msg1 = '{"jsonrpc":"2.0","method":"ext.flutter.inspector.getRootWidgetTree","params":{"isolateId":"'+$ISOLATE_ID+'","groupName":"my_group","withPreferencedSemantics":false},"id":"1"}'
# ... 发送并接收 ...
# 步骤3在同一连接内查询属性使用相同的 groupName/objectGroup
$msg2 = '{"jsonrpc":"2.0","method":"ext.flutter.inspector.getDetailsSubtree","params":{"isolateId":"'+$ISOLATE_ID+'","objectGroup":"my_group","arg0":"inspector-7","subtreeDepth":2},"id":"2"}'
# ... 发送并接收 ...
# 步骤4释放对象组在同一连接内
$msg3 = '{"jsonrpc":"2.0","method":"ext.flutter.inspector.disposeGroup","params":{"isolateId":"'+$ISOLATE_ID+'","objectGroup":"my_group"},"id":"3"}'
# ... 发送并接收 ...
$ws.Dispose()
```
> ✅ **测试结果**: 同一连接内 `getDetailsSubtree` 成功返回子树详情(含 Focus、Semantics、Actions 等子节点)。
> 跨连接调用返回 `null`。
**参数说明**:
| 参数 | 说明 | 注意 |
|------|------|------|
| `groupName` | getRootWidgetTree 使用的组名 | 创建对象组时用 |
| `objectGroup` | 其他 API 使用的组名 | **必须与 groupName 一致** |
| `arg0` | Widget 的 valueId | 如 `inspector-7` |
| `subtreeDepth` | 子树深度 | 建议不超过 5避免数据过大 |
---
## 2. 操作 — 模拟用户交互
### 2.1 模拟点击
```bash
# 基本点击 (x, y 为像素坐标)
adb shell input tap 720 1600
# 点击屏幕中心 (1440x3200 设备)
adb shell input tap 720 1600
# 点击左上角
adb shell input tap 100 200
# 点击右上角
adb shell input tap 1340 200
```
**坐标系**: 原点在左上角x 向右增大y 向下增大。设备分辨率决定最大值。
**获取设备分辨率**:
```bash
adb shell wm size
# 输出: Physical size: 1440x3200
```
> ✅ **测试结果**: 点击 (720, 1600) 成功,截屏确认界面有变化。
### 2.2 模拟滑动
```bash
# 向上滑动 (从下方滑到上方)
adb shell input swipe 720 2400 720 800 500
# x1 y1 x2 y2 持续时间(ms)
# 向下滑动
adb shell input swipe 720 800 720 2400 500
# 向左滑动 (翻页)
adb shell input swipe 1200 1600 200 1600 300
# 向右滑动
adb shell input swipe 200 1600 1200 1600 300
# 慢速滑动 (用于查看滚动效果)
adb shell input swipe 720 2400 720 800 1500
```
**参数说明**:
| 参数 | 说明 | 建议值 |
|------|------|--------|
| x1, y1 | 起始坐标 | - |
| x2, y2 | 终止坐标 | - |
| duration | 持续时间(ms) | 快滑 300正常 500慢滑 1500 |
> ✅ **测试结果**: 向上滑动 (720,2000→720,800, 500ms) 成功,页面滚动可见。
### 2.3 模拟文字输入
```bash
# 输入文字 (需要先点击输入框获取焦点)
adb shell input tap 720 800 # 点击输入框
adb shell input text "hello%sworld" # 输入文字 (%s=空格)
# 清除输入框内容
adb shell input keyevent KEYCODE_CLEAR
# 使用 adb am broadcast 输入中文 (需要安装 ADBKeyboard)
adb shell am broadcast -a ADB_INPUT_TEXT --es msg "你好世界"
```
**特殊字符转义**:
| 字符 | 转义 | 示例 |
|------|------|------|
| 空格 | `%s` | `hello%sworld` |
| `&` | `\\&` | `test\\&more` |
| `<` | `\\<` | `a\\<b` |
| `>` | `\\>` | `a\\>b` |
### 2.4 模拟按键
```bash
# 返回键
adb shell input keyevent KEYCODE_BACK
# Home 键
adb shell input keyevent KEYCODE_HOME
# 最近任务键
adb shell input keyevent KEYCODE_APP_SWITCH
# 回车键
adb shell input keyevent KEYCODE_ENTER
# 删除键
adb shell input keyevent KEYCODE_DEL
# 音量增/减
adb shell input keyevent KEYCODE_VOLUME_UP
adb shell input keyevent KEYCODE_VOLUME_DOWN
```
### 2.5 组合操作:导航到指定页面
```bash
# 示例:点击底部 Tab 导航到"发现"页
adb shell input tap 360 3050 # 点击第2个 Tab
Start-Sleep -Seconds 1
adb shell screencap -p /sdcard/nav_result.png
adb pull /sdcard/nav_result.png ./debug_nav.png
# 示例:滚动列表并点击某个卡片
adb shell input swipe 720 2000 720 800 500 # 滚动
Start-Sleep -Seconds 1
adb shell input tap 720 1200 # 点击卡片
Start-Sleep -Seconds 2
adb shell screencap -p /sdcard/detail.png # 截屏确认
```
---
## 3. 分析 — 诊断性能和问题
### 3.1 帧率分析Android 层)
```bash
# 获取帧率数据
adb shell dumpsys gfxinfo $PACKAGE
# 重置帧率计数器 (开始新的测量)
adb shell dumpsys gfxinfo $PACKAGE reset
# 等待 5 秒后获取新数据
Start-Sleep -Seconds 5
adb shell dumpsys gfxinfo $PACKAGE | Select-String "Janky|percentile|Missed|Slow|deadline|Total frames"
```
**关键指标** (实测数据):
| 指标 | 实测值 | 正常值 | 评价 |
|------|--------|--------|------|
| Total frames rendered | 5 | - | 样本少 |
| **Janky frames** | **40.00%** | <5% | 🔴 严重卡顿 |
| 50th percentile | **750ms** | <16ms | 🔴 极慢 |
| Number Missed Vsync | 1 | 0 | 🟡 有丢帧 |
| Number Slow UI thread | 2 | 0 | 🟡 UI 线程阻塞 |
| Number Frame deadline missed | 2 | 0 | 🔴 帧超时 |
> ⚠️ 注意Flutter 使用自己的渲染管线Android gfxinfo 只能捕获 View 层帧数据。
> Flutter 内部帧率需通过 VM Service 的 `ext.flutter.frames` 获取。
### 3.2 内存分析Android 层)
```bash
adb shell dumpsys meminfo $PACKAGE
```
**关键指标** (实测数据):
| 指标 | 实测值 | 正常值 | 评价 |
|------|--------|--------|------|
| **Total PSS** | **1,335 MB** | 200-400MB | 🔴 极高 |
| Native Heap | 101 MB | 30-60MB | 🔴 偏高 |
| Dalvik Heap | 32 MB | 20-40MB | ✅ 正常 |
| **EGL mtrack (GPU)** | **130 MB** | 30-80MB | 🔴 GPU 纹理泄漏嫌疑 |
| **Unknown** | **803 MB** | <50MB | 🔴 图片缓存未释放嫌疑 |
### 3.3 Dart 堆内存分析VM Service
```powershell
# 获取分配概要 (含 GC)
Call-VMService -Method "getAllocationProfile" -Params @{
isolateId = $ISOLATE_ID
gc = $true
}
```
**返回关键字段** (实测):
```json
{
"memoryUsage": {
"heapUsage": 245066832, // ~234MB Dart 堆使用
"heapCapacity": 266690560, // ~254MB Dart 堆容量
"externalUsage": 28192 // ~28KB 外部内存
}
}
```
**分析步骤**:
1. 记录初始 `heapUsage`
2. 执行操作(如切换页面、滚动列表)
3. 再次获取 `heapUsage`
4. 如果持续增长且不回落 → 内存泄漏
### 3.4 ANR 检测
```bash
# 检查 ANR 文件
adb shell "ls -la /data/anr/" 2>&1 | Select-String "anr_"
# 检查最近的 ANR
adb shell "ls -lt /data/anr/" 2>&1 | Select-Object -First 10
```
**实测发现**: 设备上存在 **6 个正式 ANR + 13 个临时 ANR**,时间集中在 5/27-5/28。
**ANR 常见原因**:
| 原因 | 检测方法 | 修复方向 |
|------|---------|---------|
| 主线程 IO 操作 | 检查同步文件读写 | 改用 `compute()``Isolate` |
| 网络请求阻塞 | 检查同步 HTTP 调用 | 使用 async/await |
| 大量 JSON 解析 | 检查 `jsonDecode` 调用 | 改用 `compute()` 解析 |
| 数据库操作 | 检查同步 DB 查询 | 使用 Drift 的 isolate 模式 |
| 插件初始化慢 | 检查 `initState` 中的同步调用 | 延迟初始化 |
### 3.5 Flutter 日志分析
```bash
# 实时查看 Flutter 日志
adb logcat -s flutter
# 查看最近 200 条
adb logcat -d -t 200 -s flutter
# 过滤错误
adb logcat -d -s flutter | Select-String "Exception|Error|SEVERE|overflow"
# 过滤网络请求
adb logcat -d -s flutter | Select-String "http|api|request|response"
```
### 3.6 GC 分析
通过 VM Service 的 `getAllocationProfile` 获取 GC 信息:
```powershell
Call-VMService -Method "getVM" -Params @{} | ConvertFrom-Json
# 查看 isolates → heaps → new/old 的 collections 和 avgCollectionPeriodMillis
```
**实测 GC 数据**:
| 堆区 | GC 次数 | 平均 GC 间隔 | GC 耗时 |
|------|---------|-------------|---------|
| New (Scavenger) | 1611 次 | 299ms | 3.99s |
| Old (PageSpace) | 68 次 | 7099ms | 0.43s |
> 🟡 New Space GC 间隔 299ms 偏短(正常应 >500ms说明短命对象创建频繁。
---
## 4. 修复 — 修改代码并验证
### 4.1 Hot Reload 热重载
修改代码后,有三种方式触发热重载:
**方法一:在 flutter run 终端按 `r`**(推荐,最可靠)
flutter run 终端会持续运行,直接向其发送 `r` 即可触发热重载。
**方法二:通过 VM Service 触发 `ext.flutter.reassemble`**(推荐,可编程)
```powershell
Call-VMService -Method "ext.flutter.reassemble" -Params @{
isolateId = $ISOLATE_ID
}
```
> ✅ **测试结果**: `ext.flutter.reassemble` 成功,返回 `{"type":"_extensionType","method":"ext.flutter.reassemble"}`。
> 注意:此方法只触发 Widget 重建,**不会重新编译 Dart 代码**。需要先修改文件并保存,
> 再通过 flutter run 终端按 `r` 完成编译+重载,最后用 reassemble 刷新 Inspector 缓存。
**方法三:通过 VM Service 触发 `reloadSources`**(不推荐)
```powershell
Call-VMService -Method "reloadSources" -Params @{
isolateId = $ISOLATE_ID
force = $true
}
```
> ❌ **测试结果**: `reloadSources` 返回 `"Error while starting Kernel isolate task"`。
> 原因Kernel isolate 在 `flutter run` 模式下由终端进程管理VM Service 无法直接调用。
> 此方法仅在 `flutter attach` 或 Dart VM 直接运行时可用。
**最佳实践**:
| 场景 | 推荐方法 |
|------|---------|
| 修改 Dart 代码后热重载 | flutter run 终端按 `r` |
| 修改代码后刷新 Inspector | 先按 `r`,再调 `reassemble` |
| 仅刷新 Widget 树(未改代码) | 调 `reassemble` |
| 完全重启应用 | flutter run 终端按 `R` |
### 4.2 修复布局溢出
**典型问题**: `RenderFlex overflowed by X pixels`
**诊断流程**:
1. 获取 Widget 树 → 定位溢出的 Column/Row
2. 查看 Widget 属性 → 确认约束
3. 修复代码
**修复示例**:
```dart
// ❌ 错误Column 内容超出屏幕
Column(
children: [
Text('Title'),
Text('Content'),
Text('Footer'), // 可能溢出
],
)
// ✅ 修复方案一:包裹 SingleChildScrollView
SingleChildScrollView(
child: Column(
children: [
Text('Title'),
Text('Content'),
Text('Footer'),
],
),
)
// ✅ 修复方案二:使用 Expanded
Column(
children: [
Text('Title'),
Expanded(child: Text('Content')),
Text('Footer'),
],
)
```
### 4.3 修复内存泄漏
**诊断流程**:
1. `getAllocationProfile` → 记录 heapUsage
2. 操作应用(切换页面、滚动)
3. 再次 `getAllocationProfile` → 对比 heapUsage
4. 如果持续增长 → 定位泄漏类
**常见泄漏原因及修复**:
| 泄漏原因 | 修复方法 |
|---------|---------|
| Stream 未取消订阅 | 在 `dispose()` 中调用 `subscription.cancel()` |
| Timer 未取消 | 在 `dispose()` 中调用 `timer.cancel()` |
| Controller 未 dispose | 在 `dispose()` 中调用 `controller.dispose()` |
| 图片缓存过大 | 使用 `CachedNetworkImage` 并设置缓存上限 |
| 全局 List 不断追加 | 定期清理或使用 LRU 缓存 |
### 4.4 修复 ANR 问题
**诊断流程**:
1. 检查 `/data/anr/` 目录确认 ANR
2. 查看 Flutter 日志定位阻塞操作
3. 将同步操作改为异步
**修复示例**:
```dart
// ❌ 错误:主线程同步解析大 JSON
final data = jsonDecode(bigJsonString);
// ✅ 修复:使用 compute 在后台线程解析
final data = await compute(jsonDecode, bigJsonString);
```
### 4.5 验证修复结果
**完整验证流程**:
```bash
# 1. 修改代码后 Hot Reload
# (在 flutter run 终端按 r)
# 2. 截屏确认界面正常
adb shell screencap -p /sdcard/verify.png
adb pull /sdcard/verify.png ./debug_verify.png
# 3. 检查溢出错误
adb logcat -d -s flutter | Select-String "overflow"
# 应无输出
# 4. 检查帧率
adb shell dumpsys gfxinfo $PACKAGE reset
Start-Sleep -Seconds 5
adb shell dumpsys gfxinfo $PACKAGE | Select-String "Janky|percentile"
# Janky frames 应 <5%
# 5. 检查内存
adb shell dumpsys meminfo $PACKAGE | Select-String "TOTAL PSS"
# 应比修复前下降
# 6. 检查 ANR
adb shell "ls -lt /data/anr/" | Select-Object -First 3
# 不应有新的 ANR 文件
```
---
## 5. 完整闭环示例
### 示例:发现并修复首页列表卡顿
```
┌─────────────────────────────────────────────────────────────┐
│ 第一步:看 │
│ │
│ 1. 获取 Widget 树 │
│ → ext.flutter.inspector.getRootWidgetTree │
│ → 发现首页使用 ListView (未使用 builder) │
│ │
│ 2. 截屏确认界面 │
│ → adb screencap → 列表滚动有明显卡顿 │
│ │
│ 3. 检测溢出错误 │
│ → structuredErrors → 发现 2 个 RenderFlex overflow │
├─────────────────────────────────────────────────────────────┤
│ 第二步:操作 │
│ │
│ 4. 模拟滚动列表 │
│ → adb shell input swipe 720 2000 720 800 500 │
│ → 观察到明显掉帧 │
│ │
│ 5. 滚动到底部 │
│ → adb shell input swipe 720 2000 720 800 500 (多次) │
│ → 内存从 400MB 涨到 800MB │
├─────────────────────────────────────────────────────────────┤
│ 第三步:分析 │
│ │
│ 6. 帧率分析 │
│ → dumpsys gfxinfo → Janky frames 40% │
│ │
│ 7. 内存分析 │
│ → dumpsys meminfo → Total PSS 824MB │
│ → getAllocationProfile → heapUsage 234MB 持续增长 │
│ │
│ 8. ANR 检测 │
│ → /data/anr/ → 6 个 ANR 文件 │
│ │
│ 9. 定位根因 │
│ → ListView 未使用 builder一次性创建所有子项 │
│ → 图片未使用缓存,每次滚动重新加载 │
│ → 列表项包含大图,未做缩略图处理 │
├─────────────────────────────────────────────────────────────┤
│ 第四步:修复 │
│ │
│ 10. 修改 ListView → ListView.builder │
│ 11. 添加 CachedNetworkImage │
│ 12. 修复 RenderFlex overflow → 包裹 Expanded │
│ 13. Hot Reload (按 r) │
│ │
│ 14. 验证: │
│ → 截屏确认界面正常 ✅ │
│ → 溢出错误消失 ✅ │
│ → Janky frames 降至 3% ✅ │
│ → 内存稳定在 350MB ✅ │
│ → 无新 ANR ✅ │
└─────────────────────────────────────────────────────────────┘
```
---
## 6. VM Service API 速查表
### Flutter Inspector 扩展
| 方法 | 用途 | 参数 | 实测状态 |
|------|------|------|---------|
| `ext.flutter.inspector.getRootWidgetTree` | 获取 Widget 树 | `isolateId`, `groupName`, `withPreferencedSemantics` | ✅ 成功 |
| `ext.flutter.inspector.getRootWidget` | 获取根 Widget | `isolateId`, `groupName` | ⚠️ 需先 disposeGroup |
| `ext.flutter.inspector.getDetailsSubtree` | 获取子树详情 | `isolateId`, `objectGroup`, `arg0`(valueId), `subtreeDepth` | ✅ 同连接内成功 |
| `ext.flutter.inspector.getProperties` | 获取 Widget 属性 | `isolateId`, `objectGroup`, `arg0`(valueId) | ⚠️ 同连接内返回空 |
| `ext.flutter.inspector.structuredErrors` | 启用/禁用结构化错误 | `isolateId`, `enabled` | ✅ 成功 |
| `ext.flutter.inspector.setPubRootDirectories` | 设置项目根目录 | `isolateId`, `arg0`(路径) | ✅ 成功 |
| `ext.flutter.inspector.disposeGroup` | 释放对象组 | `isolateId`, `objectGroup` | ⚠️ 同连接内成功 |
| `ext.flutter.inspector.getSelectedWidget` | 获取选中 Widget | `isolateId`, `objectGroup`, `previousSelectionId` | ✅ 返回 null(未选中) |
| `ext.flutter.reassemble` | 触发 Widget 重建 | `isolateId` | ✅ 成功 |
> ⚠️ **重要**: Inspector API 的对象组groupName/objectGroup**必须在同一 WebSocket 连接内使用**。
> 跨连接调用会返回 null 或报错。
### VM 核心 API
| 方法 | 用途 | 参数 | 实测状态 |
|------|------|------|---------|
| `getVM` | 获取 VM 信息 | 无 | ✅ 成功 |
| `getIsolate` | 获取 Isolate 详情 | `isolateId` | ✅ 成功 |
| `getAllocationProfile` | 获取内存分配概要 | `isolateId`, `gc` | ✅ 成功 |
| `reloadSources` | Hot Reload | `isolateId`, `force` | ❌ Kernel isolate 错误 |
| `evaluate` | 执行表达式 | `isolateId`, `targetId`, `expression` | ⚠️ 需要正确的 library ID |
| `evaluateInFrame` | 在栈帧中执行表达式 | `isolateId`, `frameIndex`, `expression` | ⚠️ 需要暂停状态 |
| `getObject` | 获取对象详情 | `isolateId`, `objectId` | ✅ 成功 |
| `getStack` | 获取调用栈 | `isolateId` | ✅ 成功 (返回空 frames + messages) |
| `getScripts` | 获取脚本列表 | `isolateId` | ✅ 成功 |
---
## 7. adb 命令速查表
### 设备交互
| 命令 | 用途 |
|------|------|
| `adb shell input tap x y` | 点击 |
| `adb shell input swipe x1 y1 x2 y2 duration` | 滑动 |
| `adb shell input text "hello"` | 输入文字 |
| `adb shell input keyevent KEYCODE_BACK` | 按返回键 |
| `adb shell input keyevent KEYCODE_HOME` | 按 Home 键 |
| `adb shell input keyevent KEYCODE_ENTER` | 按回车键 |
### 截屏和录屏
| 命令 | 用途 |
|------|------|
| `adb shell screencap -p /sdcard/s.png` | 截屏 |
| `adb shell screenrecord /sdcard/video.mp4` | 录屏 (最长 180s) |
### 性能分析
| 命令 | 用途 |
|------|------|
| `adb shell dumpsys gfxinfo <pkg>` | 帧率数据 |
| `adb shell dumpsys gfxinfo <pkg> reset` | 重置帧率计数 |
| `adb shell dumpsys meminfo <pkg>` | 内存数据 |
| `adb shell dumpsys cpuinfo` | CPU 使用率 |
| `adb shell dumpsys battery` | 电池信息 |
### 日志和调试
| 命令 | 用途 |
|------|------|
| `adb logcat -s flutter` | Flutter 日志 |
| `adb logcat -d -t 200` | 最近 200 条日志 |
| `adb shell "ls -la /data/anr/"` | ANR 文件列表 |
| `adb shell dumpsys activity top` | 当前 Activity |
| `adb shell am start -n <pkg>/<activity>` | 启动 Activity |
### 端口转发
| 命令 | 用途 |
|------|------|
| `adb forward tcp:LOCAL tcp:REMOTE` | 端口转发 |
| `adb forward --list` | 查看转发列表 |
| `adb forward --remove tcp:LOCAL` | 移除转发 |
---
## 8. 故障排除
### VM Service 连接失败
| 现象 | 原因 | 解决方法 |
|------|------|---------|
| 403 Forbidden | 未使用认证 token | URL 中必须包含 auth token`/quZORSmVYqk=/` |
| Connection refused | 端口转发未设置 | 执行 `adb forward tcp:LOCAL tcp:REMOTE` |
| WebSocket 握手失败 | URL 路径错误 | 确保路径以 `/ws` 结尾 |
| Inspector API 返回 null | 跨连接使用对象组 | 必须在同一 WebSocket 连接内调用 |
| getSelectedWidget 需要 objectGroup | 参数名错误 | 使用 `objectGroup` 而非 `groupName` |
### uiautomator dump 失败
| 现象 | 原因 | 解决方法 |
|------|------|---------|
| "could not get idle state" | Flutter 持续动画 | 使用 VM Service 的 Inspector API 代替 |
| Permission denied | 非 root 设备 | 使用 VM Service 方案 |
### Hot Reload 失败
| 现象 | 原因 | 解决方法 |
|------|------|---------|
| "Reload not supported" | 修改了非 Dart 文件 | 需要完全重启 (`R`) |
| "Reload rejected" | 修改了全局状态 | 需要完全重启 (`R`) |
| "Error while starting Kernel isolate task" | flutter run 模式下 VM Service 无法调用 reloadSources | 使用 flutter run 终端按 `r` |
| 连接断开 | 设备断开 | 重新 `flutter run` |
### 内存数据异常
| 现象 | 原因 | 解决方法 |
|------|------|---------|
| Unknown 内存过高 | 图片缓存未释放 | 检查 `CachedNetworkImage` 配置 |
| EGL mtrack 过高 | GPU 纹理泄漏 | 检查图片加载和释放逻辑 |
| Native Heap 偏高 | 原生插件泄漏 | 逐个禁用插件排查 |
| Total PSS 超过 1GB | 综合内存问题 | 优先修复 Unknown 和图片缓存 |
### Inspector 对象组问题
| 现象 | 原因 | 解决方法 |
|------|------|---------|
| getDetailsSubtree 返回 null | 跨连接使用对象组 | 在同一 WebSocket 连接内调用 |
| getProperties 返回空数组 | valueId 已过期 | 重新获取 Widget 树 |
| disposeGroup 报错 | 跨连接释放 | 在同一连接内释放 |
| groupName vs objectGroup | 参数名不一致 | getRootWidgetTree 用 `groupName`,其他 API 用 `objectGroup`,值必须一致 |