微调试技术解密(一)——不用电脑,手机也能无线ADB,我们是怎么做到的?
微调试之所以能够摆脱传统电脑的束缚,实现无线ADB调试,得益于@yume-chan/adb和微信TCPSocket这两项核心技术,本文主要介绍微调试小程序直连设备端adbd守护进程的具体实现思路。
01
—
ADB 的三代演进:我们站在了第四代的起点
Android Debug Bridge(ADB)自诞生以来,经历过三代主流形态:
第一代:有线 + 命令行
原理:adb client通过 USB 与adbd通信,中间经过adb server管理多设备。
痛点:必须插数据线、装驱动、敲命令。对非开发者极不友好。
第二代:桌面图形化工具(如 scrcpy、Vysor)
原理:在 PC 上运行一个笨重的GUI,将常见的adb命令封装为可视化的鼠标操作。
痛点:需要下载几十 MB 甚至上百 MB 的安装包(尤其 Electron 应用),电脑仍然是必需品。
第三代:Web 化尝试(基于 WebUSB / WebSocket 桥接)
原理:浏览器通过 WebUSB 直连设备(仍需数据线),或让 PC 跑一个 WebSocket 服务端,转发 ADB 指令。
痛点:要么还在用线,要么依赖一个本地代理服务器,没有彻底摆脱 PC。
我们做的 —— 第四代:纯无线,零依赖,手机即控制台
原理:微信小程序通过wx.createTCPSocket与设备adbd的 5555 端口建立 TCP 连接,直接发送 ADB 协议报文。
效果:不需要数据线、不需要电脑、不需要安装任何 exe、不需要WebSocket 中转。
打开小程序,连上 WiFi,你的手机就成了一台能执行adb shell、install、screencap的无线调试器。
02
—
先搞懂 ADB 的原生架构,才知道我们绕过了什么
ADB 的经典结构分为三层:

ADB Client:你敲命令的那个终端(比如adb devices),其底层调用的是adb.exe这个可执行文件。
ADB Server:PC 上的后台进程(监听localhost:5037),负责管理所有连接的设备,转发 client 的请求。
adbd:Android 设备里的守护进程,真正执行命令。
这种设计在 PC 时代很合理:一个 server 可以同时服务多个 client。
但到了移动端,尤其是小程序这种没有“本地进程”的环境,adb server就成了累赘。
其实,从本质上来讲,只要能让应用程序直接与adbd对话,整个 PC 端的client + server都可以扔掉。而adbd本身就支持 TCP 通信 —— 无线 ADB 开启后,它会在端口5555上监听。
于是问题转化为:应用程序能否建立到设备 IP:5555 的 TCP 连接?
很遗憾,传统的网页 JavaScript 运行在浏览器沙箱中,没有暴露原始 TCP Socket API,无法直接与设备的 5555 端口建立连接。因此,大多数 Web 方案只能借助 WebUSB(仍需插线)或依赖本地 WebSocket 桥接服务(仍离不开 PC)。
而微信小程序从基础库 2.18.0 开始提供了wx.createTCPSocket,恰好填补了这一空白——它允许应用发起纯 TCP 连接,且局域网内不受IP限制。这正是微调试能够直连adbd的关键基础设施。
03
—
从零到一:封装一个 AdbDaemonDirectSocketsDevice
裸 TCP 连接只是第一步,我们还需要处理 ADB协议层的细节。
ADB 的通信不是简单的字符串传输,而是一种二进制报文协议,每个报文必须包含如下字段:

手工拼这些报文既容易出错,又耗时。
我们选择站在巨人的肩膀上—— 使用开源库@yume-chan/adb(代号 Tango)。
Tango 已经把 ADB 协议抽象成了Web Streams:
interface AdbStream {readable: ReadableStream<AdbPacket>;writable: WritableStream<AdbPacket>;}
我们做的,就是给 Tango 接上微信的WxTCPSocket:
export class AdbDaemonDirectSocketsDevice {private host: string;private port: number;private serial: string;constructor(options: AdbDaemonDirectSocketsDeviceOptions) {this.host = options.host;this.port = options.port || 5555;this.serial = `${this.host}:${this.port}`;}getSerial(): string {return this.serial;}async connect() {// 1. 建立原始 TCP 字节流const tcpSocket = new WxTCPSocket(this.host, this.port);const { readable: rawReadable, writable: rawWritable } = await tcpSocket.opened;// 2. 字节流 → AdbPacket 对象流(反序列化)const packetReadable = rawReadable.pipeThrough(new StructDeserializeStream(AdbPacket));// 3. AdbPacket 对象流 → 字节流(序列化)const serializeStream = new AdbPacketSerializeStream();// 4. 将序列化后的字节流(Consumable<Uint8Array>)通过 WrapWritableStream 写入原始 socketconst wrappedRawWritable = new WrapWritableStream(rawWritable);serializeStream.readable.pipeTo(wrappedRawWritable).catch((err) => {console.error("Serialization pipe failed:", err);});// 5. 返回 AdbPacket 对象流return {readable: packetReadable,writable: serializeStream.writable, // WritableStream<AdbPacket>};}}
这个ADBDaemonSocketDevice就是我们无线 ADB 的核心类,不超过 50 行代码。
然后,在小程序中,我们可以这样使用:
const device = new AdbDaemonDirectSocketsDevice({host: '192.168.116.209',port: 5555});let adbInstance = {value: null};// 导出一个Promise,在需要时初始化adb实例const initAdb = async () => {if (adbInstance.value) {return adbInstance.value;}try {const connection = await device.connect();const transport = await adbDaemonAuthenticate({serial: device.getSerial(),connection,credentialManager: new CustomLocalStorageCredentialManager('adb-credentials'),});adbInstance.value = new Adb(transport);return adbInstance.value;} catch (error) {console.error('初始化adb失败:', error);throw error;}};// 导出初始化函数和adb实例module.exports = {initAdb,getAdb: () => adbInstance.value}
后续的所有操作都将基于此处创建的ADB实例对象实现。
04
—
背压(Backpressure):一个容易忽视但至关重要的细节
当设备端的adbd向小程序发送大量数据(比如screencap -p返回几 MB 的 PNG)时,如果小程序消费速度跟不上,缓冲区会爆掉,导致连接断开。
原生socket.onMessage是一个“推”模式,没有背压控制。
而 Web Streams 天然支持背压:当ReadableStream的消费者处理不过来时,它会自动向数据源发出信号,让生产端“慢一点”。
我们在ADBDaemonSocketDevice中把TCPSocket的数据源包装成ReadableStream,然后整个处理链路(Tango 内部的报文解析、业务逻辑)都通过pipeTo串联,背压自动生效,无需额外代码。
这就是用 Web Streams 重构传输层的最大好处。
05
—
隧道之上的轻量扩展:文件管理与虚拟遥控
前文我们打通了无线 ADB 隧道。有了这条直连adbd的通道,上层功能只需调用对应的 ADB 命令即可实现,无需任何额外网络开销。
以文件管理为例,我们使用 ADB 的sync协议,列出设备上的目录和文件:
const sync = await this.adbInstance.sync?.();const entries: any[] = [];const generator = await sync.opendir(path + "/");for await (const entry of generator) {// 此处省略一些简单的处理操作...if (entry.name === "." || entry.name === "..") {console.log("跳过", entry.name, entry.size.toString());continue;}entries.push({...entry,});// 实时更新 UI(流式反馈)this.setData({ files: [...entries] });}this.setData({ files: [...entries] });
多路复用:调用sync方法,会在现有 ADB 会话上派生一个新的逻辑通道,这个通道专门用于文件操作。
流式返回:generator逐条yield条目,并在每次迭代过程中更新UI,确保UI可见渐进渲染。


/*** 通过 ADB 发送按键事件* @param keycode 按键码* @param longpress 是否长按*/async sendAdbKeyEvent(keycode: number, longpress: boolean = false) {if (!this.adbInstance) {wx.showToast({ title: "未连接设备", icon: "error" });return;}const cmd = longpress? `input keyevent --longpress ${keycode}`: `input keyevent ${keycode}`;try {await this.execAdbShell(cmd);// 可选:震动反馈(保留全局震动设置)if (getApp().globalData.isVibrateOn) {wx.vibrateShort();}} catch (err) {console.error("发送按键失败", err);wx.showToast({ title: "按键发送失败", icon: "none" });}}
06
—