Physical Address:
ChongQing,China.
WebSite:

一种远程调试安卓设备的简易方法。

在很多个项目的初期阶段,团队都会遇到一个问题,那就是硬件设备有限的情况下,怎么让大家可以远程共用一个安卓设备。
就我们团队而言,有开发上层APP的,也有开发Java服务的,以及开发HAL层或者NativeRuntime的,大家对于设备的使用诉求都会有所不同。
很多时候其实是让大家可以同时使用一个安卓设备的,开发应用的同事需要关注界面交互,ADB连接之后借助Scrcpy投屏即可。而开发HAL、Native服务的同事完全不需要关注界面交互,只要有相应的命令行环境就行。
面对这种需求,我的第一想法是将安卓设备接入到一台单独的PC机,然后让开发者都登录到该PC机,该PC机为Linux系统,每个人都可以设定自己的用户。但这种方式需要额外的一台PC机,且需要将安卓设备与该PC机通过USB线进行连接,且不方便应用开发的同事进行投屏测试。
其实,该问题的核心就在于要将该安卓设备的ADB接口暴露到每个开发者的PC端,这样就好比该安卓设备通过USB线真的连接到了开发人员的电脑上。
经常接触安卓开发的朋友应该知道安卓自身提供了无线调试的能力,我们可以通过以下方式开启ADB无线调试:
#接入ADB线束,开启无线调试
franzkafka@:~$ adb devices
List of devices attached
0.0.0.0:6520 device
franzkafka@:~$ adb tcpip 5555
restarting in TCP mode port: 5555
出现以上信息表明现在安卓设备已经支持了无线ADB调试,我们需要确保我们的电脑与该安卓设备处于同一局域网中。假设安卓设备与电脑都连接了同一个WIFI热点,安卓设备IP地址为1.2.3.4,而电脑设备IP地址为1.2.3.5,我们通过如下命令建立无线连接:
#建立无线连接
adb connect 1.2.3.4
#进入Shell终端
adb shell
通过这种方式,开发者不再局限于通过USB线束连接物理安卓设备来进行调试,可以通过网络就能方便快捷的实现远程调试。
那有没有什么办法可以让安卓设备将无线调试的能力暴露到公网上呢,这样大家都可以通过公网建立网络连接来进行远程调试。最简单的办法自然是——端口转发。
要实现端口转发的方式有很多,其中SSH隧道是最为简单的,这里我们推荐通过SSH隧道实现端口转发。其实现原理参考下图:

前置条件:
1.具备公网IP的服务器,该服务防火墙需要放行特定端口;
2.适配安卓系统的SSH二进制,在Android 14之前其实在AOSP的源码树中:external/openssh可以直接编译;在Android 14之后需要通过额外的patch才能进行编译。
第一点我们不做过多赘述,大家需要有自己的带公网IP的服务器。出于速度、稳定考虑,建议选择的服务器与我们的设备位于同一个地区或者就近地区。这样可以获得比较好的体验。
针对第二点,如果是Android14之后的版本,我们可以通过以下方式获取合适的源码:
//在AOSP源码树下执行
git clone --depth 1 https://github.com/LineageOS/android_external_openssh.git external/openssh
//进入目录,完成编译
cd external/openssh
mm
在编译完成之后,我们只需要将ssh产物(即ssh client)放至安卓设备中即可,接下来就是进行具体的配置。
基于SSH实现隧道其实非常简单,参考以下命令即可:
#命令行
ssh -p SSH_PORT -N -o ExitOnForwardFailure=yes -R REMOTE_IP:REMOTE_PORT:LOCAL_IP:ADB_PORT SSH_USER_NAME@SERVER_IP
#使用示例
ssh -p 46325 -N -o ExitOnForwardFailure=yes -R 0.0.0.0:50001:127.0.0.1:5555 [email protected]
简单说明以上命令的含义:
-p SSH_PORT:用于指定SSH连接端口,SSH默认端口为22端口,如果有改动SSH配置中关于端口的部分,则需要通过-p选项进行指定;
-N:只建立隧道,不执行远程命令;
-o ExitOnForwardFailure=yes:设置SSH连接行为,即转发失败时退出;
-R REMOTE_IP:REMOTE_PORT:LOCAL_IP:ADB_PORT SSH_USER_NAME@SERVER_IP:建立反向端口转发,REMOTE_IP即远程监听地址,这里设置0.0.0.0为远程服务器所有的IPv4网卡监听地址,REMOTE_PORT是远程监听端口;LOCAL_IP为本地目标地址,这里选择127.0.0.1也就是本机(安卓设备)的地址,而ADB_PORT则是无线ADB TCP监听的端口,通常也就是5555端口;
为了确保SSH隧道正常建立,还要在服务端进行配置,这里我们需要编辑sshd_config配置:
// vim /etc/ssh/sshd_config
# 允许 TCP 端口转发
AllowTcpForwarding yes
# 允许客户端指定反向转发监听地址
GatewayPorts clientspecified
# 允许远程转发
PermitListen any
# 保持连接
ClientAliveInterval 60
ClientAliveCountMax 3
除了sshd_config配置,我们还需要修改服务器的防火墙,将SSH端口以及REMOTE_PORT端口放行:
//ufw 配置
ufw allow REMOTE_PORT
ufw allow SSH_PORT
经过以上配置,应该能够正常建立SSH隧道。此时我们就将远端设备的无线ADB调试端口暴露在了公网中,我们可以在PC上通过以下命令来进行无线ADB调试:
adb connect SERVER_IP:REMOTE_PORT
经过上述的配置,我们现在可以将安卓设备暴露给远端的PC设备,从而进行远程调试了。但当前的配置下会存在一个问题,那就是每次还需要手动执行命令来建立SSH隧道,一旦命令行窗口关掉或者中途退出则隧道关闭,无法再进行调试。
那有没有办法在安卓设备开机之后就自动建立SSH隧道呢,方法自然是有的,这里我们可以通过在系统启动时通过Shell脚本去启动SSH隧道。
Shell脚本参考如下:
// /system/bin/start-ssh-reverse
#!/system/bin/sh
# Reverse tunnel: remote R_BIND -> local L_TARGET. Password via SSH_ASKPASS. Retry until stopped.
umask 077
BIN=/system/bin
SYSCONF=/system/etc/ssh/reverse.conf
DATADIR=/data/ssh
DATACONF="$DATADIR/reverse.conf"
HOME_DIR="$DATADIR/root"
ASKPASS="$BIN/ssh-reverse-askpass"
mkdir -p "$HOME_DIR/.ssh"
chmod 700 "$DATADIR" "$HOME_DIR" "$HOME_DIR/.ssh"
if [ ! -f "$DATACONF" ]; then
if [ ! -f "$SYSCONF" ]; then
echo "start-ssh-reverse: missing $SYSCONF" >&2
exit 1
fi
cp "$SYSCONF" "$DATACONF"
chmod 600 "$DATACONF"
fi
# shellcheck disable=SC1090
. "$DATACONF"
LOGFILE="${LOGFILE:-/data/ssh/reverse.log}"
LOG_MAX="${LOG_MAX:-524288}"
rotate_log() {
[ -f "$LOGFILE" ] || return 0
sz=$(wc -c < "$LOGFILE" 2>/dev/null || echo 0)
[ "$sz" -ge "$LOG_MAX" ] 2>/dev/null || return 0
mv "$LOGFILE" "${LOGFILE}.old" 2>/dev/null || true
}
log() {
rotate_log
echo "$(date) $*" >> "$LOGFILE" 2>/dev/null || true
}
export HOME="$HOME_DIR"
export SSH_REVERSE_PASSFILE="$PASSFILE"
export SSH_ASKPASS="$ASKPASS"
export SSH_ASKPASS_REQUIRE=force
STOP=0
SSH_PID=
BACKOFF=5
MAX_BACKOFF=60
on_stop() {
log "signal stop pid=$SSH_PID"
STOP=1
if [ -n "$SSH_PID" ]; then
kill "$SSH_PID" 2>/dev/null
wait "$SSH_PID" 2>/dev/null
SSH_PID=
fi
}
trap on_stop TERM INT
log "wrapper start user=$REMOTE_USER host=$REMOTE_HOST port=$REMOTE_PORT"
while [ "$STOP" = "0" ]; do
if [ ! -f "$PASSFILE" ]; then
echo "start-ssh-reverse: missing password file $PASSFILE" >&2
log "missing password file $PASSFILE"
elif [ ! -x "$ASKPASS" ]; then
echo "start-ssh-reverse: missing $ASKPASS" >&2
log "missing $ASKPASS"
else
set -- -v
if [ "${DEBUG:-0}" = "1" ]; then
set -- -vvv
fi
log "ssh start -N $REMOTE_USER@$REMOTE_HOST:$REMOTE_PORT -R $R_BIND:$L_TARGET"
"$BIN/ssh" "$@" -N \
-p "$REMOTE_PORT" \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o TCPKeepAlive=yes \
-o PubkeyAuthentication=no \
-o PreferredAuthentications=password,keyboard-interactive \
-o PasswordAuthentication=yes \
-o NumberOfPasswordPrompts=1 \
-o "UserKnownHostsFile=$KNOWN_HOSTS" \
-R "$R_BIND:$L_TARGET" \
"${REMOTE_USER}@${REMOTE_HOST}" >>"$LOGFILE" 2>&1 &
SSH_PID=$!
log "ssh pid=$SSH_PID"
wait "$SSH_PID"
rc=$?
log "ssh pid=$SSH_PID exit=$rc"
SSH_PID=
fi
[ "$STOP" = "1" ] && break
log "backoff ${BACKOFF}s"
sleep "$BACKOFF"
BACKOFF=$((BACKOFF * 2))
if [ "$BACKOFF" -gt "$MAX_BACKOFF" ]; then
BACKOFF=$MAX_BACKOFF
fi
done
log "wrapper exit"
exit 0
在启动SSH隧道时还可以通过-o TCPKeepAlive=yes参数来确保SSH隧道的稳定性;
除了上述的Shell脚本,我们还需要配置rc,以表init进程在系统启动时执行该脚本:
service ssh-reverse /system/bin/sh /system/bin/start-ssh-reverse
class late_start
user root
group root
seclabel u:r:shell:s0
disabled
on boot
start ssh-reverse
经过上述配置,我们就能实现设备在开机启动时自动建立SSH隧道了。在远端的PC设备也就能通过SSH隧道实现远程调试。总体来说,这整个流程也是比较简单的。不过这个方案不适合传输大数据的无线调试。
如果我们需要scrcpy,可以通过以下命令来降低流量从而提升稳定性:
scrcpy \
--max-fps=15 \
--max-size=960 \
--video-bit-rate=1M \
--no-audio