1. 当两台开发板撞了序列号ADB 到底在纠结什么批量采购开发板的朋友大概率都遇到过这种离谱情况两块板子插上电脑adb devices一敲列表里躺着两行一模一样的序列号后面跟着两个device状态。你想给其中一块推个文件、抓个日志结果命令发出去鬼知道进了哪块板子的肚子。更邪门的是有时候一块板子重启另一块跟着掉线仿佛它们共享了同一条命。这个问题的根子在于ADB 默认用硬件序列号ro.serialno / USB iSerial作为设备的唯一标识。当两块板子的序列号完全一致时ADB 的传输层transport就懵了——它分不清哪个 transport 对应哪块物理设备。注意这里说的不是ADB 不支持多设备而是ADB 不支持多设备共用同一个身份标识。这是两个完全不同的概念。我最早踩这个坑是在一个批量烧录的项目里产线上同时插了 8 块同型号开发板序列号全是出厂默认的0123456789ABCDEF。当时用adb -s 0123456789ABCDEF shell想指定设备结果 ADB 直接报more than one device连随机选一个都不给你。后来才搞明白-s参数匹配的是序列号当序列号不唯一时这个参数就废了。那正确的解法是什么核心思路有两条要么让每块板子的序列号变得唯一要么绕开序列号用 ADB 内部真正唯一的标识——transport_id 来指定设备。前者是治本后者是治标但更快。实际工作中两条路我都会用具体选哪条取决于你是在产线批量操作还是临时调试。这篇文章会从 ADB 的设备识别机制讲起把 transport_id 的用法彻底拆开再给出修改序列号的完整方案最后分享几个我在实际项目里踩过的坑和验证过的技巧。不管你是刚接触 ADB 的新手还是被这个问题折磨过的老手应该都能找到能直接抄作业的东西。2. ADB 设备识别机制序列号、transport_id 与连接链路2.1 序列号从哪来为什么它会重复Android 设备的序列号来源分几个层次。最底层是 USB 描述符里的 iSerialNumber 字段这是 USB 协议规定的由设备固件写入。再往上是 Android 系统属性ro.serialno通常在 init 阶段从 USB 描述符或存储分区读取。最后 ADB 在建立连接时会通过CNXN握手包把序列号报给 host 端的 adb server。开发板厂商在量产时如果没做序列号个性化烧录所有板子就会共用同一个默认值。这在成本敏感的板子上极其常见——写序列号需要额外的烧录工序和产线工具很多厂商直接省了。所以你拿到手的板子序列号重复是常态不是故障。提示用adb shell getprop ro.serialno可以看系统属性里的序列号用lsusb -v在 Linux 下能看到 USB 描述符里的 iSerialNumber两者可能不一致排查时要分清。2.2 transport_id 是什么它凭什么能区分设备每次 ADB 与一个设备建立连接adb server 内部会创建一个 transport 对象并分配一个自增的整数 ID这就是 transport_id。关键点在于transport_id 是 adb server 在运行时分配的跟设备硬件无关每次重新插拔或重启 adb server 都可能变化。但它在当前会话内是唯一的哪怕两块板子序列号一模一样它们的 transport_id 也必然不同。你可以用adb devices -l看到更详细的信息但 transport_id 默认不显示。要看到它得用adb devices -l输出大概长这样List of devices attached 0123456789ABCDEF device product:xxx model:xxx transport_id:1 0123456789ABCDEF device product:xxx model:xxx transport_id:2看到没序列号一样但 transport_id 一个是 1 一个是 2。这就是我们的突破口。2.3 为什么-s参数在这种情况下会失效adb -s serial的工作方式是adb client 把序列号发给 adb serverserver 在 transport 列表里查找匹配的序列号。如果找到多个匹配项server 无法决定用哪个就会报错。这不是 bug是设计上的歧义——序列号本应是唯一的重复了就是异常情况。而adb -t transport_id是直接按 transport_id 查找transport_id 在 server 内部是唯一的所以不会有歧义。这就是为什么解决序列号冲突transport_id 是最直接的方案。指定方式参数唯一性来源序列号重复时是否可用序列号-s设备硬件否会报 more than one devicetransport_id-tadb server 运行时分配是USB 端口环境变量/udev物理端口是但配置复杂3. 用 transport_id 精准操作指定开发板3.1 获取 transport_id 的正确姿势很多人只知道adb devices但这个命令默认不显示 transport_id。你需要加-l参数adb devices -l如果输出里没有 transport_id说明你的 ADB 版本较老。transport_id 是在 platform-tools 24.0.0 之后引入的建议升级到最新版 platform-tools。升级后如果还不显示试试adb devices -l还是不行的话用adb -s serial get-state配合adb server-status也能间接推断但不如直接升级来得痛快。拿到 transport_id 后所有原本用-s的命令都可以换成-t# 原本的写法序列号重复时会失败 adb -s 0123456789ABCDEF shell # 改用 transport_id adb -t 1 shell adb -t 2 shell3.2 批量操作时如何动态获取 transport_id产线上板子多手动看 transport_id 不现实。我通常写个小脚本把序列号和 transport_id 的对应关系抓出来然后按需操作。下面这个 bash 脚本可以直接用#!/bin/bash # 获取所有设备的 transport_id 列表 adb devices -l | grep -oP transport_id:\K[0-9] | while read tid; do echo Transport ID: $tid # 对每个 transport 执行操作比如获取序列号 adb -t $tid shell getprop ro.serialno done如果你用的是 Python可以用subprocess调用 adb解析输出。核心逻辑是一样的先枚举 transport_id再逐个操作。注意transport_id 在 adb server 重启后会重新分配。如果你在脚本里缓存了 transport_id记得在 server 重启后重新获取。我吃过这个亏脚本跑了一半 server 崩了后面全乱套。3.3 用 transport_id 做端口转发和文件推送端口转发是开发板调试的高频操作序列号重复时同样会出问题。用 transport_id 可以这样写# 把 transport_id 为 1 的设备的 5555 端口转发到本地 5555 adb -t 1 forward tcp:5555 tcp:5555 # 推送文件到 transport_id 为 2 的设备 adb -t 2 push local_file.apk /data/local/tmp/这里有个细节forward命令的端口映射是跟 transport 绑定的如果你用-s指定了重复的序列号forward 可能建到错误的设备上。用-t就没这个问题。3.4 一个容易忽略的坑adb server 重启后 transport_id 变化这是我在实际项目里踩得最狠的一个坑。当时写了个自动化测试脚本启动时获取了 transport_id然后跑了一小时的测试。中途 adb server 因为某个设备异常断连自动重启了transport_id 全部重新分配脚本后面所有的操作都打到了错误的设备上测试结果全废。解决办法有两个一是在脚本里每次操作前都重新获取 transport_id二是用adb wait-for-device配合序列号USB端口的方式做更稳定的绑定。前者简单但效率低后者复杂但稳定。我的建议是如果操作间隔超过 30 秒就重新获取一次 transport_id。4. 从根上解决让每块开发板的序列号变得唯一4.1 修改序列号的几种途径及适用场景transport_id 是治标改序列号才是治本。修改序列号的方法取决于你的开发板平台高通平台通过 fastboot 或 diag 模式写入需要厂商工具或特定分区操作MTK 平台用 SN Writer 工具在 meta 模式下写入全志/瑞芯微通常改ro.serialno属性或写特定分区通用 Android 设备如果 root 了可以改/system/build.prop或/vendor/build.prop里的ro.serialno但这是只读属性需要重新打包或使用 magisk 模块对于开发板最常见的是改ro.serialno。但注意ro.开头的属性是只读的运行时改不了必须在系统镜像层面改。4.2 通过 build.prop 修改序列号的实操步骤假设你有一块 root 过的开发板想批量改序列号。步骤如下拉取当前 build.propadb pull /system/build.prop ./build.prop修改ro.serialno的值比如改成BOARD001ro.serialnoBOARD001推回去并重启adb remount adb push ./build.prop /system/build.prop adb shell chmod 644 /system/build.prop adb reboot重启后adb devices就会显示新的序列号。但这个方法有个问题每块板子都要单独改批量操作时效率低。更好的做法是在烧录镜像时就写入不同的序列号这需要改烧录工具或使用产线工具。4.3 用 magisk 模块动态修改序列号适合已 root 设备如果不想动系统镜像可以用 magisk 模块在启动时重置属性。写一个简单的模块# module.prop idserialchanger nameSerial Changer version1.0# post-fs-data.sh #!/system/bin/sh resetprop ro.serialno BOARD001把模块打包刷入重启后序列号就变了。这个方法的优点是灵活缺点是每块板子还是要单独刷不同的模块。批量场景下可以结合 MAC 地址或其他唯一标识在脚本里动态生成序列号。4.4 修改序列号后的验证与注意事项改完序列号用以下命令验证adb devices -l adb shell getprop ro.serialno确保两处显示一致。不一致的话说明 USB 描述符里的序列号和系统属性不同步ADB 可能仍然用 USB 描述符里的值。注意修改序列号可能影响某些依赖序列号的应用如授权、DRM。在开发板上通常没问题但如果是量产设备要评估合规性。另外改序列号后建议清一次 adb server 的缓存adb kill-server adb start-server。5. 实战排查序列号冲突引发的连锁问题5.1 问题现象一块板子掉线另一块跟着消失这是我遇到的最诡异的现象。两块序列号相同的板子同时连接拔掉其中一块另一块在adb devices里也消失了。原因是 adb server 把两个 transport 当成了同一个设备的两个连接拔掉一个触发了 transport 清理逻辑把另一个也带走了。排查过程先用adb devices -l确认序列号重复再用adb -t id get-state逐个测试。如果拔掉一块后另一块的 transport_id 也消失基本可以确认是序列号冲突导致的 transport 混淆。解决方案立即改用 transport_id 操作或者改序列号。临时应急的话可以只插一块板子操作完事再换另一块但这在产线上不可接受。5.2 问题现象adb install 装到了错误的设备批量安装 APK 时用adb -s serial install指定设备结果 APK 装到了另一块板子上。这是因为-s匹配到了多个设备adb 可能随机选了一个或者选了第一个匹配的。排查用adb -t id install替代并在安装后验证包名和版本号。我通常会在安装后跑一句adb -t id shell pm list packages | grep package确认。5.3 问题现象adb logcat 抓到了另一块板子的日志抓日志时序列号冲突同样会导致抓错设备。更麻烦的是logcat 是流式的你可能抓了半天才发现抓错了。用 transport_id 可以避免adb -t 1 logcat -v time device1.log adb -t 2 logcat -v time device2.log 这样两个设备的日志分别落到不同文件不会混。5.4 排查工具与命令速查表场景命令说明查看设备列表adb devices -l显示序列号和 transport_id指定设备执行adb -t id shell用 transport_id 精准操作重启 adb serveradb kill-server adb start-server清理 transport 缓存查看序列号属性adb shell getprop ro.serialno确认系统属性查看 USB 序列号lsusb -vLinux 下查看 USB 描述符批量获取 transport_idadb devices -l | grep -oP transport_id:\K[0-9]脚本化处理6. 产线批量场景下的稳定方案与经验总结6.1 产线批量烧录时的序列号管理策略产线上几十块板子同时操作靠 transport_id 手动指定不现实。我的做法是在烧录阶段就写入唯一序列号。具体来说烧录工具支持从配置文件读取序列号每块板子烧录时传入不同的值。这样烧录完成后每块板子的序列号天然唯一后续 ADB 操作直接用-s就行。如果烧录工具不支持退而求其次烧录完成后用脚本批量改序列号。脚本逻辑是枚举所有 transport_id对每个 transport 执行改序列号操作序列号用前缀序号生成。改完后重启 adb server再验证。6.2 用 udev 规则绑定 USB 端口Linux 下的终极方案在 Linux 下可以通过 udev 规则给每个 USB 端口分配固定的设备节点然后 ADB 通过端口来区分设备。这个方法最稳定但配置最复杂。核心思路是根据 USB 物理端口路径如1-1.2创建符号链接ADB 通过符号链接操作。配置示例/etc/udev/rules.d/51-android.rulesSUBSYSTEMusb, ATTR{idVendor}XXXX, ATTR{idProduct}XXXX, SYMLINKandroid_%p然后通过/dev/android_1-1.2这样的路径来区分。不过 ADB 本身不直接支持按 USB 路径指定设备需要配合adb -d或其他工具。这个方法我用得不多因为配置和维护成本高但在极端场景下是有效的。6.3 我踩过的三个坑和对应的解法坑一transport_id 在 server 重启后变化。解法脚本里每次操作前重新获取或者用adb wait-for-device配合序列号做等待。坑二改序列号后 USB 描述符没变。解法改完系统属性后还要确保 USB 描述符里的 iSerialNumber 也变了。有些平台需要改内核或 USB gadget 配置这个比较底层建议查平台文档。坑三多块板子同时操作时 adb server 负载高。解法减少并发操作数或者用adb -t id逐个操作避免 server 同时处理太多 transport。我实测下来同时操作 8 块板子时 server 还算稳定超过 16 块就明显卡顿。6.4 一个实用的批量操作脚本模板最后分享一个我常用的批量操作脚本可以直接改改用#!/bin/bash # 批量对所有设备执行命令 # 用法./batch_adb.sh shell getprop ro.serialno CMD$1 adb devices -l | grep -oP transport_id:\K[0-9] | while read tid; do echo Transport ID: $tid adb -t $tid $CMD echo done这个脚本会遍历所有 transport对每个设备执行你传入的命令。比如批量抓序列号、批量安装 APK、批量推文件都能用。注意命令里的引号处理复杂命令建议写成单独脚本再调用。实际用下来transport_id 方案在临时调试和中小批量场景下完全够用改序列号方案适合产线大批量。两者结合基本能覆盖所有序列号冲突的场景。如果你还遇到其他诡异现象欢迎一起交流我踩过的坑可能比文档里写的还多。