Android-NDK开发之JNI缘起

NDK,既原生开发套件,是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码。

  • Android 原生开发套件 (NDK):这套工具使您能在 Android 应用中使用 C 和 C++ 代码。
  • CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。强烈建议使用Cmake。
  • LLDB:Android Studio 用于调试原生代码的调试程序。

这篇文章不是入门文章,Android有详细入门指引。也有其他博客介绍ndk-build和Cmake开发入门。

1、JNI说起

JNI,全称Java NativeInterface,一种本地应用程序标准的应用程序接口。它允许运行在JVM上的代码与C/C++实现交互。

2、类型映射表

Java JNI 签名缩写 说明
boolean jboolean Z 无符号8为整数
byte jbyte B 有符号8位整数
char jchar C 无符号16位整数
short jshort S 有符号16位整数
int jint I 有符号32位整数
long jlong J 有符号64位整数
float jfloat F 32位浮点数
double jdouble D 64位浮点数
void Void V -
String - Ljava/lang/String; L完整包名加类名;
int[] jintArray [I 数组的时候在前面加[

3、本地函数注册

JNI里面函数注册之后,才能被Java或Kotlin调用!对应Java的native关键字修饰的函数或者Kotlin的external关键字修饰的函数。

静态注册(Java代码)

这种方法最常见也最简单,初学者首选。可以用javah -jni 命令直接生成native函数对应的头文件。 比如,有如下Java函数:

1
2
3
4
package com.qfleng.jpeg;  
public class JpegEngine {  
    public static native int[] decodeJpeg(String path, ImageInfo img_info);  
} 

对应的C++静态注册函数如下:

1
2
JNIEXPORT jintArray JNICALL 
Java_com_qfleng_jpeg_JpegEngine_decodeJpeg(JNIEnv *env, jobject obj, jstring path, jobject img_info){ }

由于静态注册函数签名包含了Java包名部分,非常冗长;调用时,搜索效率没有动态注册的效率高;函数量大的时候也不利于管理代码。

动态注册(Kotlin代码)

动态注册,是在JNI_OnLoad函数里面通过RegisterNatives函数实现的。RegisterNatives函数需要传入JNINativeMethod结构体,这其中保存着本地函数和native函数的对应关系。

JNINativeMethod结构体:

1
2
3
4
5
typedef struct {
  const char* name;
  const char* signature;
  void* fnPtr;
} JNINativeMethod;

比如有如下Kotlin函数:

1
2
3
4
package com.qfleng.opencv
object CvUtil{
  private external fun nGaussianBlur(srcAddr: Long, dstAddr: Long, radius: Int)
}

对应动态注册的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
const char *classPathName = "com/qfleng/opencv/CvUtil";

void gaussian_blur(JNIEnv *env, jobject thiz, jlong src_addr, jlong dst_addr, jint radius) {
  Mat &srcMat = *(reinterpret_cast<Mat *>(src_addr));
  Mat &dstMat = *(reinterpret_cast<Mat *>(dst_addr));
  GaussianBlur(srcMat, dstMat, Size(radius, radius), 0, 0);
}

static JNINativeMethod methods[] = {
  {"nGaussianBlur", "(JJI)V", (void *) gaussian_blur},
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
  jclass clazz = NULL;
  JNIEnv *env = NULL;

  if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
    LOGE("JNI_OnLoad->GetEnv error!");
    return -1;
  }

  clazz = env->FindClass(classPathName);
  if (!clazz) {
    LOGE("JNI_OnLoad->FindClass error!");
    return -1;
  }

  if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
    LOGE("JNI_OnLoad->RegisterNatives error!");
    return -1;
  }

  return JNI_VERSION_1_6;
}

动态注册,虽然代码优美、效率也高,但不能命令生成,需要手动注册并管理对应关系。函数签名便是其中重点:
如上面Kotlin代码fun nGaussianBlur(srcAddr: Long, dstAddr: Long, radius: Int) 对应的注册签名如下(JJI)V

jstring和char转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//char*转jstring
jstring charToJString(JNIEnv *env, char *src, size_t len) {
    jbyteArray array = env->NewByteArray(len);
    env->SetByteArrayRegion(array, 0, len, (const jbyte *) src);
    jstring strEncode = env->NewStringUTF("UTF-8");
    jclass cls = env->FindClass("java/lang/String");
    jmethodID ctor = env->GetMethodID(cls, "<init>", "([BLjava/lang/String;)V");
    jstring object = (jstring) env->NewObject(cls, ctor, array, strEncode);
    return object;
}

//将jstring转换成为UTF-8格式的char*
std::string jstringToString(JNIEnv *env, jstring srcStr) {
    const char *tmpChar = env->GetStringUTFChars(srcStr, JNI_FALSE);
    std::string result(tmpChar);
    env->ReleaseStringUTFChars(srcStr, tmpChar);
    return result;
}

需要特别注意的是,调用GetStringUTFChars后需要调用ReleaseStringUTFChars函数。

其他

Built with Hugo
主题 StackJimmy 设计