Android NDK 示例-返回字符串,数组,Java对象;兼容性问题
本文最后更新于:2023年4月15日 下午
Android Studio 2.2.3 创建工程 NDKProj
工程准备
在SmartAlgorithm.java
中加载了库文件1
2
3
4
5
6java
`-- com
`-- rustfisher
`-- ndkproj
|-- MainActivity.java
`-- SmartAlgorithm.java
JNI目录,需要mk文件,头文件和源文件。这里头文件和源文件故意不统一文件名,也可实现效果。
但还是建议用同样的文件名,方便定位。1
2
3
4
5jni/
|-- 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
11package 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
6LOCAL_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
7APP_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
6sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/libs']// 指定so库的位置
}
}
编译出头文件,得到 com_rustfisher_ndkproj_SmartAlgorithm.h
1 |
|
将头文件放到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
6Administrator@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
19Administrator@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 |
|
处理数组的方法
1.不要直接操作输入的数组;
2.注意释放本地引用,防止溢出。
1 |
|
1 |
|
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
10public 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
8NDKUtils 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
4bytes: 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 |
|
1 |
|
观察输出可以看出,输入的数组直接被改变了1
2origin 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
10JNIEXPORT 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
22public 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
13public 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
7JNIEXPORT 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
5private 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 |
|
分析处理兼容性问题
将Parrot官方apk解包后,找到so库文件。发现只有x86、mips、armeabi_v7a、armeabi 这4个。
而我加载了有更多的库。
将我自己的so文件删除至只剩下Parrot那4个即可。
Android.mkTARGET_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
2java.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
3static {
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
16char *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;
}