0%

Android-NDK开发之JNI

概述

JNI 是 Java Native Interface 的简写,可以译作 Java 原生接口。

JNI 定义了两个关键数据结构,即 JavaVMJNIEnv, 两者本质上都是指向函数表的二级指针。

  • JavaVM: Java 虚拟机在 JNI 层的代表,理论上每个进程可以有多个 JavaVM, 但在 Android 中一个进程只允许有一个 JavaVM。
  • JNIENV: 代表了 Java 在本线程的运行环境,每个线程都有一个。无法在线程之间共享 JNIEnv, 如果一段代码无法通过其他方法获取自己的 JNIEnv,则应该共享相应的 JavaVM,然后使用 GetEnv 发现线程的 JNIEnv。

JNI 中所有线程都是 Linux 线程,由内核调度。线程通常从受管理代码启动(Thread.start()),也可以在其他位置创建,然后附加到 JavaVM。例如,可以使用 AttachCurrentThread() 或 AttachCurrentThreadAsDaemon() 函数附加通过 pthread_create() 或 std::thread 启动的线程。在附加之前,线程不包含任何 JNIEnv,也无法调用 JNI。通常,最好使用 Thread.start() 创建需要调用 Java 代码的任何线程。这样做可以确保您有足够的堆栈空间、属于正确的 ThreadGroup 且与您的 Java 代码使用相同的 ClassLoader。

附加原生创建的线程会构建 java.lang.Thread 对象并将其添加到“主”ThreadGroup,从而使调试程序能够看到它。在已附加的线程上调用 AttachCurrentThread() 属于空操作。通过 JNI 附加的线程在退出之前必须调用 DetachCurrentThread()。

引用

局部引用

通过 NewLocalRef 和各种 JNI 接口创建(FindClass, NewObject, GetObjectClass 和 NewCharArray 等)创建的引用,从 Native 方法返回到 Java 层之后,如果 Java 层没有对返回的局部引用使用的话,局部引用就会被JVM自动释放。释放一个局部引用有两种方式,一个是本地方法执行完毕后 JVM 自动释放,另外一个是自己调用 DeleteLocalRef 手动释放。

JNI 会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。所以在不需要局部引用时可以调用 DeleteLocalRef 手动删除。

全局引用

调用 NewGlobalRef 基于局部引用创建的引用,它会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 DeleteGlobalRef 手动释放。

弱全局引用

调用 NewWeakGlobalRef 基于局部引用或全局引用创建,它不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。在 JVM 认为应该回收它的时候(比如内存紧张时)会进行回收而被释放,或调用 DeleteWeakGlobalRef 手动释放。

JNI函数注册

静态注册

对于如下代码:

1
2
3
4
5
6
7
8
9
10
11
public class Main {
static {
System.loadLibrary("JNIDemo");
}

public static native void hello();

public static void main(String[] args) {
hello();
}
}

执行命令:

1
2
javac Main.java
javah Main

第二个命令会在当前目录中生成 Main.h 文件,如下所示:

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

#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Main
* Method: hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Main_hello
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

然后新建 Main.c 文件:

1
2
3
4
5
6
#include <stdio.h>
#include "Main.h"

JNIEXPORT void JNICALL Java_Main_hello(JNIEnv *env, jclass jc) {
printf("Hello World!\n");
}

接着通过 gcc -I $JAVA_HOME/include/ -I $JAVA_HOME/include/darwin -fPIC -shared -o libJNIDemo.so Main.c 命令生成目标 so 文件,最后执行。

当我们在 Java 中调用 hello 方法时,就会从 JNI 中寻找 Java_Main_hello 方法,如果没有就会报错,如果找到就会为其建立关联,其实是保存 JNI 的方法指针,这样再次调用 hello 方法时就会直接通过这个方法指针来找到对应 JNI 方法。

静态注册就是根据方法名,将 Java 的 native 方法和 JNI 方法建立关联,而 JNI 层的方法名称过长,且初次调用 native 方法时需要建立关联,影响效率。

动态注册

静态注册是 Java 的 native 方法通过方法指针来与 JNI 进行关联,如果 Native 方法知道它在 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <jni.h>
#include <Business/Log/Log.hpp>
#include <mars/xlog/xlogger.h>

static const char *classPath = "com/mail/jni/Log";

void log(JNIEnv *env, jclass type, jint level_, jstring module_, jstring file_, jstring func_,
jint line_, jstring msg_) {
const char *module = env->GetStringUTFChars(module_, NULL);
const char *file = env->GetStringUTFChars(file_, NULL);
const char *func = env->GetStringUTFChars(func_, NULL);
const char *msg = env->GetStringUTFChars(msg_, NULL);

SE_LOG_INIT("/sdcard/", LOG_LEVEL_ALL);
xlogger2((TLogLevel) level_, module, file, func, line_, msg);

env->ReleaseStringUTFChars(module_, module);
env->ReleaseStringUTFChars(msg_, msg);
}

static JNINativeMethod getMethods[] = {
{"log", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V",
(void*) log},
};

// 此函数通过调用JNI中 RegisterNatives 方法来注册我们的函数
static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* getMethods,
int methodsNum){
jclass clazz;
//找到声明native方法的类
clazz = env->FindClass(className);
if(clazz == NULL){
return JNI_FALSE;
}
//注册函数 参数:java类 所要注册的函数数组 注册函数的个数
if(env->RegisterNatives(clazz, getMethods, methodsNum) < 0){
return JNI_FALSE;
}
return JNI_TRUE;
}

static int registerNatives(JNIEnv* env){
//指定类的路径,通过FindClass 方法来找到对应的类
const char* className = classPath;
return registerNativeMethods(env, className, getMethods, sizeof(getMethods)/ sizeof(getMethods[0]));
}

// 回调函数 在这里面注册函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//判断虚拟机状态是否有问题
if(vm->GetEnv((void**)&env, JNI_VERSION_1_6)!= JNI_OK){
return -1;
}
assert(env != NULL);
//开始注册函数 registerNatives -》registerNativeMethods -》env->RegisterNatives
if(!registerNatives(env)){
return -1;
}
//返回jni 的版本
return JNI_VERSION_1_6;
}

JNI数据类型对应

JAVA类型 JNI类型 描述
int jint/jsize signed 32 bits
long jlong signed 64 bits
byte jbyte signed 8 bits
boolean jboolean unsigned 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

Java对象做为引用被传递到本地方法中,所有这些Java对象的引用都有一个共同的父类型jobject(相当于java中的Object类是所有类的父类一样)。

String对象

从 Java 程序中传过去的 String 对象在 JNI 方法中对应的是 jstring 类型,jstring 和 C 中的 char* 不同,如果直接当做 char* 使用会出错。在使用之前需要将 jstring 转换成为 c/c++ 中的 char*:

1
2
3
4
char buf[128];
const char *str = (*env)->GetStringUTFChars(env, /* jstring */prompt, 0);
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);

下面是访问 String 的一些方法:

  • GetStringUTFChars: 将 jstring 转换成为 UTF-8 格式的 char*
  • GetStringChars: 将 jstring 转换成为 Unicode 格式的 char*
  • ReleaseStringUTFChars: 释放指针
  • ReleaseStringChars: 释放指针
  • NewStringUTF: 创建一个 UTF-8 格式的 jstring 对象
  • NewString: 创建一个 Unicode 格式的 jstring 对象
  • GetStringUTFLength: 获取 UTF-8 格式的 char* 的长度
  • GetStringLength: 获取 Unicode 格式的 char* 的长度

Java原始类型数组

1、获取数组的长度

1
2
3
4
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) {
int i, sum = 0;
jsize len = (*env)->GetArrayLength(env, arr);
}

2、获取一个指向数组元素的指针

1
2
// 第三个参数是数组里面开始的元素
jint *body = (*env)->GetIntArrayElements(env, arr, 0);

3、使用指针取出Array中的元素

1
2
3
for (i=0; i<len; i++) {
  sum += body[i];
}

4、释放数组元素的引用

1
(*env)->ReleaseIntArrayElements(env, arr, body, 0);
获取函数 释放函数 数组类型
GetBooleanArrayElements ReleaseBooleanArrayElements boolean
GetByteArrayElements ReleaseByteArrayElements byte
GetCharArrayElements ReleaseCharArrayElements char
GetShortArrayElements ReleaseShortArrayElements short
GetIntArrayElements ReleaseIntArrayElements int
GetLongArrayElements ReleaseLongArrayElements long
GetFloatArrayElements ReleaseFloatArrayElements float
GetDoubleArrayElements ReleaseDoubleArrayElements double

Java自定义对象数组

JNI 提供了一组单独的函数来访问对象数组的元素,我们可以使用这些函数来获取和设置单独的对象数组元素。

  • GetObjectArrayElement: 在给定的索引中返回对象元素。
  • SetObjectArrayElement: 在给定索引处更新对象元素。

Java对象

在本地方法中调用Java对象的方法的步骤:

1、获取你需要访问的Java对象的类:

1
jclass cls = (*env)->GetObjectClass(obj);

2、获取MethodID:

1
2
3
4
5
// env-->JNIEnv
// cls-->第一步获取的jclass
// "callback"-->要调用的方法名
// "(I)V"-->方法的Signature
jmethodID mid = (*env)->GetMethodID(cls, "callback", "(I)V");

3、调用方法:

1
2
3
4
5
// env-->JNIEnv
// obj-->通过本地方法穿过来的jobject
// mid-->要调用的MethodID(即第二步获得的MethodID)
// depth-->方法需要的参数(对应方法的需求,添加相应的参数
(*env)->CallVoidMethod(env, obj, mid, depth);

注:这里使用的是 CallVoidMethod 方法调用,因为没有返回值,如果有返回值的话使用对应的方法,在后面会提到。

Java 中参数类型和其对应的签名值如下:

Signature Java中的类型
Z boolean
B byte
C char
S short
I int
L long
F float
D double
V void
L开头, ;结尾, /隔开包和类名, 嵌套类使用$. Ljava/lang/String;Lxx/xx$$Class object对象

一个Java类的方法的 Signature 可以通过 javap 命令获取。

通常我们直接在 methodID 后面将要传的参数添加在后面,但是还有其他的方法也可以传参数:

  • CallVoidMethodV: 可以获取一个数量可变的列表作为参数
  • CallVoidMethodA: 可以获取一个union

调用静态方法:

  • GetStaticMethodID: 获取对应的静态方法的ID
  • CallStaticIntMethod: 调用静态方法

Java对象的属性

访问 Java 对象的属性和访问 Java 对象的方法基本上一样,只需要将函数里面的 Method 改为 Field 即可。

集合对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获得 ArrayList 类引用
jclass list_cls = env->FindClass("Ljava/util/ArrayList;");
if (listcls == NULL) {
// ...
}
// 获得得构造函数Id
jmethodID list_costruct = env->GetMethodID(list_cls , "<init>","()V");
// 创建一个 Arraylist 集合对象
jobject list_obj = env->NewObject(list_cls , list_costruct);
// 获得 Arraylist.add(Object object) 方法 ID
jmethodID list_add = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");
// 获得Student类引用
jclass stu_cls = env->FindClass("Lcom/hearing/jni/Student;");
jmethodID stu_costruct = env->GetMethodID(stu_cls , "<init>", "(ILjava/lang/String;)V");
for(int i = 0; i < 10; i++) {
jstring str = env->NewStringUTF("Hearing");
// 通过调用该对象的构造函数来 new 一个 Student 实例
jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str);
// 执行 add 方法
env->CallBooleanMethod(list_obj , list_add , stu_obj);
}
return list_obj;