1. 全局替换tool_center/inspiration为discover模块,统一路由路径 2. 调整AppRoutes路由常量,将discover作为主Tab页,inspiration作为子页面 3. 更新页面注册表与路由配置,修正跳转目标 4. 调整启动页可选配置项,修正路由ID对应关系 5. 新增翻译服务、内容发现、热搜相关工具类与数据模型 6. 修复缓存清理后未刷新统计的问题,调整x86_64架构注释 7. 更新AGENTS.md文档约束规则 8. 新增一批调试用截图资源文件
30 KiB
Flutter AI 全流程闭环调试指南:看 → 操作 → 分析 → 修复
创建时间: 2026-05-28 | 版本: 1.1 | 测试环境: Windows + Android 真机 + Flutter 3.11.5 上次更新: 2026-05-28 | 更新内容: 补充实测结果,修正 API 参数,新增关键发现和注意事项
目录
- 0. 前置准备
- 1. 看 — 观察应用状态
- 2. 操作 — 模拟用户交互
- 3. 分析 — 诊断性能和问题
- 4. 修复 — 修改代码并验证
- 5. 完整闭环示例
- 6. VM Service API 速查表
- 7. adb 命令速查表
- 8. 故障排除
0. 前置准备
0.1 启动应用
flutter run -d <device_id>
0.2 获取 VM Service URI
应用启动后,从日志中获取 VM Service 地址:
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 设置端口转发
adb forward tcp:<local_port> tcp:<remote_port>
# 示例:
adb forward tcp:62562 tcp:43079
0.4 保存关键变量
在后续所有命令中需要用到以下变量:
$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 请求:
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 组件。
命令:
Call-VMService -Method "ext.flutter.inspector.getRootWidgetTree" -Params @{
isolateId = $ISOLATE_ID
groupName = "debug_group"
withPreferencedSemantics = $false
}
返回示例 (实测):
{
"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(推荐,速度快)
adb shell screencap -p /sdcard/screenshot.png
adb pull /sdcard/screenshot.png ./debug_screenshot.png
方法二:Android CLI(支持 UI 标注)
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 等结构化错误。
步骤一:启用结构化错误监听
Call-VMService -Method "ext.flutter.inspector.structuredErrors" -Params @{
isolateId = $ISOLATE_ID
enabled = $true
}
步骤二:触发错误后查看
错误会通过 VM Service 的事件流推送。也可以直接查看 Flutter 日志:
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或空数组。
正确用法(同一连接内):
# 步骤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 模拟点击
# 基本点击 (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 向下增大。设备分辨率决定最大值。
获取设备分辨率:
adb shell wm size
# 输出: Physical size: 1440x3200
✅ 测试结果: 点击 (720, 1600) 成功,截屏确认界面有变化。
2.2 模拟滑动
# 向上滑动 (从下方滑到上方)
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 模拟文字输入
# 输入文字 (需要先点击输入框获取焦点)
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 模拟按键
# 返回键
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 组合操作:导航到指定页面
# 示例:点击底部 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 层)
# 获取帧率数据
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 层)
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)
# 获取分配概要 (含 GC)
Call-VMService -Method "getAllocationProfile" -Params @{
isolateId = $ISOLATE_ID
gc = $true
}
返回关键字段 (实测):
{
"memoryUsage": {
"heapUsage": 245066832, // ~234MB Dart 堆使用
"heapCapacity": 266690560, // ~254MB Dart 堆容量
"externalUsage": 28192 // ~28KB 外部内存
}
}
分析步骤:
- 记录初始
heapUsage - 执行操作(如切换页面、滚动列表)
- 再次获取
heapUsage - 如果持续增长且不回落 → 内存泄漏
3.4 ANR 检测
# 检查 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 日志分析
# 实时查看 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 信息:
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(推荐,可编程)
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(不推荐)
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
诊断流程:
- 获取 Widget 树 → 定位溢出的 Column/Row
- 查看 Widget 属性 → 确认约束
- 修复代码
修复示例:
// ❌ 错误: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 修复内存泄漏
诊断流程:
getAllocationProfile→ 记录 heapUsage- 操作应用(切换页面、滚动)
- 再次
getAllocationProfile→ 对比 heapUsage - 如果持续增长 → 定位泄漏类
常见泄漏原因及修复:
| 泄漏原因 | 修复方法 |
|---|---|
| Stream 未取消订阅 | 在 dispose() 中调用 subscription.cancel() |
| Timer 未取消 | 在 dispose() 中调用 timer.cancel() |
| Controller 未 dispose | 在 dispose() 中调用 controller.dispose() |
| 图片缓存过大 | 使用 CachedNetworkImage 并设置缓存上限 |
| 全局 List 不断追加 | 定期清理或使用 LRU 缓存 |
4.4 修复 ANR 问题
诊断流程:
- 检查
/data/anr/目录确认 ANR - 查看 Flutter 日志定位阻塞操作
- 将同步操作改为异步
修复示例:
// ❌ 错误:主线程同步解析大 JSON
final data = jsonDecode(bigJsonString);
// ✅ 修复:使用 compute 在后台线程解析
final data = await compute(jsonDecode, bigJsonString);
4.5 验证修复结果
完整验证流程:
# 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,值必须一致 |