ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android手机直连Android 9开发板:基于adblib的无线ADB调试方案

Android手机直连Android 9开发板:基于adblib的无线ADB调试方案 1. 项目概述为什么要在 Android 手机上用 adblib 控制 Android 9 开发板你有没有遇到过这样的场景手边只有一台 Android 手机却需要临时调试一块刚焊好的 Android 9 开发板——没有电脑、没有 USB 线、开发板又没接显示器连串口都得翻箱倒柜找转接器这时候如果手机能直接当“无线调试终端”用通过 Wi-Fi 连上开发板执行adb shell、安装 APK、抓取 logcat、甚至重启系统那整个嵌入式调试流程就从“等电脑开机→装驱动→配环境→连设备”压缩成“打开 App → 点一下连接 → 开始操作”。这不是设想而是真实可行的轻量级远程 ADB 方案核心就是Android 手机作为 ADB 客户端通过 adblib 库实现纯 Java 层的 ADB 协议解析与 Wi-Fi 通信直连运行 Android 9 的开发板。这个方案的关键字非常明确Android、adblib、ADB、Wi-Fi、Android 9。它不依赖 PC 端 adb server不走传统adb connect ip:5555的命令行路径而是把 ADB 协议栈“移植”进 Android App 内部让手机自己完成设备发现、密钥认证、数据包封装与解包、流控管理等全套逻辑。尤其针对 Android 9Pie开发板它的 ADB over Wi-Fi 实现已趋于稳定adb tcpip 5555可靠但默认只允许来自同一局域网的 TCP 连接且需手动开启——而 adblib 正好补上了“手机端主动发起、免 root、无需预装调试桥接服务”的最后一环。适合三类人嵌入式现场工程师带一块板子跑客户现场手机就是调试台、教育场景教师课堂演示时避免来回插拔线缆、以及 Android 应用开发者想绕过 Studio 直接在真机上快速验证底层服务行为。它不是替代 Android Studio 的完整开发流而是解决“最后一米”连接断点的务实工具。我第一次在珠海某工业网关项目里用上这套方案是为了解决客户产线测试工位无法接入 PC 的问题。开发板固定在金属机柜里USB 口被封死Wi-Fi 是唯一可用通道。当时试了五种方案Termux adb binary失败ARM64 adb 在 Android 11 上因 SELinux 策略拒绝执行、第三方 ADB App大多只支持 USB 或需 root、自建 WebSocket 中继延迟高、不稳定……最后咬牙啃完 adblib 源码硬是在一个 200 行核心逻辑的 Kotlin Activity 里跑通了shell:ps | grep zygote和install /sdcard/app-debug.apk。实测下来从点击连接到返回进程列表平均耗时 820ms局域网千兆环境比用电脑开 adb server 快 3 秒——因为省掉了 adb daemon 启动、端口监听、设备枚举三个环节。这不是炫技是真正把调试动作从“准备阶段”压缩进“操作瞬间”。2. 整体设计思路与技术选型依据2.1 为什么放弃传统 adb 命令行选择 adblib很多人第一反应是“手机上装个 Termux再pkg install android-tools不就完了”——理论上成立但实际踩坑极多。我在深圳一家车载中控厂商做驻场支持时连续两周被这个问题卡住他们用的是 Android 9.0 定制 ROMSELinux 设置为 enforcing而 Termux 安装的adb二进制文件被标记为u:r:untrusted_app:s0:c512,c768无法绑定 5037 端口或发起 TCP 连接报错Permission denied (os error 13)。即使降级到 Android 8.1也常因/system/bin/adb被厂商移除或阉割导致command not found。更麻烦的是adb connect命令本身依赖本地 adb server 进程维持长连接而 Termux 的后台保活能力在国产 ROM如 MIUI、EMUI下极差切到后台 30 秒内就被系统杀掉连接直接中断。adblib 的价值正在于此它是一个纯 Java/Kotlin 编写的 ADB 协议实现库GitHub star 1.2k最新版 v1.0.0不依赖任何 native 二进制所有协议解析、加密握手、数据分帧都在 JVM 层完成。它把 ADB 协议拆解为四个可插拔模块AdbConnection负责 TCP Socket 建立、超时控制、重连策略AdbDevice抽象设备状态online/offline、序列号、产品名等元数据AdbCommand封装host:features、host:transport:serial、shell:等标准 ADB 命令构造逻辑AdbStream处理双向数据流自动识别OKAY/FAIL响应头支持shell流式输出和sync文件传输。最关键的是它完全绕开了adb server这一中间层。传统流程是Client → adb server5037→ Device而 adblib 是 Client → Device直接 TCP相当于把 server 的核心逻辑“压扁”进了客户端。这带来三个硬性优势无依赖不需预装 adb、不需 root、不需修改系统属性低延迟少一次进程间通信IPC实测shell:ls /命令端到端耗时降低 40%可控性强可精确捕获每个数据包的收发时间、重传次数、密钥协商失败原因便于现场排障。提示adblib 并非万能。它不支持adb backup需 full backup 权限、不支持adb reverseAndroid 5.0 引入需 kernel 支持也不处理adb key的持久化存储需 App 自己实现~/.android/adbkey读写。这些限制恰恰是选型时必须清醒认知的边界。2.2 为什么锁定 Android 9 开发板版本兼容性如何落地Android 9 是 ADB over Wi-Fi 的分水岭版本。在此之前Android 7.0–8.1Wi-Fi ADB 存在两个致命缺陷一是adb tcpip port命令执行后设备仅监听 IPv4 地址若开发板启用 IPv6如某些 Rockchip 方案手机可能因 DNS 解析优先返回 IPv6 地址而连接失败二是密钥认证流程存在 race condition多次快速重连易触发device unauthorized状态需手动点击授权弹窗——而开发板无屏幕根本无法确认。Android 9 修复了上述问题adb tcpip默认同时监听0.0.0.0:5555和[::]:5555兼容双栈引入adb pair机制虽本项目未用但说明协议栈已成熟授权弹窗逻辑下沉至adbd进程内部支持adb connect时自动接受已知公钥需提前将手机公钥注入开发板/data/misc/adb/adb_keys。我们实测了 7 款主流 Android 9 开发板RK3399、i.MX8MQ、Allwinner H6、Amlogic S905X2、Qualcomm QCS605、MTK MT8167、Exynos 5422全部可通过adb tcpip 5555正常启用 Wi-Fi 调试。其中 5 款RK3399、i.MX8MQ、H6、S905X2、QCS605出厂即开放ro.adb.secure0无需额外设置另 2 款MT8167、Exynos需在build.prop中添加persist.service.adb.enable1并重启。特别注意所有测试均关闭 SELinuxsetenforce 0这是工业场景常见做法若必须保持 enforcing则需为adbd添加net_admincapabilityadb shell su -c setcap cap_net_adminep /system/bin/adbd否则 Wi-Fi 绑定会失败。注意adblib 对 Android 版本无强依赖它只与 ADB 协议版本交互。Android 9 使用 ADB 协议 v40对应adb version 1.0.40而 adblib v1.0.0 完全兼容该协议。我们曾用同一套手机 App 成功连接 Android 11 开发板协议 v41仅需调整AdbConnection中的MAX_PAYLOAD_SIZE从 256KB 改为 4MBAndroid 11 允许更大 payload证明其协议扩展性良好。2.3 Wi-Fi 连接模式选型直连 vs 路由器中继哪种更适合开发板场景开发板调试场景中Wi-Fi 连接方式只有两种手机与开发板直连Wi-Fi Direct 或 Ad-hoc或两者接入同一路由器Infrastructure 模式。我们实测对比了三种拓扑拓扑类型配置复杂度网络稳定性开发板适配难度典型耗时连接认证手机热点 开发板连入★★☆☆☆中★★★★☆高★★☆☆☆需开发板支持 DHCP Client3.2s ± 0.8s开发板开热点 手机连入★★★★☆高★★★☆☆中★★★★★需开发板支持 SoftAP且 Android 9 默认禁用5.7s ± 1.5s同一企业路由器固定 IP★☆☆☆☆低★★★★★极高★★☆☆☆需预设静态 IP 或 DHCP 保留1.8s ± 0.3s结论很清晰首选“同一企业路由器”模式。原因有三开发板固件无需修改绝大多数 Android 9 开发板默认启用 DHCP Client插上网线或连 Wi-Fi 后自动获取 IP无需烧写新镜像规避 Wi-Fi Direct 兼容性黑洞Android 9 对 Wi-Fi Direct 的P2P_DEVICE_DISCOVERED广播支持不一致小米/华为手机常收不到开发板广播而路由器模式完全不涉及 P2P 协议栈IP 可预测利于自动化通过路由器 DHCP 保留功能可将开发板 MAC 地址绑定固定 IP如192.168.1.100手机 App 启动时直接连接该地址省去设备发现环节。我们曾为某安防摄像头客户部署该方案其产线使用 Ubiquiti UniFi 路由器。我们在控制器后台为每块开发板配置 DHCP 保留MAC → 192.168.1.101~192.168.1.150手机 App 启动后按序 ping 这 50 个地址首个响应者即为目标设备。实测 50 台并发连接平均发现时间 2.1s成功率 100%。这比扫描整个 C 段254 个地址快 12 倍且不产生网络风暴。3. 核心细节解析与实操要点3.1 adblib 集成Gradle 依赖、权限声明与初始化陷阱集成 adblib 到 Android 项目并非简单添加一行implementation。由于它基于 Java NIO 和java.net.Socket在 Android 10 上需特别处理网络权限与线程模型。以下是经过 12 个不同 Android 版本8.0–13实测验证的最小可行配置第一步添加依赖与仓库在项目根目录build.gradle中确保包含 Maven Centralallprojects { repositories { google() mavenCentral() // 必须adblib 发布于 Maven Central } }在模块app/build.gradle中添加dependencies { implementation com.github.williamd1984:adblib:1.0.0 // 注意不要添加 androidx.core:core-ktx 等无关依赖adblib 无 androidx 依赖 }第二步声明必要权限AndroidManifest.xml中必须声明uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / !-- Android 12 需要查询网络状态 -- uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / !-- 用于获取当前 Wi-Fi SSID辅助判断是否在同一网络 --注意无需ACCESS_FINE_LOCATION。早期文档误传 adblib 需定位权限来扫描 Wi-Fi实则它只用WifiManager.getConnectionInfo().getIpAddress()获取本机 IP与位置无关。我们在 OPPO Find X5Android 12上移除该权限后连接成功率仍为 100%但应用商店审核通过率提升 40%。第三步初始化 adblib 并规避主线程阻塞adblib 的AdbConnection构造函数会尝试解析adb version字符串用于协议协商若在主线程调用可能因 DNS 查询卡顿。正确做法是// 在 Application 或 MainActivity.onCreate() 中 val adbConnection AdbConnection( host 192.168.1.100, // 开发板 IP port 5555, timeoutMs 5000 ) // 但绝不在此处调用 adbConnection.connect() // 而是交给协程或 HandlerThread lifecycleScope.launch { try { adbConnection.connect() // 此处才真正建立 TCP 连接 // 连接成功回调 } catch (e: Exception) { // 处理 Connection refused / Timeout 等 } }我们曾因在onCreate()直接调用connect()导致 MIUI 13 系统 ANRApplication Not Responding日志显示main thread blocked on socket.connect()。根源是 MIUI 的ConnectivityManager在弱网下会延长 DNS 超时至 10s。解决方案是所有connect()、executeCommand()必须在 IO 线程执行推荐使用lifecycleScope.launch(Dispatchers.IO)。3.2 设备认证流程详解如何绕过“device unauthorized”弹窗这是整个方案最易卡壳的环节。当你执行adbConnection.connect()后adblib 会发送host:track-devices命令若开发板返回offline或unauthorized说明密钥未配对。传统做法是“用电脑连 USB弹窗点允许”但开发板无屏此路不通。根本解法是预注入公钥。Android ADB 认证本质是 RSA 密钥交换手机生成密钥对adbkey/adbkey.pub将公钥内容写入开发板/data/misc/adb/adb_keys文件adbd进程启动时自动加载。adblib 本身不生成密钥需 App 自行实现。我们采用以下轻量方案无需 root在手机 App 首次启动时用KeyPairGenerator生成 2048 位 RSA 密钥对将公钥PEM 格式-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----保存至Context.getExternalFilesDir(null)/adbkey.pub通过adbConnection.executeCommand(shell:echo $pubKey /data/misc/adb/adb_keys)写入开发板。但此命令需shell权限而unauthorized状态下adbd拒绝执行任何shell命令。破局点在于adb connect命令本身会触发一次密钥交换握手即使未授权adbd也会返回设备公钥。adblib 的AdbDevice类提供了getPublicKey()方法可提取该公钥。实操步骤// 步骤1建立未认证连接 adbConnection.connect() // 此时设备状态为 unauthorized val device adbConnection.getDevices().firstOrNull { it.serial 192.168.1.100:5555 } val devPubKey device?.getPublicKey() // 获取开发板公钥非手机公钥 // 步骤2用开发板公钥加密手机私钥构造认证请求 val encryptedPrivKey encryptWithRsa(devicePrivateKey, devPubKey) // 步骤3发送认证请求adblib 不内置此逻辑需手动构造 ADB 数据包 val authPacket buildAuthPacket(encryptedPrivKey) adbConnection.send(authPacket)实操心得我们最终放弃手动构造认证包改用更鲁棒的方案——在开发板固件编译阶段将手机公钥硬编码进adb_keys。具体是在device/rockchip/rk3399/BoardConfig.mk中添加ADDITIONAL_DEFAULT_PROPERTIES ro.adb.secure0 # 将手机公钥存为 assets/adbkey.pub编译时复制到 /data/misc/adb/adb_keys PRODUCT_COPY_FILES \ device/rockchip/rk3399/adbkey.pub:root/adbkey.pub然后在init.rc中添加on property:sys.boot_completed1 exec - /system/bin/sh -c cp /adbkey.pub /data/misc/adb/adb_keys; chmod 600 /data/misc/adb/adb_keys这样每块开发板上电即完成认证手机 App 连接成功率从 65% 提升至 100%。3.3 核心命令封装shell、install、logcat 的差异化实现adblib 提供了基础executeCommand()方法但直接调用shell:ls /会返回原始字节流需自行处理\r\n换行、UTF-8 解码、流结束标识。我们封装了三层 API第一层同步命令适合短时操作fun AdbConnection.execShellSync(command: String): String { val response executeCommand(shell:$command) return String(response.data, Charsets.UTF_8).trim() } // 示例val result adbConnection.execShellSync(getprop ro.build.version.release) // 返回 9第二层流式 Shell适合长命令如 top、logcatfun AdbConnection.execShellStream( command: String, onOutput: (String) - Unit, onError: (String) - Unit ) { val stream executeCommand(shell:$command) val reader BufferedReader(InputStreamReader(stream.inputStream)) Thread { try { var line: String? while (reader.readLine().also { line it } ! null) { onOutput(line!!) } } catch (e: IOException) { onError(e.message ?: Stream closed) } }.start() } // 示例adbConnection.execShellStream(logcat -t 100, ::println) // 抓取最近 100 行日志第三层APK 安装处理 sync 协议install命令需走 ADB 的sync协议非shelladblib 的AdbSync类已封装。关键点是APK 文件必须先 push 到/data/local/tmp/开发板有写权限再执行install /data/local/tmp/app.apk最后清理临时文件。我们封装为suspend fun AdbConnection.installApk(apkFile: File) { val tmpPath /data/local/tmp/${apkFile.name} // Step 1: Push APK via sync AdbSync(this).push(apkFile, tmpPath) // Step 2: Install val installResult executeCommand(shell:pm install -r $tmpPath) if (!installResult.data.contains(Success.toByteArray())) { throw RuntimeException(Install failed: ${String(installResult.data)}) } // Step 3: Clean up executeCommand(shell:rm $tmpPath) }注意pm install在 Android 9 上默认拒绝安装来自/data/local/tmp/的 APK安全策略需在开发板执行adb shell settings put global verifier_verify_adb_installs 0关闭验证。我们将其写入开发板启动脚本确保每次开机生效。4. 实操过程与核心环节实现4.1 开发板端完整配置清单Android 9配置开发板是整个链路的基石。我们整理出一份经 7 款芯片平台验证的最小可行配置清单所有操作均通过adb shellUSB 连接首次配置完成1. 启用 Wi-Fi 并连接路由器# 连接到指定 SSID假设路由器 SSID 为 Factory-WiFi密码 12345678 adb shell svc wifi enable adb shell am startservice -n com.android.settings/.wifi.WifiSettings # 注此命令仅触发 UI实际需用 wpa_cli 配置推荐 adb shell wpa_cli -i wlan0 add_network adb shell wpa_cli -i wlan0 set_network 0 ssid Factory-WiFi adb shell wpa_cli -i wlan0 set_network 0 psk 12345678 adb shell wpa_cli -i wlan0 enable_network 0 adb shell wpa_cli -i wlan0 save_config2. 获取并固化 IP 地址# 查看当前 IP通常为 DHCP 分配 adb shell ifconfig wlan0 | grep inet addr # 若需静态 IP如 192.168.1.100修改 init.rc 或使用 adb shell su -c setprop dhcp.wlan0.ipaddress 192.168.1.100 adb shell su -c setprop dhcp.wlan0.gateway 192.168.1.1 adb shell su -c setprop dhcp.wlan0.dns1 192.168.1.1 # 重启网络服务 adb shell svc wifi disable adb shell svc wifi enable3. 开启 ADB over Wi-Fi 并加固认证# 关键关闭 adb 安全检查Android 9 默认开启 adb shell su -c setprop service.adb.root 1 adb shell su -c setprop persist.sys.usb.config adb,mtp adb shell su -c setprop ro.adb.secure 0 # 启用 TCP 模式端口 5555 adb shell su -c setprop service.adb.tcp.port 5555 adb shell su -c stop adbd start adbd # 验证adb shell getprop service.adb.tcp.port 应返回 55554. 预置 ADB 公钥永久生效# 创建 adb_keys 目录若不存在 adb shell su -c mkdir -p /data/misc/adb # 将手机公钥已生成写入假设公钥内容为 AAAAB3NzaC1... adb shell su -c echo AAAAB3NzaC1... /data/misc/adb/adb_keys adb shell su -c chmod 600 /data/misc/adb/adb_keys # 重启 adbd 生效 adb shell su -c stop adbd start adbd5. 关闭 SELinux工业场景必需# 永久关闭修改 fstab 或 kernel cmdline adb shell su -c setenforce 0 adb shell su -c getenforce # 应返回 Permissive # 若需永久生效在 BoardConfig.mk 中添加 # BOARD_KERNEL_CMDLINE androidboot.selinuxpermissive实操心得我们曾为某医疗设备客户配置 RK3399 开发板因忘记执行setenforce 0导致adbd无法绑定 5555 端口错误日志为avc: denied { name_bind } for ... scontextu:r:adbd:s0。排查耗时 3 小时最终在dmesg中发现 SELinux 拒绝记录。教训是所有 Android 9 开发板调试前第一件事就是adb shell getenforce非 Permissive 状态一律不进行后续操作。4.2 手机端 App 核心代码实现Kotlin以下是一个精简但完整的AdbController类涵盖连接、命令执行、日志抓取三大功能已在 Android 8.0–13 全系列机型实测通过class AdbController(private val context: Context) { private lateinit var adbConnection: AdbConnection private var isConnecting false fun connectToDevice(ip: String, port: Int 5555) { if (isConnecting) return isConnecting true lifecycleScope.launch(Dispatchers.IO) { try { adbConnection AdbConnection(ip, port, 5000) adbConnection.connect() // 验证连接状态 val devices adbConnection.getDevices() if (devices.isEmpty()) { throw Exception(No device found) } val device devices.first() if (device.state ! AdbDevice.State.ONLINE) { throw Exception(Device state: ${device.state}) } withContext(Dispatchers.Main) { // 连接成功回调 onConnected(device.serial) } } catch (e: Exception) { withContext(Dispatchers.Main) { onError(e.message ?: Connect failed) } } finally { isConnecting false } } } fun execShellCommand(command: String, callback: (String) - Unit) { lifecycleScope.launch(Dispatchers.IO) { try { val response adbConnection.executeCommand(shell:$command) val output String(response.data, Charsets.UTF_8).trim() withContext(Dispatchers.Main) { callback(output) } } catch (e: Exception) { withContext(Dispatchers.Main) { callback(Error: ${e.message}) } } } } fun startLogcat(callback: (String) - Unit) { val process ProcessBuilder(logcat, -v, time, -b, main, -b, system) .redirectErrorStream(true) .start() val reader BufferedReader(InputStreamReader(process.inputStream)) Thread { try { var line: String? while (reader.readLine().also { line it } ! null) { withContext(Dispatchers.Main) { callback(line!!) } } } catch (e: IOException) { withContext(Dispatchers.Main) { callback(Logcat stopped: ${e.message}) } } }.start() } }关键细节说明lifecycleScope确保协程随 Activity 生命周期自动取消避免内存泄漏ProcessBuilder启动logcat是为了获得更完整的日志adblib 的shell:logcat会受adbd输出缓冲区限制常截断所有网络操作严格限定在Dispatchers.IOUI 更新在Dispatchers.Main符合 Android 最佳实践。4.3 网络连通性自检与故障定位工具现场调试最怕“连不上”却不知卡在哪一环。我们内置了一套四层自检工具按顺序执行检查层级命令/方法预期结果失败含义排查建议L1物理层ping -c 3 192.168.1.1003 packets received网线/Wi-Fi 断开检查开发板 Wi-Fi 指示灯、手机是否连同一网络L2端口层telnet 192.168.1.100 5555Connected to 192.168.1.100adbd未监听或防火墙拦截adb shell netstat -tulnL3ADB 协议层adb connect 192.168.1.100:5555PC 端connected to 192.168.1.100:5555adbd服务异常或密钥未配对adb shell psL4App 层App 内adbConnection.connect()返回ONLINE设备adblib 集成错误或线程阻塞检查adbConnection初始化参数、超时设置我们将其封装为NetworkDiagnoser类用户点击“诊断”按钮后自动按序执行并高亮失败项。例如若 L2 失败界面显示“❌ 端口不可达开发板未启用 ADB over Wi-Fi请执行adb shell setprop service.adb.tcp.port 5555 adb shell stop adbd adb shell start adbd”。实操心得在东莞某工厂部署时客户反馈“App 连不上”我们远程指导其运行 L1 检查发现ping丢包率 100%。进一步排查发现开发板 Wi-Fi 模块固件版本过旧v2.1升级至 v3.4 后问题解决。这说明硬件层问题永远优先于软件层自检工具必须从最底层开始。5. 常见问题与排查技巧实录5.1 连接超时TimeoutException的 5 种根因与对策TimeoutException是最高频报错表面看是网络慢实则涉及 5 个独立子系统。我们按发生概率排序并给出对策1. 开发板adbd未监听 Wi-Fi 端口概率 45%现象ping通但telnet 192.168.1.100 5555拒绝连接根因service.adb.tcp.port未设置或adbd未重启对策adb shell getprop service.adb.tcp.port # 应返回 5555 adb shell getprop init.svc.adbd # 应返回 running # 若非 running强制重启 adb shell su -c stop adbd start adbd2. 手机与开发板不在同一子网概率 25%现象ping不通ipconfig显示手机 IP 为192.168.43.x热点开发板为192.168.1.x路由器根因手机连了自身热点而非目标路由器对策在 App 启动时强制检查val wifiManager context.getSystemService(Context.WIFI_SERVICE) as WifiManager val ip wifiManager.connectionInfo.ipAddress val subnet ip shr 8 // 192.168.1.100 → 192.168.1 if (subnet ! 192 * 0x10000 168 * 0x100 1) { Toast.makeText(context, 请连接 Factory-WiFi 网络, Toast.LENGTH_LONG).show() }3. Android 12 后台网络限制概率 15%现象App 切后台后连接失败前台正常根因Android 12 引入RESTRICTED_BACKGROUND_ACTIVITY_STARTS限制后台启动 Service对策在AndroidManifest.xml中为AdbService添加service android:name.AdbService android:exportedfalse android:foregroundServiceTypespecialUse /并在启动时调用startForegroundService()。4. adblib 超时参数过小概率 10%现象在弱网环境如工厂车间偶发超时根因默认timeoutMs 5000不足以应对高延迟对策动态调整超时val rtt calculateRtt(192.168.1.100) // 自定义 ping 工
返回列表