# 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 ``` ### 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: tcp: # 示例: 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\\` | `\\>` | `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 ` | 帧率数据 | | `adb shell dumpsys gfxinfo reset` | 重置帧率计数 | | `adb shell dumpsys meminfo ` | 内存数据 | | `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 /` | 启动 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`,值必须一致 |