前言
由于开发需要,下了最新版的 Android Studio(Dolphin)
,发现在选择 Android
设备的时候多了一个 Pair Devices Using WI-FI
,然后这里可以使用二维码和配对码,我使用了二维码进行配对,发现挺方便的,可以不用数据线了。
本文会分析一下实现的过程。
原始的无线连接
做 Android
开发的应该都知道,可以通过 adb connect ip:port
来使用无线连接。
不过这种方式需要先通过 USB
连接成功后,然后再使用 adb tcpip port
来指定一个端口,然后才能通过上面的指令进行连接。
这使用起来就非常麻烦,还要有一根数据线,不然还没法操作,有时候没有数据线还真是没办法。
所以当我发现新版的 Android Studio
有二维码连接之后就来了好奇心,这是怎做到的?
二维码连接的原理
由于 Android Studio
开源了它的代码,所以可以进行研究。
我们这就直接开始研究。
从二维码入手
首先在通过二维码连接的时候, Android Studio
会生成一张二维码的图片,我们只要在手机的设置中找到开发者选项,找到无线调试,点进去然后开启无线调试,这时候使用二维码配对设备就会可以使用了,然后扫描 Android Studio
中的二维码,稍等片刻手机就连上了。
WIFI
连接的入口在设备列表中

这里是怎么做到的,我们从二维码入手。
我们知道二维码里面其实存的就是一些字符,我们可以使用微信扫描一下,微信无法识别二维码的意义会把二维码的内容显示出来,我们就需要看看二维码里面到底存了什么。
我把二维码放到下方,感兴趣可以扫一下

扫出来的结果如下
1
|
WIFI:T:ADB;S:studio-R*RsW2zVC@;P:y8G^#OcZg*z0;;
|
虽然看不懂这些字符表达的是什么意思,但是还是能发现一些规律的。为了找出规律,我把二维码的页面关掉,重新生成一个二维码,扫描得出的结果如下
1
|
WIFI:T:ADB;S:studio-0d6^I(ncs1;P:*pj3cLQLzt<V;;
|
这下有一些不同了,我们大概能猜到哪些不同,哪些一样了,放到一起对比一下
1
2
|
WIFI:T:ADB;S:studio-R*RsW2zVC@;P:y8G^#OcZg*z0;;
WIFI:T:ADB;S:studio-0d6^I(ncs1;P:*pj3cLQLzt<V;;
|
可以发现不变的是 WIFI:T:ADB;S:studio-
看来是有固定的前缀,到这里就不太好猜接下来的字符表达的是什么意思,不过像这种没有意义的字符,很可能是随机生成的字符串,没多大的意义,主要是不能重复且随机。
从代码入手
经过二维码的分析,我们知道了二维码里面的字符是有一个固定的前缀,但是我们并不知道后面的规律是什么,所以我们还是需要从源码中看下能不能找出规律,代码主要在 WiFiPairingServiceImpl.kt - Android Code Search
我们直接看生成二维码的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@UiThread
class WiFiPairingServiceImpl(
private val randomProvider: RandomProvider,
private val adbService: AdbServiceWrapper,
taskExecutor: Executor
) : WiFiPairingService {
private val studioServiceNamePrefix = "studio-"
override fun generateQrCode(backgroundColor: Color, foregroundColor: Color): ListenableFuture<QrCodeImage> {
return taskExecutor.executeAsync {
val serviceName = studioServiceNamePrefix + createRandomString(10)
val password = createRandomString(12)
val pairingString = createPairingString(serviceName, password)
val image = QrCodeGenerator.encodeQrCodeToImage(pairingString, backgroundColor, foregroundColor)
QrCodeImage(serviceName, password, pairingString, image)
}
}
/**
* Format is "WIFI:T:ADB;S:service;P:password;;" (without the quotes)
*/
private fun createPairingString(service: String, password: String): String {
return "WIFI:T:ADB;S:$service;P:$password;;"
}
private fun createRandomString(charCount: Int): String {
@Suppress("SpellCheckingInspection")
val charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-+*/<>{}"
val sb = StringBuilder()
for (i in 1..charCount) {
val char = charSet[randomProvider.nextInt(charSet.length)]
sb.append(char)
}
return sb.toString()
}
}
|
代码不多,生成二维码的函数是 generateQrCode
这里使用 studioServiceNamePrefix
和一串10位数的随机字符串组成 serviceName
,然后随机生成12位的字符串用作 password
。
最后面调用 createPairingString
生成二维码的内容。
到这里我们就可以知道 WIFI:T:ADB;S:${service};P:${password};;
这里除了 service
和 password
之外其它都是固定的。
而 service
又是以 studio-
开头的,所以其实二维码里的内容除了固定的之外, service
和 password
是可以随便设置的。
来看下 WIFI:T:ADB;S:${service};P:${password};;
这里一些字符的含义
- 首先这里使用
:
和 ;
进行分隔,可以分成三部分 WIFI:T:ADB;
、 S:${service};
、 P:${password};;
- 第一部分是固定的
WIFI:T:ADB
表示的意思就是通过 ADB
来连接 WIFI
里的手机
- 第二部分表示的是服务的名字,
S
可能就是 SSID
的缩写,然后通过 :
和 service
进行分割
- 第三部分表示的是服务的密码,有密码是为了安全,同一个局域网里可能有多个设备,有密码可以避免连接到别人的电脑上,
P
应该就是 password
的意思,后面接的就是密码,最后有两个 ;;
第一个好理解,第二个很可能就是用来表示结束的意思。
还做了啥?
从上面的分析,我们知道了二维码生成的规则,但是怎么手机扫一下码就连上了呢?
所以我们猜测 Android Studio
在生成二维码的同时应该还做了其它事情,于是我们查看一下 generateQrCode
是在哪里调用的,看看这行代码附近有没有什么线索。
通过搜索发现调用的地方在 QrCodeScanningController.kt - Android Code Search ,我提取了关键部分如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@UiThread
class QrCodeScanningController(
private val service: WiFiPairingService,
private val view: WiFiPairingView,
parentDisposable: Disposable
) : Disposable {
suspend fun startPairingProcess() {
view.showQrCodePairingStarted()
generateQrCode(view.model)
state = State.Polling
pollMdnsServices()
}
private suspend fun generateQrCode(model: WiFiPairingModel) {
// 这里的 service 是一个接口,它的实现类就是上一小结提到的WiFiPairingServiceImpl
val qrCode = service.generateQrCode(UIColors.QR_CODE_BACKGROUND, UIColors.QR_CODE_FOREGROUND)
model.qrCodeImage = qrCode
}
}
|
从 startPairingProcess
中可以发现,除了调用 generateQrCode
生成二维码之外,还调用了 pollMdnsServices
。
service
的实现类是 WiFiPairingServiceImpl
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private fun pollMdnsServices() {
scope.launch {
// Don't start a new polling request if we are not in "polling" mode
while (state == State.Polling) {
try {
val services = service.scanMdnsServices()
withContext(uiThread(ModalityState.any())) {
view.model.pairingCodeServices = services.filter { it.serviceType == ServiceType.PairingCode }
view.model.qrCodeServices = services.filter { it.serviceType == ServiceType.QrCode }
}
} catch (e: Throwable) {
// TODO: Should we show an error to the user?
LOG.warn("Error scanning mDNS services", e)
}
// Run again in 1 second, unless we are disposed
delay(Duration.ofSeconds(1))
}
}
}
|
在 pollMdnsServices
中有一个延迟1s的循环,只要状态是 Polling
就会一直调用 service.scanMdnsServices
,所以关键还是在 WiFiPairingServiceImpl.scanMdnsServices
上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Throws(AdbCommandException::class)
override suspend fun scanMdnsServices(): List<MdnsService> {
val result = adbService.executeCommand(listOf("mdns", "services"))
// Output example:
// List of discovered mdns services
// adb-939AX05XBZ-vWgJpq _adb-tls-connect._tcp. 192.168.1.86:39149
// adb-939AX05XBZ-vWgJpq _adb-tls-pairing._tcp. 192.168.1.86:37313
// Regular expression
// adb-<everything-until-space><spaces>__adb-tls-pairing._tcp.<spaces><everything-until-colon>:<port>
val lineRegex = Regex("([^\\t]+)\\t*_adb-tls-pairing._tcp.\\t*([^:]+):([0-9]+)")
return result.stdout
.drop(1)
.mapNotNull { line ->
val matchResult = lineRegex.find(line)
matchResult?.let {
try {
val serviceName = it.groupValues[1]
val ipAddress = withContext(Dispatchers.IO) { InetAddress.getByName(it.groupValues[2]) }
val port = it.groupValues[3].toInt()
val serviceType = if (serviceName.startsWith(studioServiceNamePrefix)) ServiceType.QrCode else ServiceType.PairingCode
MdnsService(serviceName, serviceType, ipAddress, port)
} catch (ignored: Exception) {
LOG.warn("mDNS service entry ignored due do invalid characters: $line")
null
}
}
}
}
|
分析上面的代码可以知道这里会调用 adb mdns services
获得输出的结果,然后通过正则表达式去匹配 _adb-tls-pairing._tcp.xxx
的服务,如果能找到就获取它的服务名字、IP、端口号还有服务的类型,然后返回给上一层函数调用。
这里的服务类型有两种一种是 WIFI
配对,另一中是配对码配对。这里判断是否是 WIFI
配对也很简单,就是判断是不是以 studio-
开头的,是就是 WIFI
配对,不是就是用配对码配对。
这里的 adbService
是对 adb
这个命令做了封装,所以 executeCommand
就是执行 adb
的命令,带上参数。这里我就不带大家看了,感兴趣的可以自己查看。
分析了这么多发现关键在 adb mdns services
上,我们尝试在命令行里执行这条指令,看看会输出什么
1
|
List of discovered mdns services
|
啥也没有,这时候你打开手机,在设置里找到 开发者选项 ,然后找到 无线调试 ,点击使用二维码配对设备,然后再次执行这条指令,得到结果如下。注意这里要快点执行,不然连接完成是看不到的
1
2
|
adb-ba46f2ef-1ZThbU _adb-tls-connect._tcp. 192.168.1.15:41583
studio-R*RsW2zVC@ _adb-tls-pairing._tcp. 192.168.1.15:39757
|
有输出了,可以看到第二行 studio-R*RsW2zVC@ _adb-tls-pairing._tcp. 192.168.1.15:39757
就是我们要的。
你会发现第一列不就是我们使用字符串随机生成的吗,这时候就派上用场了。至于第一行的有其它用处,这里暂时先不说。
通过这些分析,我们可以知道,手机使用二维码扫描之后会生成一个 _adb-tls-pairing._tcp.
的服务,然后他的名字就是在 Android Studio
里随机生成的,这里还包含了它的 IP
和端口号。
有了 IP
和端口号接来下就需要连接了,我们来看看怎么连接的。
怎么连接的?
在上一步我们知道了手机对于的 IP
和端口号,所以接下来就需要在电脑上进行连接了,我们并没有手动去连接,所以这里也是 Android Studio
做的。
我们接着从 pollMdnsServices
看起,我把一些代码精简了
1
2
3
4
5
|
private fun pollMdnsServices() {
val services = service.scanMdnsServices()
view.model.pairingCodeServices = services.filter { it.serviceType == ServiceType.PairingCode }
view.model.qrCodeServices = services.filter { it.serviceType == ServiceType.QrCode }
}
|
从上面的代码可以发现,调用 adb mdns services
之后会把 pairingCodeServices
和 qrCodeServices
给保存到 model
里。我们到 model
里一探究竟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* Model used for pairing devices
*/
@UiThread
open class WiFiPairingModel {
/** The list of listeners */
private val listeners: ArrayList<AdbDevicePairingModelListener> = ArrayList()
open var qrCodeServices: List<MdnsService> = emptyList()
set(value) {
field = value
listeners.forEach { it.qrCodeServicesDiscovered(value) }
}
open var pairingCodeServices: List<MdnsService> = emptyList()
set(value) {
field = value
listeners.forEach { it.pairingCodeServicesDiscovered(value) }
}
}
|
可以发现这里会分别调用 MdnsService
中的 pairingCodeServicesDiscovered
和 qrCodeServicesDiscovered
所以这里的关键是找到 AdbDevicePairingModelListener
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
@UiThread
class QrCodeScanningController(
private val service: WiFiPairingService,
private val view: WiFiPairingView,
parentDisposable: Disposable
) : Disposable {
private val modelListener = MyModelListener()
init {
// ...
view.model.addListener(modelListener)
}
@UiThread
inner class MyModelListener : AdbDevicePairingModelListener {
override fun qrCodeServicesDiscovered(services: List<MdnsService>) {
LOG.info("${services.size} QR code connect services discovered")
services.forEachIndexed { index, it ->
LOG.info(" QR code connect service #${index + 1}: name=${it.serviceName} - ip=${it.ipAddress} - port=${it.port}")
}
// If there is a QR Code displayed, look for a mDNS service with the same service name
view.model.qrCodeImage?.let { qrCodeImage ->
services.firstOrNull { it.serviceName == qrCodeImage.serviceName }
?.let {
// We found the service we created, meaning the phone is in "pairing" mode
startPairingDevice(it, qrCodeImage.password)
}
}
}
override fun pairingCodeServicesDiscovered(services: List<MdnsService>) {
LOG.info("${services.size} pairing code pairing services discovered")
services.forEachIndexed { index, it ->
LOG.info(" Pairing code pairing service #${index + 1}: name=${it.serviceName} - ip=${it.ipAddress} - port=${it.port}")
}
}
}
}
|
view.model
的 listener
是在构造函数里赋值的,它的类型是 MyModelListener
。
这个对象的 pairingCodeServicesDiscovered
没做什么事情,我们重点看 qrCodeServicesDiscovered
。
在 qrCodeServicesDiscovered
中最重要的就是调用了 startPairingDevice
然后把之前生成的密码传进去了。
1
2
3
4
5
6
7
8
9
|
private fun startPairingDevice(mdnsService: MdnsService, password: String) {
// ...
state = State.Pairing
val pairingResult = service.pairMdnsService(mdnsService, password)
view.showQrCodePairingWaitForDevice(pairingResult)
val device = service.waitForDevice(pairingResult)
state = State.PairingSuccess
view.showQrCodePairingSuccess(mdnsService, device)
}
|
省略部分代码,发现最关键的在 service.pairMdnsService
中,接着就是调用 service.waitForDevice
,我把代码精简后如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
override suspend fun pairMdnsService(mdnsService: MdnsService, password: String): PairingResult {
// ...
val deviceAddress = "${mdnsService.ipAddress.hostAddress}:${mdnsService.port}"
val passwordInput = password + LineSeparator.getSystemLineSeparator().separatorString
val result = adbService.executeCommand(listOf("pair", deviceAddress), passwordInput)
val lineRegex = Regex("Successfully paired to ([^:]*):([0-9]*) \\[guid=([^\\]]*)\\]")
val matchResult = lineRegex.find(result.stdout[0])
return matchResult?.let {
val ipAddress = withContext(Dispatchers.IO) { InetAddress.getByName(it.groupValues[1]) }
val port = it.groupValues[2].toInt()
val serviceGuid = it.groupValues[3]
PairingResult(ipAddress, port, serviceGuid)
}
override suspend fun waitForDevice(pairingResult: PairingResult): AdbOnlineDevice {
return adbService.waitForOnlineDevice(pairingResult)
}
|
可以看到这里最关键的还是调用 adb
去执行命令,也就是调用 adb pair ip:port password
。其中 IP
和端口号就是我们之前获取到的,密码就是我们自己生成的随机字符串。
接下来就是获取执行这条命令的结果,接着就是调用 waitForDevice
等待设备的连接。
总结
分析完了,我们来总结一下
- 生成两个随机字符串用于名字和密码,并根据一定的规则生成二维码,等待用户扫描
- 通过不断地调用
adb mdns services
查看是否有 _adb_tls-pairing._tcp.
的服务,并判断名字是否就是我们上一步生成的。
- 找到对应的服务后获取到对应的
IP
和端口,通过 adb pair ip:port password
进行配对
- 等待设备连接成功
知道了 WIFI
连接的原理,我们自己也可以实现一个类似的功能,这样就不用每次都打开 Android Studio
才能使用 WIFI
连接了。
在下一篇文章我会采用 Rust
实现类似的功能。
参考