How to run Java standalone app (with JNI) on Android without creating an apk
source link: https://yrom.net/blog/2023/07/07/run-java-with-jni-app-on-android/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In this week, I found a great POC to run a pure Java standalone app (command line tool, no apk) on Android. But what about running a standalone application using JNI (with .so files) on Android like this?
Java app with JNI
Imagine there is a Java program that loads the JNI shared native library to run and use some Android APIs :
package com.example;
import android.os.Build;
import android.util.Log;
public class Helloworld {
static { System.loadLibrary("hello"); }
public static native String stringFromJNI();
public static void main(String[] args) {
Log.i("@@", "Hello world, " + Build.MANUFACTURER + " "+ Build.MODEL + "!");
Log.i("@@", stringFromJNI());
System.out.println(stringFromJNI());
System.out.println("DONE.");
}
public static String getBuildVersion() {
return Build.VERSION.RELEASE;
}
}
…the JNI source would be like:
// ... emit codes
JNIEXPORT jstring JNICALL
Java_com_example_Helloworld_stringFromJNI(JNIEnv *env,
jobject thiz)
{
// ... emit codes
jmethodID versionFunc = (*env)->GetStaticMethodID(env, clz, "getBuildVersion", "()Ljava/lang/String;");
jstring buildVersion = (*env)->CallStaticObjectMethod(env, clz, versionFunc);
const char *version = (*env)->GetStringUTFChars(env, buildVersion, NULL);
if (!version)
{
LOGE("Unable to get version string");
}
else
{
LOGI("Build Version - %s\n", version);
(*env)->ReleaseStringUTFChars(env, buildVersion, version);
}
(*env)->DeleteLocalRef(env, buildVersion);
return (*env)->NewStringUTF(env,
"Hello from JNI ! Compiled with ABI " ABI ".");
}
// ...
The working directory structure
.
├── Helloworld.java
└── hello-jni.c
Compile and deploy
Now we need to compile both the Java and C sources for Android.
Using javac
and dx
to compile for a jar file which Android can read:
export BUILD_DIR=$PWD/build
export JARFILE=helloworld.jar
export JAVAC_OPTS=-source 1.8 -target 1.8 -cp .:$ANDROID_HOME/platforms/android-30/android.jar
# Compile .java to .class
javac $JAVAC_OPTS -d $BUILD_DIR/classes Helloworld.java
# Convert .class file into a dex file and embedded in a jar file
$ANDROID_HOME/build-tools/30.0.2/dx --output=$BUILD_DIR/$JARFILE --dex ./$BUILD_DIR/classes
Cross-compile the C to Android shared native library via NDK:
# Using the prebuilt toolchain diretly
# See https://developer.android.com/ndk/guides/other_build_systems
export ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
$ANDROID_NDK_STANDALONE/bin/clang \
--target=aarch64-none-linux-android21 \
--gcc-toolchain=$ANDROID_NDK_STANDALONE \
--sysroot $ANDROID_NDK_STANDALONE/sysroot \
-L${ANDROID_NDK_STANDALONE}/sysroot/usr/lib \
-shared -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables \
-fstack-protector-strong -no-canonical-prefixes -fno-addrsig -fPIC \
-Wl,-llog \
-Wl,-soname,libhello.so \
-o $BUILD_DIR/libhello.so hello-jni.c
The build directory looks like:
build
├── classes
│ └── com
│ └── example
│ └── Helloworld.class
├── helloworld.jar
└── libhello.so
Using the adb
tool to deploy the helloworld.jar
and libhello.so
to Android device:
adb shell mkdir /data/local/tmp/helloworld
adb push $BUILD_DIR/helloworld.jar $BUILD_DIR/libhello.so /data/local/tmp/helloworld/
Run appliation on Android
Run helloworld.jar
via app_process
on Android:
adb shell CLASSPATH="/data/local/tmp/helloworld/helloworld.jar" \
LD_LIBRARY_PATH=/data/local/tmp/helloworld \
app_process \
/data/local/tmp/helloworld \
com.example.Helloworld
# output
Hello from JNI ! Compiled with ABI arm64-v8a.
DONE.
Logging in adb logcat
:
07-06 07:57:38.911 21599 21599 D AndroidRuntime: >>>>>> START com.android.internal.os.RuntimeInit uid 2000 <<<<<<
07-06 07:57:38.915 21599 21599 I AndroidRuntime: Using default boot image
07-06 07:57:39.025 21599 21599 D AndroidRuntime: Calling main entry com.example.Helloworld
07-06 07:57:39.027 21599 21599 I @@ : Hello world, vivo V2219A!
07-06 07:57:39.027 21599 21599 I hello-jni: Build Version - 12
07-06 07:57:39.027 21599 21599 I @@ : Hello from JNI ! Compiled with ABI arm64-v8a.
07-06 07:57:39.027 21599 21599 I hello-jni: Build Version - 12
07-06 07:57:39.028 21599 21599 D AndroidRuntime: Shutting down VM
Startup shell script
We can create a startup shell script for this tool:
#!/system/bin/sh
HERE="$(cd "$(dirname "$0")" && pwd)"
export CLASSPATH=$HERE/helloworld.jar
export ANDROID_DATA=$HERE
export LD_LIBRARY_PATH="$HERE"
if [ -f "$HERE/libc++_shared.so" ]; then
# Workaround for https://github.com/android-ndk/ndk/issues/988.
export LD_PRELOAD="$HERE/libc++_shared.so"
fi
echo "try com.example.Helloworld with LD_LIBRARY_PATH=${LD_LIBRARY_PATH}"
cmd="app_process $HERE com.example.Helloworld $@"
echo "run: $cmd"
exec $cmd
Push the script to Android device:
adb push helloworld /data/local/tmp/helloworld/
# executable
adb shell chmod a+x /data/local/tmp/helloworld/helloworld
Execute the shell script via adb shell
:
adb shell /data/local/tmp/helloworld/helloworld
Makefile
We can bundle the compile and deploy commands into a convenient Makefile :
BUILD_DIR=build
JAVAC_OPTS=-source 1.8 -target 1.8 -cp .:$(ANDROID_HOME)/platforms/android-30/android.jar
APP_PROCESS=app_process
JARFILE=helloworld.jar
ifeq ($(ARCH),arm)
TARGET=--target=armv7-none-linux-androideabi19 -march=armv7-a -mfpu=vfpv3-d16
APP_PROCESS=app_process32
else
TARGET=--target=aarch64-none-linux-android21
endif
$(BUILD_DIR)/$(JARFILE) : Helloworld.java
test -d $(BUILD_DIR) || mkdir $(BUILD_DIR)
$(JAVA_HOME)/bin/javac $(JAVAC_OPTS) -d $(BUILD_DIR)/classes Helloworld.java
$(ANDROID_HOME)/build-tools/30.0.2/dx --output=$(BUILD_DIR)/$(JARFILE) --dex ./$(BUILD_DIR)/classes
$(BUILD_DIR)/libhello.so : hello-jni.c
test -d $(BUILD_DIR) || mkdir $(BUILD_DIR)
$(ANDROID_NDK_STANDALONE)/bin/clang \
$(TARGET) --gcc-toolchain=$(ANDROID_NDK_STANDALONE) \
--sysroot $(ANDROID_NDK_STANDALONE)/sysroot \
-L$(ANDROID_NDK_STANDALONE)/sysroot/usr/lib \
-shared -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables \
-fstack-protector-strong -no-canonical-prefixes -fno-addrsig -fPIC \
$(CFLAGS) -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libatomic.a \
-Wl,--build-id -Wl,--warn-shared-textrel \
-Wl,--no-undefined -Wl,--as-needed \
$(LINKFLAGS) -Wl,-llog \
-Wl,-soname,libhello.so \
-o $(BUILD_DIR)/libhello.so hello-jni.c
all: $(BUILD_DIR)/$(JARFILE) $(BUILD_DIR)/libhello.so helloworld
.PHONY : clean deploy
deploy : all
adb shell mkdir /data/local/tmp/helloworld
adb push --sync $(BUILD_DIR)/$(JARFILE) $(BUILD_DIR)/libhello.so helloworld /data/local/tmp/helloworld/
adb shell chmod a+x /data/local/tmp/helloworld/helloworld
$(info adb shell /data/local/tmp/helloworld/helloworld)
clean :
test -d $(BUILD_DIR) && rm -rf $(BUILD_DIR) || true
adb shell -n "test -d /data/local/tmp/helloworld && rm -rf /data/local/tmp/helloworld || true"
Using make
for compile all targets:
ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64 \
make all
Complete codes
You can get the complete codes in my git repo: https://github.com/yrom/sample-android-java-standalone-tool
git clone https://github.com/yrom/sample-android-java-standalone-tool.git
cd sample-android-java-standalone-tool
ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$HOST_TAG \
make all
…the HOST_TAG:
NDK host | HOST_TAG |
---|---|
macOS | darwin-x86_64 |
Linux | linux-x86_64 |
64bit Windows | windows-x86_64 |
(from https://developer.android.com/ndk/guides/other_build_systems#overview)
Conclusion:
- Compile Java sources to an Android dex jar file via
javac
anddx
- Compile C (or C++) sources to shared native library via
ndk
- Push complete .jar file and .so file(s) to Android device under
/data/local/tmp
- Run the main class via
app_prcoess
on Android device fromadb shell
References:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK