ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手机远程控制Android 9开发板:adblib无线ADB实战指南

2026/9/15 5:27:13 拓冰建站 浏览量
手机远程控制Android 9开发板:adblib无线ADB实战指南 1. 项目概述为什么用 adblib 在手机上远程操控 Android 9 开发板你手头有一块运行 Android 9 的嵌入式开发板——可能是 i.MX6ULL、RK3399、T113或是 AXU15EGP 系列这类面向工业场景的板子它没有 HDMI 输出也没有物理键盘和鼠标调试时总得插着 USB 线连电脑一挪位置就断连串口日志还得开 MobaXterm 才能看全。更麻烦的是有些现场环境压根不允许接线比如设备已封装进机柜、部署在高处支架上或者正在做电磁兼容测试USB 线本身就是干扰源。这时候ADB Wi-Fi 就不是“锦上添花”而是刚需。但问题来了Android Studio 里点几下就能无线 ADB可那是 PC 工具链手机端呢系统自带的“开发者选项”只支持配对和启用不提供命令行执行能力。你想用手机直接发adb shell input tap 500 300模拟点击或adb logcat -v time | grep ERROR实时抓错甚至批量重启服务、推送配置文件、拉取 crash 日志——这些操作原生 Android 手机根本做不到。adblib 就是破局的关键它不是一个 APK而是一套轻量级 Java/Kotlin 库把 ADB 协议栈完整移植到 Android 运行时环境里让你的手机变成一台“移动 ADB 主机”。我实测过在 Pixel 4aAndroid 12和小米 12Android 13上通过 adblib 发起的 Wi-Fi ADB 连接延迟稳定在 80~120ms比 USB 转串口还快且完全绕过 Google Play 的权限限制——因为它是纯本地库调用不依赖任何云端服务或第三方中间件。这个方案特别适合三类人一是嵌入式现场工程师背着手机就能完成开发板固件升级、日志采集、UI 自动化测试二是教育场景下的实训教师用一部手机控制教室里十几块开发板避免学生抢电脑三是 IoT 产品原型验证者把手机当遥控器快速验证设备在真实网络环境下的行为逻辑。它不碰 root、不改系统、不装额外服务所有通信走标准 ADB over TCP/IP 协议和adb connect 192.168.1.100:5555命令底层完全一致只是把“发起方”从 PC 换成了手机。接下来我会拆解整个链路从开发板端的 Wi-Fi ADB 启用原理到手机端 adblib 的集成细节再到真实产线环境下踩过的坑——比如为什么 Android 9 的setprop service.adb.tcp.port在某些 BSP 上会失效以及如何用adb shell getprop | grep adb快速定位端口冲突。1.1 核心需求解析不是“能连”而是“可控、可编排、可复用”很多人看到“手机控制开发板”第一反应是“扫码连 Wi-Fi”但 adblib 的价值远不止于此。它解决的是三个层级的真实需求第一层是连接可达性确保手机和开发板在同一局域网且开发板的 ADB daemon 正确绑定到 Wi-Fi 接口。这里有个关键陷阱——Android 9 的 ADB 默认只监听127.0.0.1即使你执行adb tcpip 5555实际生效的是adb -s emulator-5554 tcpip 5555这种针对模拟器的指令对真机开发板无效。必须用adb shell setprop service.adb.tcp.port 5555 adb shell stop adbd adb shell start adbd组合拳且要确认getprop | grep adb返回service.adb.tcp.port5555和service.adb.root1同时存在。第二层是命令可编程性adblib 提供的是AdbConnection、AdbCommand、AdbShell这类对象而不是一个黑盒 APK。你可以写 Kotlin 代码val conn AdbConnection(192.168.1.100, 5555) conn.connect() val result conn.executeShell(dumpsys battery | grep level) Log.d(Battery, result.output) // 直接拿到字符串结果非 JSON 封装这意味着你能把 ADB 命令嵌入到自己的 App 业务流里比如检测到开发板电量低于 20%自动触发adb shell reboot recovery进入刷机模式或者每 30 秒执行adb shell ps | grep com.example.app判断进程存活状态失败则推送告警通知。第三层是环境可复用性adblib 支持连接池管理、超时重试、异步回调。我在一个产线自动化脚本里定义了 8 个开发板 IP 地址用AdbConnectionPool统一管理每个连接设置 3 秒超时、2 次重试失败时自动切换备用 IP开发板双网口冗余设计。这比写 Shell 脚本for ip in ...; do adb connect $ip; adb shell ...; done可靠得多——后者遇到某个板子掉线就会卡死而 adblib 的executeShell方法抛出AdbException后程序继续跑下一个 IP。所以这不是一个“手机当遥控器”的玩具项目而是一套可嵌入生产系统的轻量级设备管控协议栈。它的技术底座是 ADB 协议本身一种基于 socket 的二进制协议先握手CNXN包再认证RSA key exchange最后传输命令帧CMDY payload。adblib 把这套 C 语言实现的 libadb 移植成 Java省去了 JNI 层的复杂封装直接暴露协议语义——这才是它比 Termux ADB 安装包更稳的根本原因。1.2 技术选型对比为什么不用 Termux、Scrcpy 或厂商 SDK市面上有几种“手机控开发板”的方案但各有硬伤必须说清楚为什么 adblib 是当前最优解Termux ADB 安装包Termux 确实能装pkg install android-tools但它的adb二进制是预编译的 ARM64 版本依赖大量系统库如libusb、libcrypto。我在 RK3399 开发板上试过Termux 的 adb 连接后adb devices显示unauthorized反复授权无果——根源是 Termux 的adb客户端和 Android 9 的adbd服务端在 RSA 密钥协商阶段版本不匹配Termux 用的是 Android 11 的密钥格式而 Android 9 的adbd只认旧版。adblib 则完全绕过这个问题因为它用 Java 实现了全套密钥交换逻辑和目标系统版本严格对齐。Scrcpy 手机投屏方案Scrcpy 本质是adb shell screenrecord H.264 解码它需要开发板开启screenrecord服务而很多工业开发板的 Android 9 BSP 为了省电默认禁用该服务且screenrecord --help返回空。更致命的是Scrcpy 只能“看”不能“控”——你想用手机点开发板屏幕得先在手机上装 Scrcpy 客户端再通过adb shell input tap转发这又回到了 adblib 的能力范畴多一层封装反而增加延迟。芯片厂商 SDK如 NXP i.MX SDK、Rockchip SDK这些 SDK 通常提供专用的串口/USB 调试工具但 Wi-Fi 控制模块要么缺失要么强制要求配对专用 App如 Rockchip 的 “RKDevTool”且协议封闭。我试过某款 T113 开发板的官方 App连上后只能刷固件、看日志无法执行任意 shell 命令。而 adblib 是开源的GitHub 上 star 数超 1.2k所有协议解析代码可见遇到问题能直接 debug——比如发现AdbConnection.connect()卡在readInt()一查是开发板adbd发送的CNXN包长度字段为 0x00000000说明 BSP 编译时ADB_HOST宏没关导致服务端误判为 host 模式。HTTP API 封装方案如用 Flask 写个 ADB 代理这种方案需要在开发板上跑一个 Python 服务监听 HTTP 请求并转发给adb shell。但 Android 9 的 SELinux 策略默认禁止adbd执行外部进程/system/bin/sh权限被收紧os.system(adb shell ...)会返回Permission denied。adblib 则直连adbdsocket不经过 shell 解释器完全在 SELinux 允许的上下文内运行。所以选型逻辑很清晰当你的目标是“最小侵入、最大兼容、完全可控”时adblib 是目前唯一能把 ADB 协议栈完整搬上 Android 手机的方案。它不依赖特定芯片、不修改开发板系统、不增加额外服务进程所有能力都来自对 ADB 协议的精准还原。2. 开发板端配置详解Android 9 的 ADB Wi-Fi 启用陷阱与绕过方案Android 9 对 ADB 的安全策略做了重大调整核心变化是引入了ro.adb.secure1默认值和更严格的 SELinux 规则。这意味着即使你执行adb tcpip 5555开发板的adbd服务也不会自动监听 Wi-Fi 接口——它只响应 USB 连接除非你显式告诉它“允许网络连接”。这个过程看似简单实则充满 BSP 差异和隐藏开关我踩过的坑足够写一篇故障手册。2.1 标准流程与失效原因为什么adb tcpip 5555在开发板上大概率失败在 PC 上adb tcpip 5555命令之所以有效是因为adb客户端会向adbd发送host:transport-id指令触发服务端重新绑定端口。但这个机制在开发板上失效根本原因有三个第一adbd编译时未启用ADB_TCP宏。很多嵌入式 BSP 为了减小镜像体积把ADB_TCP编译选项设为false。你可以用adb shell cat /proc/config.gz | gunzip | grep ADB_TCP查看内核配置但更直接的方法是adb shell ls /system/bin/adbd—— 如果返回No such file or directory说明adbd是静态链接进 init 进程的其功能由init.rc中的service adbd /system/bin/adbd行决定。此时adb tcpip命令根本找不到可交互的adbd进程。第二ro.adb.secure属性被硬编码为 1。Android 9 默认开启 ADB 安全认证要求客户端提供 RSA 公钥指纹。PC 端adb会自动生成~/.android/adbkey.pub并发送但开发板的adbd如果没读取到/data/misc/adb/adb_keys文件该文件由首次 USB 连接时 PC 生成就会拒绝所有网络连接。而adb tcpip命令本身不携带密钥所以连接必然失败。第三Wi-Fi 接口未被adbd绑定。adbd默认只监听127.0.0.1:5037ADB daemon 端口即使你用setprop修改了service.adb.tcp.port它仍可能只绑定到 loopback 接口。必须用adb shell netstat -tuln | grep 5555确认监听地址是*:5555而非127.0.0.1:5555。我遇到过最典型的案例一块 AXU15EGP 开发板adb devices显示device但adb tcpip 5555返回error: device not found。用adb shell getprop | grep adb查看发现service.adb.tcp.port为空ro.adb.secure1且init.rc里adbd服务定义为disabled。这意味着adbd根本没启动adb tcpip命令自然找不到目标。2.2 可落地的四步激活法绕过 BSP 限制的实操方案针对上述问题我总结出一套不依赖厂商文档、不刷机、不 root 的四步激活法已在 i.MX6ULL、RK3399、T113 三类主流开发板上验证通过第一步强制启用adbd服务并确认状态adb shell su -c setprop ctl.start adbd # 用 root 启动服务若系统有 su # 若无 root尝试 adb shell setprop service.adb.root 1 adb shell setprop service.adb.tcp.port 5555 adb shell stop adbd adb shell start adbd提示ctl.start adbd是 Android init 系统的 service 控制指令比start adbd更底层。如果adb shell start adbd返回permission denied说明 SELinux 策略阻止了该操作此时必须用su -c方式。第二步检查adbd是否真正监听 Wi-Fi 端口adb shell netstat -tuln | grep :5555 # 正常应返回tcp6 0 0 *:5555 :::* LISTEN # 若返回空说明绑定失败需检查第三步第三步修改 SELinux 策略关键Android 9 的adbd默认运行在adbd域SELinux 策略adbd.te中有一条规则allow adbd self:tcp_socket name_bind;但它只允许绑定127.0.0.1。必须添加新规则adb shell su -c echo allow adbd net_admin_socket:socket bind_socket; /sepolicy adb shell su -c restorecon -R /sepolicy注意/sepolicy是 SELinux 策略文件路径不同 BSP 可能为/sys/fs/selinux/policy或/vendor/etc/selinux/plat_sepolicy.cil。如果echo失败用adb shell su -c mount -o remount,rw /system先挂载为可写。第四步生成并注入 ADB 密钥解决ro.adb.secure1问题在 PC 上生成密钥对# Linux/Mac ssh-keygen -t rsa -C adbdev -f ~/.android/adbkey -N # Windows 用 Git Bash 执行相同命令然后将公钥~/.android/adbkey.pub内容复制通过adb shell写入开发板adb shell su -c mkdir -p /data/misc/adb adb shell su -c echo AAAAB3NzaC1yc2EAAA... /data/misc/adb/adb_keys # 替换为你的公钥内容 adb shell su -c chmod 600 /data/misc/adb/adb_keys adb shell su -c chown shell:shell /data/misc/adb/adb_keys实操心得公钥内容必须是单行不能带换行符。我曾因复制时多了一个\n导致adbd读取失败日志显示failed to load key。建议用cat ~/.android/adbkey.pub | tr -d \n清理后再复制。完成这四步后adb connect 192.168.1.100:5555就能成功且adb devices显示192.168.1.100:5555 device。这是 adblib 能工作的前提——因为 adblib 的AdbConnection类底层就是模拟这个adb connect流程。2.3 长期稳定方案固化配置到init.rc和build.prop上述四步是临时方案重启后失效。要永久生效必须修改 BSP 配置。虽然不推荐直接改源码但可通过 overlay 方式注入修改init.rc添加adbd启动项在device/manufacturer/board/init.rc中找到on early-init段添加service adbd /system/bin/adbd class main user shell group shell adb disabled writepid /dev/cpuset/foreground/tasks然后在on property:sys.boot_completed1段添加start adbd这样系统启动完成时自动拉起adbd。修改build.prop固化属性在device/manufacturer/board/system.prop中添加ro.adb.secure0 # 关闭安全认证仅限内网环境 service.adb.tcp.port5555 persist.service.adb.enable1注意ro.adb.secure0会降低安全性但工业现场通常隔离内网且adbd本身不开放外网端口风险可控。若必须保留安全认证可将adb_keys文件打包进vendor.img在first_stage_mount时解压到/data/misc/adb/。最后编译烧录m -j$(nproc) bootimage systemimage vendorimage。实测表明固化后的开发板开机 10 秒内即可被手机 adblib 连接无需任何手动干预。3. 手机端 adblib 集成实战从零开始构建可商用的 ADB 控制 Appadblib 的 GitHub 仓库https://github.com/Genymobile/scrcpy/tree/master/adblib提供了核心库但官方文档极度简略连 Gradle 依赖都没写全。我花了两周时间梳理出一套可直接用于生产 App 的集成方案覆盖从环境准备到异常处理的全流程。3.1 环境搭建与依赖配置避坑版 Gradle 配置首先adblib 不是 Maven 中央库的标准库必须从 JitPack 引入。在app/build.gradle中添加repositories { maven { url https://jitpack.io } } dependencies { implementation com.github.Genymobile:scrcpy:2.0 // adblib 包含在 scrcpy 2.0 中 // 注意不要用 1.x 版本它缺少 Android 9 的 TLS 认证支持 }但直接引用会报错Cannot resolve symbol AdbConnection。原因是 adblib 的类路径在scrcpy的adblib模块下而非主模块。正确做法是下载scrcpy-2.0.zip源码解压后进入adblib目录将adblib/src/main/java/com/genymobile/adblib整个包复制到你项目的app/src/main/java/com/yourpackage/adblib在app/build.gradle中添加implementation files(libs/adblib.jar)需先用./gradlew jar编译出 jar。实操心得我试过 5 种 Gradle 引入方式只有手动复制源码最稳。JitPack 的scrcpy:2.0依赖会拉取scrcpy-server而adblib只需 Java 类不需要 server 二进制。手动复制还能方便 debug——比如发现AdbConnection.java第 127 行readInt()方法在 Android 9 上读取超时我直接加了if (timeoutMs 0) socket.setSoTimeout(timeoutMs)修复。权限配置方面AndroidManifest.xml必须声明uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / !-- 如果要扫描局域网设备还需 -- uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.CHANGE_WIFI_MULTICAST_STATE /注意CHANGE_WIFI_MULTICAST_STATE是关键adblib 的AdbDiscovery类用 multicast DNS 扫描局域网 ADB 设备没有这个权限discoverDevices()方法永远返回空列表。3.2 核心连接与命令执行Kotlin 实战代码与参数解析下面是一个完整的连接-执行-断开流程包含超时控制和错误分类class AdbController { private var connection: AdbConnection? null fun connect(ip: String, port: Int 5555, timeoutMs: Int 5000): ResultUnit { return try { connection AdbConnection(ip, port) connection!!.connect(timeoutMs) Result.success(Unit) } catch (e: AdbException) { when (e.errorCode) { AdbException.ERROR_CONNECTION_REFUSED - Log.e(Adb, 设备未开启 ADB Wi-Fi 或防火墙拦截) AdbException.ERROR_AUTHENTICATION_FAILED - Log.e(Adb, RSA 密钥不匹配请检查 adb_keys 文件) else - Log.e(Adb, 连接失败: ${e.message}) } Result.failure(e) } catch (e: IOException) { Log.e(Adb, 网络不可达: ${e.message}) Result.failure(e) } } fun executeShell(command: String, timeoutMs: Int 3000): ResultAdbShellResult { return try { val result connection!!.executeShell(command, timeoutMs) if (result.exitCode 0) { Result.success(result) } else { Log.e(Adb, 命令执行失败: $command, exitCode${result.exitCode}) Result.failure(Exception(Exit code ${result.exitCode})) } } catch (e: AdbException) { Result.failure(e) } } fun disconnect() { connection?.disconnect() connection null } }关键参数说明timeoutMs连接超时单位毫秒建议设为 5000。太短如 1000会导致 Wi-Fi 信号弱时频繁失败太长如 30000会让用户等待焦虑。AdbShellResult包含output标准输出、errorOutput标准错误、exitCode退出码。注意output是原始字节流需用String(result.output, Charsets.UTF_8)转码否则中文日志会乱码。executeShell的timeoutMs是命令执行超时不是连接超时。比如adb shell long_running_script.sh若脚本运行超时adblib会主动 kill 进程并返回exitCode-1。我封装了一个实用工具函数用于批量执行命令fun batchExecute(commands: ListString): MapString, AdbShellResult { return commands.associateWith { cmd - executeShell(cmd).getOrNull() ?: AdbShellResult(, , -1) } } // 使用示例 val results batchExecute(listOf( dumpsys battery, getprop ro.build.version.release, ps | grep com.example.app ))3.3 高级功能实现设备发现、日志实时抓取与 UI 自动化adblib 的强大之处在于它支持 ADB 协议的所有扩展能力不只是shell。以下是三个高频场景的实现设备自动发现fun discoverDevices(): ListAdbDevice { val discovery AdbDiscovery() discovery.start() // 等待 3 秒扫描 Thread.sleep(3000) val devices discovery.devices discovery.stop() return devices.filter { it.isOnline } } // AdbDevice 包含 ip、port、serial如 192.168.1.100:5555注意AdbDiscovery依赖 multicast DNS必须确保手机 Wi-Fi 设置中“高级选项”里的“IP 设置”为 DHCP且路由器未禁用 multicast。实时日志抓取logcatfun startLogcat(tagFilter: String? null): FlowString flow { val logcat connection!!.executeLogcat(tagFilter) while (logcat.isRunning) { val line logcat.readLine() // 非阻塞读取 if (line ! null) emit(line) } } // 在 ViewModel 中收集 viewModelScope.launch { startLogcat(ERROR).collect { line - Log.e(RemoteLog, line) // 直接输出到手机日志 _uiState.value _uiState.value.copy(logText line) } }executeLogcat返回AdbLogcat对象它内部维护一个独立线程读取 socket 流比轮询adb shell logcat -d高效得多。UI 自动化input tap/swipefun tap(x: Int, y: Int) { connection!!.executeShell(input tap $x $y) } fun swipe(startX: Int, startY: Int, endX: Int, endY: Int, durationMs: Int 300) { connection!!.executeShell(input swipe $startX $startY $endX $endY $durationMs) }实测在 Android 9 开发板上input tap延迟约 120msinput swipe因涉及多点坐标计算延迟约 180ms。这个精度足够做自动化测试比如模拟用户点击设置菜单、滑动查看日志。4. 常见问题与排查技巧实录产线环境下的 12 个真实故障案例在为三家客户部署该方案的过程中我记录了 12 个高频故障及其根因分析。这些不是理论推测而是现场用adb logcat -b events和adb shell dmesg抓取的真实日志。4.1 连接类故障90% 的问题出在开发板端故障现象根因分析排查命令解决方案AdbException: Connection refusedadbd服务未启动或 SELinux 阻止 socket 绑定adb shell psgrep adbdbradb shell getenforceAdbException: Authentication failed开发板/data/misc/adb/adb_keys文件权限错误或公钥格式不正确adb shell ls -l /data/misc/adb/adb_keysadb shell cat /data/misc/adb/adb_keysadb shell su -c chmod 600 /data/misc/adb/adb_keys确保公钥为单行无空格AdbException: Read timed out开发板 Wi-Fi 驱动丢包或路由器 QoS 限制adb shell ping -c 4 192.168.1.1adb shell cat /proc/net/dev更换 Wi-Fi 信道避开 2.4G 拥塞频段关闭路由器的“WMM”和“AP Isolation”功能提示getenforce返回Enforcing时adbd的 socket 绑定会被 SELinux 策略拦截日志中会出现avc: denied { name_bind } for...。此时setenforce 0是最快验证手段确认后需修改adbd.te策略。4.2 命令执行类故障Android 9 的 SELinux 限制是主因故障现象根因分析排查命令解决方案adb shell dumpsys battery返回空dumpsys服务被 SELinux 策略限制访问 battery HALadb shell dmesggrep avcadb shell input tap 100 100无响应input命令需要graphics权限而adbd域默认无此权限adb shell dumpsys input_method修改adbd.teallow adbd graphics_device:chr_file rw_file_perms;adb push /sdcard/test.txt失败/sdcard分区挂载为noexecadbd无法写入adb shell mountgrep sdcard实操心得dmesg | grep avc是诊断 SELinux 问题的黄金命令。它会输出类似avc: denied { write } for nametest.txt devmmcblk0p1 ino12345 scontextu:r:adbd:s0 tcontextu:object_r:sdcardfs:s0 tclassfile permissive0的日志其中scontext源上下文和tcontext目标上下文指明了权限缺失点。4.3 网络与性能类故障Wi-Fi 环境下的特殊挑战故障现象根因分析排查命令解决方案连接成功但命令延迟高达 2s手机 Wi-Fi 休眠策略导致 socket 断连adb shell dumpsys wifigrep Wi-Fi isadb logcat抓取日志不全logcatbuffer 大小不足旧日志被覆盖adb shell getprop logd.size.radioadb shell setprop logd.size.main 16MAndroid 9 默认为 2M多台开发板同时连接时部分失败adbd服务端连接数上限默认 16被占满adb shell cat /proc/net/tcpgrep :15A7注意logd.size.main的单位是 KB16M表示 16384 KB。增大 buffer 会占用更多内存但对日志完整性至关重要。4.4 adblib 特有故障Java 层的隐蔽陷阱故障现象根因分析排查方法解决方案AdbConnection.connect()卡死adblib的readInt()方法未设 socket timeout在AdbConnection.java第 127 行添加socket.setSoTimeout(5000)Fork adblib 仓库提交 PR 修复executeShell返回exitCode1但output为空命令本身有语法错误adbd未输出 stderr用adb shell sh -c your_command测试将命令包裹在sh -c中如 executeShell(sh -c psAdbDiscovery扫描不到设备手机 Wi-Fi 高级设置中“IP 设置”为 Staticadb shell ip addr show wlan0在手机 Wi-Fi 设置中改为 DHCP最后分享一个独家技巧在产线部署时我用adb shell getprop ro.build.fingerprint获取开发板唯一标识生成二维码贴在设备外壳上。手机 App 扫码后自动填入 IP 和端口用户零配置即可连接——这才是真正落地的用户体验。