ADB 能看到设备,Flutter 为什么找不到?flutter_tools 设备解析修复与多 SDK 排查
记录无线 ADB 设备在 adb devices -l 中可见却被 Flutter 丢弃的原因,从 AOSP 列宽格式定位到提交上游 PR
目录
日常用真机调试 Flutter 项目时,遇到过一种让人困惑的情况:终端里运行 adb devices -l 明明能看到手机处于正常的 device 状态,但到了 flutter devices 或 Android Studio 的设备下拉框里,无线连接的手机要么直接消失并提示解析失败(Unexpected failure parsing device information from adb output),要么有线设备虽然列了出来,一运行却报 device not found。
顺着这个问题往下查,定位到了 flutter_tools 对 adb devices -l 输出行的正则边界限制,随后向 Flutter 官方仓库提交了修复 PR(flutter/flutter#189973,已合入主干并标记 cp: stable)。这里把设备发现的三个证据层、ADB 序列号列宽边界、上游正则改法以及本地 FVM 多 SDK 对照验证的细节完整记录下来。
1. 三个独立的证据层
遇到“IDE 里选不到设备”时,如果一开始就重启电脑、重插数据线或者反复开关无线调试,很容易在错误的方向上耗费时间。更清晰的排查顺序是按下面三层逐一确认:
- ADB 连接层:直接运行
adb devices -l。如果列表为空,或者状态是offline、unauthorized,说明问题出在数据线、网络、配对授权或 ADB 本身,此时不需要去动 Flutter 配置。 - Flutter CLI 解析层:通过项目实际绑定的 Flutter SDK 执行
flutter devices -v。如果adb devices -l里设备正常,而 Flutter 提示Unexpected failure parsing device information from adb output或直接忽略了这台手机,问题就落在了flutter_tools(android_device_discovery.dart)对 ADB 文本输出的解析逻辑上。 - IDE 与后台 Daemon 层:如果命令行下的
flutter devices -v能正常识别,唯独 Android Studio 下拉框不一致,就需要把 IDE 当作独立环境看待——重点核对当前项目配置的 Flutter SDK、Dart SDK 以及后台尚未退出的 Flutter Daemon 进程是不是来自同一套 SDK。
特别是本机同时装有全局稳定版 SDK、FVM 缓存的多个版本以及本地源码仓库时,IDE 设置界面里展示的路径未必等同于当前设备列表背后那个 Daemon 实际运行的版本。从项目根目录直接调用软链接下的可执行文件,结果最准确:
& .\.fvm\flutter_sdk\bin\flutter.bat --version
& .\.fvm\flutter_sdk\bin\flutter.bat devices -v
2. adb devices -l 的列宽填充与 mDNS 冲突边界
查看 AOSP 中 ADB 的源码实现(transport.cpp)可以看到,adb devices -l 在长列表模式下使用了 %-22s %s 的格式输出每一行:序列号字段的 22 只是一个最小左对齐宽度,而不是固定列宽。这会导致真实环境里同时出现三种形态各异的输出:
# 1. 无线调试的超长 mDNS 序列号:超过 22 字符最小宽度后,序列号与状态列之间只剩单个普通空格
adb-0123456789abcdef._adb-tls-connect._tcp device product:sample model:Phone device:sample transport_id:1
# 2. 常见的有线 USB 短序列号:不足 22 字符,右侧会被补齐一串空格
USB123456789 device product:sample model:Phone device:sample transport_id:2
# 3. 发生名称冲突的无线 mDNS 序列号:自身内部包含空格(如 " (2)"),且与状态列之间同样只有单个空格
adb-0123456789abcdef (2)._adb-tls-connect._tcp device product:sample model:Phone device:sample transport_id:3
围绕这三种合法输出,如果仅靠空白字符宽度去推断列边界,很容易顾此失彼:
- 在早先的上游修改 flutter/flutter#189369 中,为了保护第 3 种情况里序列号内部的空格(如
(2))不被误切分,解析正则采用了非贪婪匹配,并要求序列号与状态列之间至少有两个空格或制表符((?:\s{2,}|\t+))。结果一旦无线调试序列号长度达到或超过22个字符(状态前只剩单个空格),无论带不带冲突后缀,整行都会匹配失败,触发解析报错并丢弃设备。 - 最初我也考虑过只给“不含空格的序列号”增加一条单空格回退分支(早期关闭的 #189431),但那样仍然覆盖不到第 3 种组合情况——既带有
(2)内部空格、总长度又超过22字符而只以单空格分隔状态列的无线设备。 - 反过来,如果在放宽匹配时把第 2 种有线短序列号右侧的填充空格也原样留在了
deviceID里(变成"USB123456789 "),设备虽然能出现在列表中,后续拼接adb -s执行命令时却会因为尾随空格找不到目标,报device not found或显示为unsupported。
3. 按连接状态贪婪锚定的修复方案(flutter#189973)
要同时兼容上述全部情况,就不能再根据“中间有几个空格”来猜测列边界,而应当改由右侧已知的 ADB 连接状态字段反向确定分界。在 flutter/flutter#189973 中,对 packages/flutter_tools/lib/src/android/android_device_discovery.dart 的核心改动如下:
// 修改前(#189369):依赖两个以上空格或 Tab 分隔,超 22 字符的单空格无线序列号会漏匹配
static final _kDeviceRegex = RegExp(
r'^(.*?)(?:\s{2,}|\t+)'
r'(device|offline|unauthorized|no permissions|bootloader|recovery|sideload|rescue|connecting|authorizing|host|unknown)'
r'(?:\s+(.*)|$)',
);
// ...
final String deviceID = match[1]!;
// 修改后(#189973):显式枚举连接状态确定右边界,左侧贪婪捕获序列号并清理右侧列宽填充
static final _kDeviceRegex = RegExp(
r'^(.*)\s+'
r'(device|offline|unauthorized|no permissions|bootloader|recovery|sideload|rescue|connecting|authorizing|host|unknown)'
r'(?:\s+(.*)|$)',
);
// ...
final String deviceID = match[1]!.trimRight();
改动行数很少,但里面有三个彼此配合的边界细节:
- 第一组必须用贪婪捕获
^(.*)而非非贪婪^(.*?):在允许单空格\s+作为分隔符之后,假设序列号自身恰好包含与连接状态同名的单词(例如冲突后缀device (2),或极端单测样例'my device offline device'),非贪婪^(.*?)会在遇到序列号内部的第一个device或offline时就提前截断;改为贪婪^(.*)后,正则会一直向右匹配到真正的状态列之前,完整保留序列号内部的所有字符与空格。 - 完整覆盖 Flutter Tools 识别的 12 种连接状态:状态组显式列出了含空格的
no permissions以及host、unknown等全部受支持状态,避免末尾的product:... model:...键值对反过来干扰贪婪回溯。 - 提取后仅执行
trimRight():贪婪^(.*)在遇到短 USB 序列号时会把%-22s补齐的右侧空格一并收入第 1 组(最后一个分隔空格归属\s+)。对捕获结果只做trimRight(),就能去掉列宽填充而不影响序列号内部的合法空格。
在对应的 android_device_discovery_test.dart 中一并补充了单空格超长无线序列号、带 (2) 冲突后缀的 12 种连接状态、序列号内含状态同名单词以及有线短序列号填充等回归用例(共 16 项测试全部通过)。
4. 本地构建快照与 FVM 多 SDK 验证细节
在向 Flutter 提交这类 flutter_tools 补丁并在本机配合真机做 A/B 对照验证时,还有几处工具链细节值得留意:
- 删除
bin/cache/flutter_tools.snapshot触发工具链重建:Flutter 启动脚本主要依据 Git Revision 与缓存标记判断工具链快照是否需要重编。如果在未提交状态下直接修改了本地 SDK 或 FVM 缓存里的packages/flutter_tools源码,命令行默认执行的仍是旧的flutter_tools.snapshot。实机复测前先手动删除该快照文件,再运行flutter devices -v观察未修改upstream/master(丢弃无线设备并报错)与打补丁后(完整识别冲突序列号并跑通adb -s ... shell getprop)的真实差异。 - 单测在
packages/flutter_tools子目录下运行:运行设备发现单测时,切入packages/flutter_tools执行flutter test test/general.shard/android/android_device_discovery_test.dart与flutter analyze,比直接从仓库根目录调用更稳妥。 - 手动切换软链接期间避免运行
fvm use:做基准 SDK 与本地补丁 SDK 对照时,若临时手动调整了.fvm/flutter_sdk的指向,中途不要再执行fvm use <version>,否则软链接会被重新覆盖回默认缓存目录;每次切换后也需重启一次 IDE 的 Flutter Daemon。 - 留意
.fvmrc的目录作用域与缓存锁:.fvmrc只约束其所在的目录树;在多包仓库(monorepo)里,如果某个子应用或根目录缺少对应的.fvmrc,执行fvm flutter可能会静默回退到全局默认版本。另外遇到fvm flutter长时间无响应时,可以先排查是否有后台安装任务残留了未清理的cache.git.lock,不要把它和 SDK 接口不兼容混为一谈。 - 临时输出干净的
flutter doctor -v:向上游提交 Issue 或 PR 需要附带doctor -v快照时,若全局PATH里原本排在前面的是另一套稳定版 SDK,不必改动系统环境变量,只在当前 PowerShell 会话里临时前置目标 SDK 的bin目录即可:
$env:PATH = "<flutter-sdk-bin-path>;" + $env:PATH
flutter doctor -v
把连接状态、命令行解析、快照缓存和 SDK 软链接这几件事拆开核对,多数“设备时有时无”的问题都能定位到具体原因。