Android NDK 示例-返回字符串,数组,Java对象;兼容性问题

本文最后更新于:2023年4月15日 下午

Android Studio 2.2.3 创建工程 NDKProj

工程准备

SmartAlgorithm.java中加载了库文件

1
2
3
4
5
6
java
`-- com
`-- rustfisher
`-- ndkproj
|-- MainActivity.java
`-- SmartAlgorithm.java

JNI目录,需要mk文件,头文件和源文件。这里头文件和源文件故意不统一文件名,也可实现效果。
但还是建议用同样的文件名,方便定位。
1
2
3
4
5
jni/
|-- Android.mk
|-- Application.mk
|-- com_rustfisher_ndkproj_SmartAlgorithm.h
`-- com_rustfisher_ndkproj_SmartAlgorithm_if_not_the_same.cpp

NDK返回值

加载SmartAlgorithm;这个是统一标示。LOCAL_MODULE 与 APP_MODULES 均为此标示。
NDK中的方法要声明为native。

1
2
3
4
5
6
7
8
9
10
11
package com.rustfisher.ndkproj;

public class SmartAlgorithm {

static {
System.loadLibrary("SmartAlgorithm");
}

public native String getMsg();
public native int add(int a,int b);
}

注意,Java文件生成头文件后,Java文件的路径不能轻易改动。

编写Android.mk文件;ABI 选择all,编译出支持多个平台的so文件。
填入源文件的文件名。

1
2
3
4
5
6
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := SmartAlgorithm
TARGET_ARCH_ABI := all
LOCAL_SRC_FILES := com_rustfisher_ndkproj_SmartAlgorithm_if_not_the_same.cpp
include $(BUILD_SHARED_LIBRARY)

编写Application.mk文件(网上copy来的)。同样ABI 选择all。

1
2
3
4
5
6
7
APP_PLATFORM := android-16
APP_MODULES := SmartAlgorithm
APP_ABI := all
APP_STL := stlport_static
APP_CPPFLAGS += -fexceptions
# for using c++ features,you need to enable these in your Makefile
APP_CPP_FEATURES += exceptions rtti

修改工程build.gradle文件,添加jni的配置。

1
2
3
4
5
6
sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/libs']// 指定so库的位置
}
}

编译出头文件,得到 com_rustfisher_ndkproj_SmartAlgorithm.h

1
2
Administrator@rust-PC /cygdrive/g/rust_proj/NDKProj/app/src/main/java
javah com.rustfisher.ndkproj.SmartAlgorithm

将头文件放到jni目录下,与源文件一起。

生成的头文件不要手动去修改,直接使用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_rustfisher_ndkproj_SmartAlgorithm */

#ifndef _Included_com_rustfisher_ndkproj_SmartAlgorithm
#define _Included_com_rustfisher_ndkproj_SmartAlgorithm
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_rustfisher_ndkproj_SmartAlgorithm
* Method: getMsg
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_getMsg
(JNIEnv *, jobject);

/*
* Class: com_rustfisher_ndkproj_SmartAlgorithm
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_add
(JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

编写源文件,实现头文件中的方法。一个是返回字符串,一个是加法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <jni.h>
#include <string.h>
#include <android/log.h>

#include "com_rustfisher_ndkproj_SmartAlgorithm.h"

/* Already define in com_rustfisher_ndkproj_SmartAlgorithm.h, no need to extern C here.
extern "C" {
JNIEXPORT jstring JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_getMsg(JNIEnv *env, jobject obj);
JNIEXPORT jint JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_add(JNIEnv *env, jobject obj, jint a, jint b);
};*/

JNIEXPORT jstring JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_getMsg(JNIEnv *env, jobject obj) {

return env->NewStringUTF("Hello from the JNI.");
}

JNIEXPORT jint JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_add(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}//*

然后在命令行 ndk-build。这里是win7下的Cygwin。

1
2
3
4
5
6
Administrator@rust-PC /cygdrive/g/rust_proj/NDKProj/app/src/main/jni
$ ndk-build.cmd
[all] Compile++ : SmartAlgorithm <= com_rustfisher_ndkproj_SmartAlgorithm_if_not_the_same.cpp
[all] SharedLibrary : libSmartAlgorithm.so
[all] Install : libSmartAlgorithm.so => libs/arm64-v8a/libSmartAlgorithm.so
# ...... 后面还有很多

在libs目录下出现了对应的so库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Administrator@rust-PC /cygdrive/g/rust_proj/NDKProj/app/src/main/libs
$ tree
.
|-- arm64-v8a
| `-- libSmartAlgorithm.so
|-- armeabi
| `-- libSmartAlgorithm.so
|-- armeabi-v7a
| `-- libSmartAlgorithm.so
|-- mips
| `-- libSmartAlgorithm.so
|-- mips64
| `-- libSmartAlgorithm.so
|-- x86
| `-- libSmartAlgorithm.so
`-- x86_64
`-- libSmartAlgorithm.so

7 directories, 7 files

在MainActivity中调用这两个方法。
运行apk到机器上,查看log。发现调用成功。

1
2
com.rustfisher.ndkproj D/MainActivity: onCreate: Hello from the JNI.
com.rustfisher.ndkproj D/MainActivity: onCreate: 3

处理数组的方法

1.不要直接操作输入的数组;
2.注意释放本地引用,防止溢出。

1
public native short[] getConvertedArray(short[] data, int dataLen);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JNIEXPORT jshortArray JNICALL Java_com_rustfisher_ndkproj_SmartAlgorithm_getConvertedArray(JNIEnv *env, jobject obj, jshortArray input, jint len) {
jshort* inputPtr;
inputPtr = env->GetShortArrayElements(input,0);// 直接操作指针会改变Android Dalvik中的值
jshort* resPtr;
jshortArray result;
result = env->NewShortArray(len);// 创建新的数组
resPtr = env->GetShortArrayElements(result,0);// 指针

for(jint i = 0;i < len;i++) {
resPtr[i] = inputPtr[i] * 2;
}
env->ReleaseShortArrayElements(input, inputPtr, 0);// 释放本地引用
env->SetShortArrayRegion(result,0,len,resPtr); // 存入数据
env->ReleaseShortArrayElements(result, resPtr, 0); // 释放本地引用
return result;// 返回结果
}

JNI层 unsigned char 与 jbyte 数组转换

本例说明的是unsigned char 与 jbyte之间互相转换
注意方法:(*env)->SetByteArrayRegion(env, jbyte_arr, 0, len, uc_ptr);
java代码

1
2
3
4
5
6
7
8
9
10
public byte[] getByteArrayFromJNI() {
return nativeGetByteArray();
}
public byte[] byteArrayTravelJNI(byte[] input) {
return nativeSendByteArray(input, input.length);
}
private native byte[] nativeGetByteArray(); // 从JNI中获取byte数组

// 输入byte数组,在JNI中转换后再获取回来
private native byte[] nativeSendByteArray(byte[] input, int len);

JNI C代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// return byte array from unsigned char array.  jbytes:  1 2 0 7f 80 81 ff 0 1
JNIEXPORT jbyteArray JNICALL Java_com_rustfisher_ndkalgo_NDKUtils_nativeGetByteArray(JNIEnv *env, jobject jObj)
{
unsigned char uc_arr[] = {1, -2, 0, 127, 128, 129, 255, 256, 257};
int uc_arr_len = sizeof(uc_arr) / sizeof(uc_arr[0]);
jbyte byte_array[uc_arr_len];
int i = 0;
for(;i < uc_arr_len; i++) {
byte_array[i] = uc_arr[i];
}
jbyteArray jbyte_arr = (*env)->NewByteArray(env, uc_arr_len);
(*env)->SetByteArrayRegion(env, jbyte_arr, 0, uc_arr_len, byte_array);
return jbyte_arr;
}

// jbyte -> unsigned char -> jbyte
JNIEXPORT jbyteArray JNICALL Java_com_rustfisher_ndkalgo_NDKUtils_nativeSendByteArray
(JNIEnv *env, jobject jObj, jbyteArray input_byte_arr, jint input_len)
{
int len = (int)input_len;
jbyte *jbyte_ptr = (*env)->GetByteArrayElements(env, input_byte_arr, 0);

unsigned char *uc_ptr = (unsigned char *)jbyte_ptr;

jbyteArray jbyte_arr = (*env)->NewByteArray(env, len);
jbyte byte_array[input_len];
int i = 0;
for(;i < input_len; i++) {
byte_array[i] = uc_ptr[i];
}
(*env)->SetByteArrayRegion(env, jbyte_arr, 0, len, byte_array);
(*env)->ReleaseByteArrayElements(env, input_byte_arr, jbyte_ptr, 0);
return jbyte_arr;
}

关于SetByteArrayRegion这个方法
方法说明:void SetXxxArrayRegion(JNIEnv *env, jarray array, jint start, jint length, Xxx elems[])
将C数组的元素复制到Java数组中。注意最后一个参数要和前面的对应上。

void ReleaseXxxArrayElements(JNIEnv *env, jarray array, Xxx elems[], jint mode)
通知虚拟机通过GetXxxArrayElements获得的一个指针已经不再需要了。Mode是0,更新数组
元素后释放elems缓存。

在这里遇到过一个bug,同样的代码在armeabi上正常运行,但是到了v7a或v8a平台上就闪退。
使用SetXxxArrayRegion这个方法时,传入的参数一定要和方法名中的Xxx对应上
详细可以参考Core Java中的Java Native和Android Develop上关于abi的解释

测试调用

1
2
3
4
5
6
7
8
NDKUtils ndkUtils = new NDKUtils();
byte[] res = ndkUtils.getByteArrayFromJNI(); // 从JNI中获取byte数组
logBytes(res);
Log.d(TAG, "-------------------------------------------------------------");
byte[] inputBytes = new byte[]{1, 2, 127, (byte) 128, (byte) 255, -120};
byte[] tRes = ndkUtils.byteArrayTravelJNI(inputBytes); // 让byte数组在JNI中旅游一圈
logBytes(inputBytes);
logBytes(tRes);

输出

1
2
3
4
bytes:  1 2 0 7f 80 81 ff 0 1 
-------------------------------------------------------------
bytes: 1 2 7f 80 ff 88
bytes: 1 2 7f 80 ff 88

直接操作输入的数组

以int数组为例
输入一个数组后,获取数组然后直接改变数组中的元素,最后释放掉本地引用

1
2
3
4
5
6
JNIEXPORT void JNICALL Java_com_rustfisher_ndkalgo_NDKUtils_nativeModifyArray
(JNIEnv *env, jobject jObj, jintArray input_arr, jint input_len) {
int * input_ptr = (*env)->GetIntArrayElements(env, input_arr, 0);
input_ptr[input_len - 1] = input_ptr[input_len - 1] - 1;
(*env)->ReleaseIntArrayElements(env, input_arr, input_ptr, 0);
}
1
2
3
4
5
NDKUtils moUtil = new NDKUtils();
int[] origin = new int[]{1, 2, 3, 4, 5, 6, 7};
Log.d(TAG, "origin before: " + Arrays.toString(origin));
moUtil.modifyArray(origin);
Log.d(TAG, "origin after: " + Arrays.toString(origin));

观察输出可以看出,输入的数组直接被改变了

1
2
origin before: [1, 2, 3, 4, 5, 6, 7]
origin after: [1, 2, 3, 4, 5, 6, 6]

或者

1
2
3
4
5
6
7
8
9
10
JNIEXPORT void JNICALL Java_com_rustfisher_face_1detect_1lib_CalHelper_cvtNV21
(JNIEnv *env, jclass jcls, jbyteArray input_arr,jint in_arr_len,
jbyteArray target_arr, jint nv21_size, jint y_size, jint yuv_gap) {
jbyte *in_ptr = env->GetByteArrayElements(input_arr, false);
jbyte *target_ptr = env->GetByteArrayElements(target_arr, false);
for(int i = y_size; i < nv21_size; i+=2){
target_ptr[i] = in_ptr[i + yuv_gap + 1];
target_ptr[i + 1] = in_ptr[i + yuv_gap];
}
}

返回Java对象

NDK中可以创建Java对象并返回。
例如我们新建一个JavaUser类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JavaUser {
private int age;
private String name;

public JavaUser(int age, String name) {
this.age = age;
this.name = name;
}

public int getAge() {
return age;
}

public String getName() {
return name;
}

@Override
public String toString() {
return name + ", " + age;
}
}

native方法返回一个JavaUser对象

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NDKUtils {

static {
System.loadLibrary("NDKMan");
}

public JavaUser createUser(int age, String name) {
return nativeGetUser(age, name);
}

private native JavaUser nativeGetUser(int age, String name);

}

c文件实现代码。注意参数签名的写法,要参照标准。

1
2
3
4
5
6
7
JNIEXPORT jobject JNICALL Java_com_rustfisher_ndkalgo_NDKUtils_nativeGetUser
(JNIEnv *env, jobject jObj, jint age, jstring name)
{
jclass userClass = (*env)->FindClass(env, "com/rustfisher/ndkalgo/JavaUser");
jmethodID userConstruct = (*env)->GetMethodID(env, userClass, "<init>", "(ILjava/lang/String;)V");
return (*env)->NewObject(env, userClass, userConstruct, age, name);
}

调用native方法生成对象

1
2
3
4
5
private void testJavaUserNDK() {
NDKUtils ndkUtils = new NDKUtils();
JavaUser tom = ndkUtils.createUser(20, "Tom");
Log.d(TAG, tom.toString());
}

NDK兼容性问题

Vivo x6plus 兼容性问题。Vivo x6plus 打开Parrot界面即崩溃。但是Parrot官方APP能够正常使用。
我自己的so库与Parrot的so库不兼容,出现

1
2
3
4
5
6
7
java.lang.UnsatisfiedLinkError:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.xx.xx.xxx-1/base.apk"],nativeLibraryDirectories=[/data/app/com.xx.xx.xxx-1/lib/arm64, /vendor/lib64, /system/lib64]]] couldn't find "libjson.so"
at java.lang.Runtime.loadLibrary(Runtime.java:366)
at java.lang.System.loadLibrary(System.java:988)
at com.parrot.arsdk.ARSDK.loadSDKLibs(ARSDK.java:20)
at com.parrot.sdk.activity.DronesListActivity.<clinit>(DronesListActivity.java:44)
at java.lang.reflect.Constructor.newInstance(Native Method)

分析处理兼容性问题

将Parrot官方apk解包后,找到so库文件。发现只有x86、mips、armeabi_v7a、armeabi 这4个。
而我加载了有更多的库。

将我自己的so文件删除至只剩下Parrot那4个即可。

Android.mk
TARGET_ARCH_ABI := x86 mips armeabi armeabi-v7a

同名so文件引起UnsatisfiedLinkError

主工程app中带有C工程与so文件。现需要将所有的C工程移到新的模块mylib中。

新建模块mylib,将C工程复制进来。gradle中配置jni,因为修改了文件路径,重新生成头文件并修改cpp文件。
在模块中进行ndk-build,获得so库。

安装运行app,出现UnsatisfiedLinkError:

1
2
java.lang.UnsatisfiedLinkError: No implementation found for void com.xx.jni.MyJNI.init(java.lang.String) 
(tried Java_com_xx_jni_MyJNI_init and Java_com_xx_jni_MyJNI_init__Ljava_lang_String_2)

分析原因,app能够正常加载库文件,但未找到实现方法。app使用的so库,究竟是不是我们指定的那个。

尝试进行修复,原app工程的Android.mk

1
LOCAL_MODULE := main

移动到模块后,新的Android.mk修改为
1
LOCAL_MODULE := mynewmain

库改了名字后,修改Java代码
1
2
3
static {
System.loadLibrary("mynewmain");
}

重装app即可正常使用。

经过分析与尝试,删除原app工程中所有的so文件,再次重装app即可正常运行。不需要修改so库的名字。

错误原因猜想:app主工程与模块mylib中有同名的so文件,安装app时会优先使用app主工程中的so库。

jstring转为char的方法 jstring -> char

jstring转为char的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char *jstringToChar(JNIEnv *env, jstring jstr) {
char *rtn = NULL;
jclass clsstring = env->FindClass("java/lang/String");
jstring strencode = env->NewStringUTF("GB2312");
jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid, strencode);
jsize alen = env->GetArrayLength(barr);
jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
rtn = (char *) malloc(alen + 1);
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);
return rtn;
}

参考 JNI中string 、 char* 和 jstring 两种转换 - CSDN xlxxcc


Android NDK 示例-返回字符串,数组,Java对象;兼容性问题
https://blog.rustfisher.com/2016/08/02/Android/NDK-use_sample_1/
作者
Rust Fisher
发布于
2016年8月2日
许可协议