title:
date: 2019-06-04 14:46:40
tags:
- ADB
- ddmlib
- Android
- Java
categories:
引言
因为最近开发的系统,需要从Java端控制Android,所以使用到了
的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操作,在请求的描述中会包含序列号来指定想要操纵的设备,随后会传输要执行的指令等数据。adb -s <serialNumber> shell <command>
- host-serial: 这种请求类似于执行
,是将手机上的socket映射到宿主机上。adb -s forward <local> <remote>
其中第二个是整个ddmlib中使用最多的,几乎所有的
功能都是通过该方式实现。接下来我会以一个简单的
adb shell ls
来介绍一下完整的Socket通讯。
#假设要操作的手机序列号为 ABCDEF
#Socket 连通后>>>
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
*/
@Nullable
GlobalTrafficShapingHandler 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);
}
}
@Override
public 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 to
// talk to a specific device
setDevice(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 device
if (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();
}
@Override
protected 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操作,在请求的描述中会包含序列号来指定想要操纵的设备。adb -s <serialNumber> shell <command>
- 处理方式: 从请求中解析出序列号,然后对这部分进行过滤,如果有非法的代理请求,就直接断开连接。
-
- host-serial
-
adb -s forward <local> <remote>
-
处理完这些需要过滤的内容后,最后我们会在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;
@Override
public void channelInactive(ChannelHandlerContext ctx) {
if (adbConnection != null && adbConnection.isActive()) {
log.info("Adb-Proxy {}-{}: Closed, reason: proxy connection closed", ctx.channel().id(), serialNumber);
adbConnection.close();
}
}
@Override
protected 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仓库
仓库地址注意事项
因为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-27090
service iptables save
service iptables restart
sysctl -w net.ipv4.conf.all.route_localnet=1
文章说明
更多有价值的文章均收录于
贝贝猫的文章目录版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。
参考内容
[1]
Android 调试桥[2]
Android源码