教程 · 2021年11月1日 0

在 Android 手机上运行任意 Java 程序

内容纲要

前言

众所周知,Android 手机底层是 Linux 内核,这就给了这个话题折腾的机会。虽然 Java 官方没有发布 Android 版的 JDK,但是一直有 aarch64/armhf 版本的,而我们的 Android 手机一般也是这个架构。

准备工作

下面的步骤假定你的手机是 aarch64 架构,其他架构只需下载对应版本的文件。

  • 了解一定的 Linux 和 Android 基础
  • Patch ELF 下载地址 (aarch64-linux-android-patchelf)
  • JDK aarch64 下载地址 (下载 Linux 平台 aarch64 架构的 JDK)
  • glibc 动态链接库 下载地址 (下载 libc6_x.x-0ubuntu3_arm64.deb,解包提取lib/aarch64-linux-gnu 目录下的所有文件,或者从其他发行版镜像中提取都可以,如果不知道如何提取的话直接下载附件)
  • 如果手机上没有 xxd realpath file unzip tar 等命令的话可以下载一个 busybox

附件:
glibc 动态运行库

安装

提示:下文的命令请不要直接复制粘贴,至少把版本号和路径改成你自己的。

这里通过 usb 调试的方式连接手机,方便直接在手机上执行命令,你也可以用其他方式,然后直接使用手机上的终端模拟器。

将文件拷贝到手机:

adb push OpenJDK8U-jdk_aarch64_linux_hotspot_8u312b07.tar.gz /sdcard/
adb push aarch64-linux-gnu.zip /sdcard/
adb push aarch64-linux-android-patchelf /sdcard/

使用 adb shell 或者打开终端模拟器:

cd /data/local/tmp # 不要装在 sdcard 下,那里无法赋予执行权限,如果使用终端模拟器,一般会有自己的家目录,或者直接进入 Android 给应用分配的独立空间中。
mv /sdcard/OpenJDK8U-jdk_aarch64_linux_hotspot_8u312b07.tar.gz ./
mv /sdcard/aarch64-linux-gnu.zip ./
mv /sdcard/aarch64-linux-android-patchelf ./patchelf
mkdir libs
unzip -c libs aarch64-linux-gnu.zip
tar -zxvf OpenJDK8U-jdk_aarch64_linux_hotspot_8u312b07.tar.gz

这时候执行 java 命令会出现奇怪的问题:

$ ./jdk8u312-b07/bin/java
/system/bin/sh: ./jdk8u312-b07/bin/java: No such file or directory

这是因为原本的 JDK 是为通用 Linux 发行版编译的,这些发行版一般会在 /lib 目录下有它需要的动态链接库文件,但是 Android 上没有,所以就报错找不到文件。可以使用 file 命令验证这一点。

$ file ./jdk8u312-b07/bin/java
./jdk8u312-b07/bin/java: ELF shared object, 64-bit LSB arm64, dynamic (/lib/ld-linux-aarch64.so.1), not stripped

这样的话可以下载这些动态运行库,然后修改 ELF 文件的链接位置就可以使用了。

chmod +x patchelf
./patchelf --set-interpreter /data/local/tmp/libs/ld-linux-aarch64.so.1 ./jdk8u312-b07/bin/java # 链接库必须是绝对路径

这个时候仍然是没办法执行的,会提示如下信息:

$ ./jdk8u312-b07/bin/java
./jdk8u312-b07/bin/java: error while loading shared libraries: libpthread.so.0: cannot open shared object file: No such file or directory

由于 Java 使用的运行库与 Android 不完全兼容,我们需要使用 GNU 标准的库,按照文档,可以通过 /etc/ld.so.conf 配置文件和 LD_LIBRARY_PATH 环境变量来指定搜索路径,但是由于 Android 文件系统的特殊性,我们只能使用环境变量的方式了。

$ export LD_LIBRARY_PATH=/data/local/tmp/libs
$ export JAVA_HOME=/data/local/tmp/jdk8u312-b07
$ export PATH="$JAVA_HOME/bin:$PATH"
$ java -version
openjdk version "1.8.0_312"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_312-b07)
OpenJDK 64-Bit Server VM (Temurin)(build 25.312-b07, mixed mode)

这样 JDK 就安装完毕了。如果需要使用其他的命令,同样的修改 ld.so 的路径即可。我写了一个脚本,可以自动设置 ld.so 的位置,代码如下。

#!/system/bin/sh

if [ ! -d bin ]; then
  echo "请在 JDK 目录下执行此脚本" >&2
  exit 1
fi

if [ -z "$PATCH_ELF_EXEC" ]; then
  PATCH_ELF_EXEC=patchelf
fi

"$PATCH_ELF_EXEC" --version > /dev/null 2>&1

if [ $? -gt 0 ]; then
  echo "无法执行 patchelf" >&2
  echo "PATCH_ELF_EXEC=$PATCH_ELF_EXEC" >&2
  exit 2
fi

if [ -z "$XXD_EXEC" ]; then
  XXD_EXEC=xxd
fi

"$XXD_EXEC" --version > /dev/null 2>&1

if [ $? -gt 0 ]; then
  echo "无法执行 xxd" >&2
  echo "XXD_EXEC=$XXD_EXEC" >&2
  exit 3
fi

if [ -z "$LD_SO" ]; then
  LD_SO="$1"
fi

# 如果没有 realpath 可以去掉这一行,只要保证输入的参数是绝对路径即可。
LD_SO=$(realpath "$LD_SO")

if [ ! -f "$LD_SO" ]; then
  echo "用法: $0 [ld-linux-aarch64.so 的路径]" >&2
  exit 4
fi

ls -1 bin | while read line
do
  HEX=$(xxd -p -l 4 "bin/$line")
  if [ -x "bin/$line" ] && [ "$HEX" == "7f454c46" ]; then
    "$PATCH_ELF_EXEC" --set-interpreter "$LD_SO" "bin/$line"
  fi
done

另外,这样移植的 Java 可能会遇到无法解析 DNS 的问题,如果遇到一直无法解析域名的问题的话需要加上几个属性才行。

# 可以使用 -Dxxx JVM 参数
java -Dsun.net.spi.nameservice.nameservers=119.29.29.29 -Dsun.net.spi.nameservice.provider.1=dns,sun

运行 Web 项目

由于 Android 没有 HOME 目录,也没有 /tmp 目录,所以正常参数是无法运行的,需要加一些额外的参数。

# 首先,定义一个 HOME 变量,用来存放 maven 仓库
mkdir /sdcard/home
export HOME=/sdcard/home

# 创建新的 Spring Boot Web 项目,写一个 Hello World(创建和修改代码略过)
cd demo
./mvnw -Dusr.home=$HOME -Dsun.net.spi.nameservice.nameservers=119.29.29.29 -Dsun.net.spi.nameservice.provider.1=dns,sun package

# 定义临时目录
mkdir /sdcard/tmp
# 运行
java -Duser.home=$HOME -Djava.io.tmpdir=/sdcard/tmp -jar target/demo-0.0.1-SNAPSHOT.jar

运行效果如下:
构建截图

运行截图

访问截图