天天看点

ADB 工具包 ddmlib 的剖析与修改

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大类:

  1. host:track-devices: 这种请求类似于执行

    adb devices

    , 它会实时返回设备的上下线情况。
  2. host:transport: 这种请求类似于执行

    adb -s <serialNumber> shell <command>

    , 是针对某一设备的ADB操作,在请求的描述中会包含序列号来指定想要操纵的设备,随后会传输要执行的指令等数据。
  3. host-serial: 这种请求类似于执行

    adb -s forward <local> <remote>

    ,是将手机上的socket映射到宿主机上。

其中第二个是整个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功能。

ADB 工具包 ddmlib 的剖析与修改

初始化完成后我们需要通过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的修改

  1. 传输带宽限制: 基于Netty的GlobalTrafficShapingHandler实现,此外由于使用到了Netty,所以处理大内存的数据(手机截图)时可以达到零拷贝的效果。
  2. 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 工具包 ddmlib 的剖析与修改

实现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请求的头部信息,并根据请求的内容来实现设备拦截的功能。

需要注意的请求:

  1. host:track-devices
    • 介绍: 这种请求类似于执行

      adb devices

    • 处理方式: 需要对原始track-devices请求的返回结果进行过滤,因为我们可能只放行了部分手机的ADB Proxy功能,这样只会传输指定设备的上线/下线信息。
  2. host:transport
    • adb -s <serialNumber> shell <command>

      , 是针对某一设备的ADB操作,在请求的描述中会包含序列号来指定想要操纵的设备。
    • 处理方式: 从请求中解析出序列号,然后对这部分进行过滤,如果有非法的代理请求,就直接断开连接。
  3. 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           

文章说明

更多有价值的文章均收录于

贝贝猫的文章目录
ADB 工具包 ddmlib 的剖析与修改

版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。

参考内容

[1]

Android 调试桥

[2]

Android源码