引言
因为最近开发的系统,需要从 Java 端控制 Android,所以使用到了ADB[1]的 Java 库 ddmlib,它的功能非常全,而且是 Google 官方维护的 ADB Java Lib。但是在实际使用的过程中,出现了并发使用时 ADB 掉线的情况,怀疑是通过 ADB 传输的数据带宽消耗过大导致的,所以对 ddmlib 进行了修改,使其可以设置每台手机的传输带宽限制。此外,为了远程调试线上系统的指定设备,我还在 ddmlib 加入了一个 ADB Proxy 的功能。
如何获取最新的官方 ddmlib 源码
在 Google官仓[2]中有很多个分支,其中很多已经很久没有维护了,我挨个看了一下,发现studio-master-dev[3]分支下的 ddmlib 是最新的,所以我就以该分支作为修改的起点,最终选用的 Git 节点是539b90ad[4]。
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[5]功能都是通过该方式实现。接下来我会以一个简单的adb shell ls
来介绍一下完整的 Socket 通讯。
假设要操作的手机序列号为 ABCDEFSocket 连通后>>>ddmlib: host:transport:ABCDEF (实际发送的数据之前还包含4个16进制ASCII符号来描述发送数据的长度,因为我们要发送的数据长度为21,对应的16进制数为0015,所以最终的发送数据为'0015host:transport:ABCDEF')ADB Server: OKAY or FAIL (返回OKAY代表成功,如果返回FAIL代表失败,之后会跟4个字符的十六进制ASCII符号来表示错误信息的长度,拿到错误信息长度后,我们需要再读取相应长度的数据作为执行错误的原因)假设请求成功>>>ddmlib: shell:ls (这部分也需要描述消息长度的前缀,构造方式同上)ADB Server: ls的返回数据会全部通过socket传输
ddmlib 的入口
在使用 ddmlib 时需要先通过 AndroidDebugBridge#initIfNeeded 初始化 ADB。其中 clientSupport 参数可以控制 ddmlib 的工作模式,这个工作模式的区别主要是关于是否开启 Java Debug 功能。
初始化完成后我们需要通过 AndroidDebugBridge#createBridge 来创建 AndroidDebugBridge 实例,在后续的使用中,我们需要通过该实例来获取设备的抽象,并通过它来操作手机。值得一提的是,ddmlib 默认是连接本机的 ADB Server,但是也可以通过 DdmPreferences 中的相关参数进行控制,使其连接到其他主机。我的 ADB proxy 功能就是基于这部分参数来使用的。
ddmlib 的使用
通常来说我们需要先注册一个 DeviceChangeListener,通过它可以监听到设备的上线,下线事件,然后初始化 AndroidDebugBridge。
AndroidDebugBridge.addDeviceChangeListener(listener);AndroidDebugBridge.initIfNeeded(false); clientSupport参数设为false,关闭Java Debug的功能AndroidDebugBridge.createBridge();
在 DeviceChangeListener 的回调中,我们也可以获取到 ddmlib 对设备的抽象 IDevice,里面包含了全部 ADB 操作。IDevice 的实现基本上都是和 ADB Server 建立 Socket 连接,然后通过协议来执行命令。
public interface IDeviceChangeListener {*** Sent when the a device is connected to the {@link AndroidDebugBridge}.* <p>* This is sent from a non UI thread.* @param device the new device.*/void deviceConnected(@NonNull IDevice device);*** Sent when the a device is connected to the {@link AndroidDebugBridge}.* <p>* This is sent from a non UI thread.* @param device the new device.*/void deviceDisconnected(@NonNull IDevice device);*** Sent when a device data changed, or when clients are started/terminated on the device.* <p>* This is sent from a non UI thread.* @param device the device that was updated.* @param changeMask the mask describing what changed. It can contain any of the following* values: {@link IDevice#CHANGE_BUILD_INFO}, {@link IDevice#CHANGE_STATE},* {@link IDevice#CHANGE_CLIENT_LIST}*/void deviceChanged(@NonNull IDevice device, int changeMask);}
对 ddmlib 的修改
传输带宽限制: 基于 Netty 的 GlobalTrafficShapingHandler 实现,此外由于使用到了 Netty,所以处理大内存的数据(手机截图)时可以达到零拷贝的效果。
ADB Proxy: 开放一个 Socket Server,并且解析了 ADB 请求的头部信息,然后通过请求头部的内容进行了一些设备拦截的操作,这样可以达到只放行指定设备 ADB Proxy 的效果。
对初始化过程的修改
为了让 Netty 的配置更加灵活,我给 AndroidDebugBridge.initIfNeeded 加了一个新参数 AdbNettyConfig,通过它可以修改 NettyClient 相关的参数。其中,TrafficHandlerGetter 就是用来获取每台手机的带宽限制的,此外还包括全部设备的整体带宽限制。
public class AdbNettyConfig {private String eventExecutorGroupPrefix = "AdbBoss";private int eventExecutorGroupThreadSize = 1;private String eventLoopGroupWorkerPrefix = "AdbWorker";private String proxyEventLoopGroupWorkerPrefix = "AdbProxyWorker";private int eventLoopGroupWorkerThreadSize = NettyRuntime.availableProcessors();private int connectTimeoutMills = 10000;private TrafficHandlerGetter trafficHandlerGetter = new DefaultTrafficHandlerGetter();}public interface TrafficHandlerGetter {*** Get device traffic handler** @param serialNumber device serial number* @return device traffic handler*/@NullableGlobalTrafficShapingHandler getDeviceTrafficHandler(String serialNumber);*** Get global traffic handler** @return global traffic handler*/@Nullable GlobalTrafficShapingHandler getGlobalTrafficHandler();}
对 ADB 连接过程的修改
有了上述参数后,我在初始化过程中构建了一个 AdbConnector,后续建立与 ADB Server 之间的连接时,都是通过该 AdbConnector 进行。在连接过程中会在 NettyPipeLine 中注入之前设置好的 GlobalTrafficShapingHandler。
public class AdbConnector extends ChannelDuplexHandler {private final AdbNettyConfig config;private final Bootstrap bootstrap;public AdbConnector(AdbNettyConfig config) {this.config = config;bootstrap = new Bootstrap().group(new NioEventLoopGroup(config.getEventLoopGroupWorkerThreadSize(),new NamedThreadFactory(config.getEventLoopGroupWorkerPrefix(),config.getEventLoopGroupWorkerThreadSize()))).channel(NioSocketChannel.class).handler(this).option(NioChannelOption.CONNECT_TIMEOUT_MILLIS, config.getConnectTimeoutMills()).option(NioChannelOption.TCP_NODELAY, Boolean.TRUE).option(NioChannelOption.SO_KEEPALIVE, Boolean.TRUE);}public AdbConnection connect(InetSocketAddress adbSockAddr, String serialnumber) throws IOException {ChannelFuture f = this.bootstrap.connect(adbSockAddr);try {f.await(config.getConnectTimeoutMills(), TimeUnit.MILLISECONDS);if (f.isCancelled()) {throw new IOException("connect cancelled, can not connect to component.", f.cause());} else if (!f.isSuccess()) {throw new IOException("connect failed, can not connect to component.", f.cause());} else {injectTrafficHandler(f.channel(), serialnumber);f.channel().pipeline().remove(this);return new AdbConnection(f.channel());}} catch (Exception e) {throw new IOException("can not connect to component.", e);}}private void injectTrafficHandler(Channel channel, String serialNumber) {GlobalTrafficShapingHandler globalTrafficHandler =config.getTrafficHandlerGetter().getGlobalTrafficHandler();if (!Objects.isNull(globalTrafficHandler)) {channel.pipeline().addLast(globalTrafficHandler);}if (!Objects.isNull(serialNumber)) {GlobalTrafficShapingHandler deviceTrafficHandler =config.getTrafficHandlerGetter().getDeviceTrafficHandler(serialNumber);if (!Objects.isNull(deviceTrafficHandler)) {channel.pipeline().addLast(deviceTrafficHandler);}}}}
完成上述内容之后,剩下的工作就基本都是重复的,就是替换现有的直接 Socket 实现,使用 AdbConnector 来创建基于 Netty 的 AdbConnection,在 AdbConnection 中,我封装了一些常用的数据发送接口。
public class AdbConnection implements Closeable {private Channel channel;private boolean alreadyProxy = false;AdbConnection(Channel channel) {this.channel = channel;}public synchronized void sendAndWaitSuccess(String message, long timeout,TimeUnit timeUnit, AdbInputHandler... nextHandlers) throws TimeoutException, AdbCommandRejectedException {AdbRespondHandler adbRespondHandler = new AdbRespondHandler();channel.pipeline().addLast(adbRespondHandler);if (nextHandlers != null && nextHandlers.length > 0) {channel.pipeline().addLast(nextHandlers);}send(message, timeout, timeUnit);adbRespondHandler.waitRespond(timeout, timeUnit);if (!adbRespondHandler.getOkay()) {throw new AdbCommandRejectedException(adbRespondHandler.getMessage());}}public boolean isActive() {return channel != null && channel.isActive();}public void buildProxyConnectionIfNecessary(ChannelHandlerContext ctx, String serialNumber) {if (!alreadyProxy) {channel.pipeline().addLast(new ProxyInputHandler(ctx, serialNumber));alreadyProxy = true;}}public void writeAndFlush(ByteBuf buf) {channel.writeAndFlush(buf);}public synchronized void syncSendAndHandle(byte[] bytes, AdbInputHandler handler, long timeout, TimeUnit timeUnit) {channel.pipeline().addLast(handler);syncSend(bytes, 0, bytes.length, timeout, timeUnit);}public synchronized void syncSend(byte[] bytes, int offset, int length, long timeout, TimeUnit timeUnit) {try {if (timeout > 0) {channel.writeAndFlush(Unpooled.wrappedBuffer(bytes, offset, length)).await(timeout, timeUnit);} else {channel.writeAndFlush(Unpooled.wrappedBuffer(bytes, offset, length)).await();}} catch (InterruptedException e) {throw new RuntimeException("Interrupted", e);}}public synchronized void syncSend(byte[] bytes, long timeout, TimeUnit timeUnit) {syncSend(bytes, 0, bytes.length, timeout, timeUnit);}public synchronized void send(String message, long timeout, TimeUnit timeUnit) throws TimeoutException,AdbCommandRejectedException {doSend(message, timeout, timeUnit, new AdbStringOutputHandler());}public synchronized void send(InputStream inputStream) throws TimeoutException,AdbCommandRejectedException {doSend(inputStream, 0, null, new AdbStreamOutputHandler());}private <T> void doSend(T message, long timeout, TimeUnit timeUnit,AdbOutputHandler<T> adbStringOutputHandler) throws TimeoutException, AdbCommandRejectedException {channel.pipeline().addLast(adbStringOutputHandler);try {ChannelFuture channelFuture = channel.writeAndFlush(message);if (timeout > 0) {if (!channelFuture.await(timeout, timeUnit)) {channelFuture.cancel(true);throw new TimeoutException("Send request timeout.");}} else {channelFuture.await();}if (!channelFuture.isSuccess()) {throw new AdbCommandRejectedException("Send data failed, " +(Objects.isNull(channelFuture.cause()) ? "" : channelFuture.cause().getMessage()));}} catch (InterruptedException e) {throw new RuntimeException("Interrupted.", e);} finally {channel.pipeline().remove(adbStringOutputHandler);}}@Overridepublic void close() {channel.close();}}
对请求处理过程的修改
在使用 AdbConnection 时,基本是按照如下模板进行的,值得注意的是,我们需要针对不同的 ADB 请求,实现不同的 ResponseHandler,就像如下例子中的 AdbStreamInputHandler,它需要在请求发送出去之前注入 Netty 的 PipeLine 中,这样才能保证返回的数据不会漏。
try (AdbConnection adbConnection = adbConnector.connect(adbSockAddr, device.getSerialNumber())) {if the device is not -1, then we first tell adb we're looking totalk to a specific devicesetDevice(adbConnection, device);AdbStreamInputHandler customRespondHandler = new AdbStreamInputHandler(rcvr);adbConnection.sendAndWaitSuccess(adbService.name().toLowerCase() + ":" + command,DdmPreferences.getTimeOut(),TimeUnit.MILLISECONDS, customRespondHandler);/ stream the input file if present.if (!Objects.isNull(is)) {adbConnection.send(is);}customRespondHandler.waitResponseBegin(maxTimeToOutputResponse, maxTimeUnits);customRespondHandler.waitFinish(maxTimeout, maxTimeUnits);}static void setDevice(AdbConnection adbConnection, IDevice device)throws TimeoutException, AdbCommandRejectedException {// if the device is not -1, then we first tell adb we're looking to talk// to a specific deviceif (device != null) {adbConnection.sendAndWaitSuccess("host:transport:" + device.getSerialNumber(),DdmPreferences.getTimeOut(),TimeUnit.MILLISECONDS);}}
我这里根据不同的 ADB 请求实现了如下 Handlers:
实现 ADB Proxy Server
参考 ddmlib 的初始化参数设置方法,我将 ADB Proxy 的设置也放进了 DdmPreferences,如果打开了该功能,我会开一个 Socket Server 来接受 ADB 命令。
public class AdbDeviceProxy extends ChannelInitializer {private volatile static AdbDeviceProxy INSTANCE;private final EventLoopGroup eventLoopGroupBoss;private final EventLoopGroup eventLoopGroupWorker;private final ServerBootstrap bootstrap = new ServerBootstrap();private AdbDeviceProxy(AdbNettyConfig adbNettyConfig) {try {eventLoopGroupWorker = new NioEventLoopGroup(adbNettyConfig.getEventLoopGroupWorkerThreadSize(),new NamedThreadFactory(adbNettyConfig.getProxyEventLoopGroupWorkerPrefix(),adbNettyConfig.getEventLoopGroupWorkerThreadSize()));eventLoopGroupBoss = new NioEventLoopGroup(adbNettyConfig.getEventExecutorGroupThreadSize(),new NamedThreadFactory(adbNettyConfig.getEventExecutorGroupPrefix(),adbNettyConfig.getEventExecutorGroupThreadSize()));} catch (Exception e) {throw new RuntimeException("Adb proxy event loop groups instantiation failed", e);}init();}@Overrideprotected void initChannel(Channel ch) {ch.pipeline().addFirst(new ConnectionProxyHandler());}private void init() {log.info("Adb-Proxy: Initializing...");bootstrap.group(eventLoopGroupBoss, eventLoopGroupWorker).channel(NioServerSocketChannel.class).childHandler(this);try {ChannelFuture bindFuture = bootstrap.bind(DdmPreferences.getAdbProxyPort()).sync();if (!bindFuture.isSuccess()) {throw new RuntimeException("Adb proxy port binding failed", bindFuture.cause());} else {log.info("Adb-Proxy: Server started at port: {}", DdmPreferences.getAdbProxyPort());}} catch (InterruptedException e) {throw new RuntimeException("Adb proxy port binding interrupted", e);}}}
在 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 的数据,此外还要达到其中一个连接断开时,自动断开另一个连接的效果。
public class ConnectionProxyHandler extends ByteToMessageDecoder {private static final int LENGTH_FIELD_SIZE = 4;private static final String SPECIFIC_DEVICE_TRANSPORT_HEADER = "host:transport:";private static final String FORWARD_HEADER = "host-serial:";private Integer headerLength;private String header;private AdbConnection adbConnection;private String serialNumber = "NULL";private boolean originalConnectionHeaderSent = false;@Overridepublic void channelInactive(ChannelHandlerContext ctx) {if (adbConnection != null && adbConnection.isActive()) {log.info("Adb-Proxy {}-{}: Closed, reason: proxy connection closed", ctx.channel().id(), serialNumber);adbConnection.close();}}@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {if (header == null) {if (headerLength == null) {if (in.readableBytes() >= LENGTH_FIELD_SIZE) {headerLength = Integer.parseInt(in.readSlice(LENGTH_FIELD_SIZE).toString(AdbHelper.DEFAULT_CHARSET), 16);}} else if (in.readableBytes() >= headerLength) {header = in.readSlice(headerLength).toString(AdbHelper.DEFAULT_CHARSET);}}if (header != null) {if (header.equals(ADB_TRACK_DEVICES_COMMAND)) {handleTrackDevices(ctx, in);} else if (header.startsWith(SPECIFIC_DEVICE_TRANSPORT_HEADER)) {handleTransport(header.replace(SPECIFIC_DEVICE_TRANSPORT_HEADER, ""), ctx, in);} else if (header.startsWith(FORWARD_HEADER)) {handleTransport(header.split(":")[1], ctx, in);} else {log.error("Adb-Proxy {}-{}: Closed, reason: header type not supported yet, {}",ctx.channel().id(), serialNumber, header);ctx.close();}}}private void handleTransport(String serialNumber, ChannelHandlerContext ctx, ByteBuf in) {this.serialNumber = serialNumber;if (DdmPreferences.shouldOpenAdbProxy(serialNumber)) {buildProxy(ctx, in);} else {log.info("Adb-Proxy {}-{}: Closed, reason: want to use limited device", ctx.channel().id(), serialNumber);ctx.close();}}private void handleTrackDevices(ChannelHandlerContext ctx, ByteBuf in) {if (createProxyConnectionSuccess(ctx)) {try {if (!originalConnectionHeaderSent) {// 先发OKAY,保证OKAY在device list之前发送,如果出错了直接断开就可以接受ctx.writeAndFlush(Unpooled.wrappedBuffer(ID_OKAY));adbConnection.sendAndWaitSuccess(header,DdmPreferences.getTimeOut(),TimeUnit.MILLISECONDS,new DeviceMonitorHandler(),new TrackDevicesFilterHandler(ctx));originalConnectionHeaderSent = true;}if (in.readableBytes() > 0) {adbConnection.writeAndFlush(in.readBytes(in.readableBytes()));}} catch (Exception e) {log.info("Adb-Proxy {}-{}: Closed, reason: {}", ctx.channel().id(), serialNumber, e.getMessage());ctx.close();}}}private void buildProxy(ChannelHandlerContext ctx, ByteBuf in) {if (createProxyConnectionSuccess(ctx)) {adbConnection.buildProxyConnectionIfNecessary(ctx, serialNumber);if (!originalConnectionHeaderSent) {adbConnection.writeAndFlush(Unpooled.wrappedBuffer(String.format("%04X%s", headerLength, header).getBytes(AdbHelper.DEFAULT_CHARSET)));originalConnectionHeaderSent = true;}if (in.readableBytes() > 0) {adbConnection.writeAndFlush(in.readBytes(in.readableBytes()));}}}private boolean createProxyConnectionSuccess(ChannelHandlerContext ctx) {if (adbConnection == null) {try {adbConnection = AdbHelper.connect(AndroidDebugBridge.getSocketAddress(), null);log.info("Adb-Proxy {}-{}: Opened, command: {}", ctx.channel().id(),serialNumber, header);return true;} catch (IOException e) {log.info("Adb-Proxy {}-{}: Closed, reason: create original adb connection failed", ctx.channel().id(),serialNumber);ctx.close();return false;}} else {return true;}}}
Github 仓库
https://github.com/BeiKeJieDeLiuLangMao/netty-ddmlib
注意事项
因为 adb forward 只会监听宿主机的 localhost,所以如果想要同时使用 ADB Proxy 和 adb forward,需要设置一下 iptables,完成从网络接口 ip 到 localhost 的映射。
# 这里以27081-27090为例iptables -t nat -A PREROUTING -p tcp -m tcp --dport 27081:27090 -j DNAT --to-destination 127.0.0.1:27081-27090service iptables saveservice iptables restartsysctl -w net.ipv4.conf.all.route_localnet=1
参考内容
[1]https://developer.android.com/studio/command-line/adb [2]https://android.googlesource.com/platform/tools/base/
参考资料
ADB: https://developer.android.com/studio/command-line/adb
[2]官仓: https://android.googlesource.com/platform/tools/base/
[3]studio-master-dev: https://android.googlesource.com/platform/tools/base/+/refs/heads/studio-master-dev
[4]539b90ad: https://android.googlesource.com/platform/tools/base/+/539b90ad38808a0f3efec037c9a6ac3603ef0f9b
[5]ADB: https://developer.android.com/studio/command-line/ad




