引言
因为最近开发的系统,需要从Java端控制Android,所以使用到了ADB的Java库ddmlib,它的功能非常全,而且是Google官方维护的ADB Java Lib。但是在实际使用的过程中,出现了并发使用时ADB掉线的情况,怀疑是通过ADB传输的数据带宽消耗过大导致的,所以对ddmlib进行了修改,使其可以设置每台手机的传输带宽限制。此外,为了远程调试线上系统的指定设备,我还在ddmlib加入了一个ADB Proxy的功能。
如何获取最新的官方ddmlib源码
在Google官仓中有很多个分支,其中很多已经很久没有维护了,我挨个看了一下,发现studio-master-dev分支下的ddmlib是最新的,所以我就以该分支作为修改的起点,最终选用的Git节点是539b90ad。
ddmlib的简单剖析
ddmlib中包含两个主要功能:
[1]Java的debug: 通过它可以连接到手机上的JVM Debugger,我推测这一部分应该主要是Android Studio使用,因为我并没有用到这一部分,所以基本没有做改动,本文也不进行深入介绍。
[2]传统ADB接口: 这些接口全部都是通过Socket连接ADB Server,然后通过ADB协议进行通讯,协议的内容也不是很复杂,可以分为如下3大类:
- host:track-devices: 这种请求类似于执行
adb devices
, 它会实时返回设备的上下线情况。 - host:transport: 这种请求类似于执行
adb -s <serialNumber> shell <command>
, 是针对某一设备的ADB操作,在请求的描述中会包含序列号来指定想要操纵的设备,随后会传输要执行的指令等数据。 - host-serial: 这种请求类似于执行
adb -s forward <local> <remote>
,是将手机上的socket映射到宿主机上。
其中第二个是整个ddmlib中使用最多的,几乎所有的ADB功能都是通过该方式实现。接下来我会以一个简单的adb shell ls
来介绍一下完整的Socket通讯。
1 | #假设要操作的手机序列号为 ABCDEF |
ddmlib的入口
在使用ddmlib时需要先通过AndroidDebugBridge#initIfNeeded初始化ADB。其中clientSupport参数可以控制ddmlib的工作模式,这个工作模式的区别主要是关于是否开启Java Debug功能。
初始化完成后我们需要通过AndroidDebugBridge#createBridge来创建AndroidDebugBridge实例,在后续的使用中,我们需要通过该实例来获取设备的抽象,并通过它来操作手机。
值得一提的是,ddmlib默认是连接本机的ADB Server,但是也可以通过DdmPreferences中的相关参数进行控制,使其连接到其他主机。我的ADB proxy功能就是基于这部分参数来使用的。
ddmlib的使用
通常来说我们需要先注册一个DeviceChangeListener,通过它可以监听到设备的上线,下线事件,然后初始化AndroidDebugBridge。
1 | AndroidDebugBridge.addDeviceChangeListener(listener); |
在DeviceChangeListener的回调中,我们也可以获取到ddmlib对设备的抽象IDevice,里面包含了全部ADB操作。IDevice的实现基本上都是和ADB Server建立Socket连接,然后通过协议来执行命令。
1 | public interface IDeviceChangeListener { |
对ddmlib的修改
- 传输带宽限制: 基于Netty的GlobalTrafficShapingHandler实现,此外由于使用到了Netty,所以处理大内存的数据(手机截图)时可以达到零拷贝的效果。
- ADB Proxy: 开放一个Socket Server,并且解析了ADB请求的头部信息,然后通过请求头部的内容进行了一些设备拦截的操作,这样可以达到只放行指定设备ADB Proxy的效果。
对初始化过程的修改
为了让Netty的配置更加灵活,我给AndroidDebugBridge.initIfNeeded加了一个新参数AdbNettyConfig,通过它可以修改NettyClient相关的参数。其中,TrafficHandlerGetter就是用来获取每台手机的带宽限制的,此外还包括全部设备的整体带宽限制。
1 | public class AdbNettyConfig { |
对ADB连接过程的修改
有了上述参数后,我在初始化过程中构建了一个AdbConnector,后续建立与ADB Server之间的连接时,都是通过该AdbConnector进行。在连接过程中会在NettyPipeLine中注入之前设置好的GlobalTrafficShapingHandler。
1 | public class AdbConnector extends ChannelDuplexHandler { |
完成上述内容之后,剩下的工作就基本都是重复的,就是替换现有的直接Socket实现,使用AdbConnector来创建基于Netty的AdbConnection,在AdbConnection中,我封装了一些常用的数据发送接口。
1 | public class AdbConnection implements Closeable { |
对请求处理过程的修改
在使用AdbConnection时,基本是按照如下模板进行的,值得注意的是,我们需要针对不同的ADB请求,实现不同的ResponseHandler,就像如下例子中的AdbStreamInputHandler,它需要在请求发送出去之前注入Netty的PipeLine中,这样才能保证返回的数据不会漏。
1 | try (AdbConnection adbConnection = adbConnector.connect(adbSockAddr, device.getSerialNumber())) { |
我这里根据不同的ADB请求实现了如下Handlers:
实现ADB Proxy Server
参考ddmlib的初始化参数设置方法,我将ADB Proxy的设置也放进了DdmPreferences,如果打开了该功能,我会开一个Socket Server来接受ADB命令。
1 | public class AdbDeviceProxy extends ChannelInitializer { |
在ADB Proxy中最重要的是如何识别每个ADB连接的请求内容,并针对不同请求作出不同的处理,在这里需要解析ADB请求的头部信息,并根据请求的内容来实现设备拦截的功能。
需要注意的请求:
- host:track-devices
- 介绍: 这种请求类似于执行
adb devices
, 它会实时返回设备的上下线情况。 - 处理方式: 需要对原始track-devices请求的返回结果进行过滤,因为我们可能只放行了部分手机的ADB Proxy功能,这样只会传输指定设备的上线/下线信息。
- 介绍: 这种请求类似于执行
- host:transport
- 介绍: 这种请求类似于执行
adb -s <serialNumber> shell <command>
, 是针对某一设备的ADB操作,在请求的描述中会包含序列号来指定想要操纵的设备。 - 处理方式: 从请求中解析出序列号,然后对这部分进行过滤,如果有非法的代理请求,就直接断开连接。
- 介绍: 这种请求类似于执行
- host-serial
- 介绍: 这种请求类似于执行
adb -s forward <local> <remote>
,是将手机上的socket映射到宿主机上。 - 处理方式: 从请求中解析出序列号,然后对这部分进行过滤,如果有非法的代理请求,就直接断开连接。
- 介绍: 这种请求类似于执行
处理完这些需要过滤的内容后,最后我们会在Netty的PipeLine中加入一个ProxyHandler,通过它来代理传输ProxyConnection和OriginalAdbConnection的数据,此外还要达到其中一个连接断开时,自动断开另一个连接的效果。
1 | public class ConnectionProxyHandler extends ByteToMessageDecoder { |
Github仓库
仓库地址
注意事项
因为adb forward只会监听宿主机的localhost,所以如果想要同时使用ADB Proxy和adb forward,需要设置一下iptables,完成从网络接口ip到localhost的映射。
1 | # 这里以27081-27090为例 |
参考内容
[1] Android 调试桥
[2] Android源码