JNI与NDK入门之二

概述

在上一篇的文章中,我们学会了在 Java 层如何通过 JNI 来调用 C 层的代码,这一篇文章我们将看一下如何在 C 层控制 Java 层的代码,主要包含以下内容:

  1. 创建 Java 对象
  2. 访问类静态成员域
  3. 调用类的静态方法
  4. 访问 Java 对象的成员变量
  5. 访问 Java 对象的方法

功能描述

我们下面要实现一个 Java 层与 C 层相互调用的混编程序,调用流程如下图:

调用流程图

  1. 整个 demo 从调用 JniFuncMain 类中的 createJniObject() 方法开始。
  2. Java_JniFuncMain_createJniObject() 函数可以通过 JNI 来创建 JniTest 对象、调用该对象的方法以及访问该对象的成员变量。

编写 Java 层代码

JniFuncMain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JniFuncMain {
private static int sIntField = 1;

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

// 使用 static 关键字声明的本地方法,可直接通过类调用
public static native JniTest createJniObject();

public static void main(String[] args) {
System.out.println("[Java] 调用 createJniObject() 方法");
// 不使用 new 运算符,通过 C 函数生成 JniTest 类的对象并返回引用
JniTest jniObj = createJniObject();
// 可直接调用对象的成员方法
jniObj.callTest();
}
}

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class JniTest {
private int mIntField;

public JniTest(int num) {
mIntField = num;
System.out.println("[Java] 调用 JniTest 对象的构造方法,mIntField值为 " + mIntField);
}

public int callByNative(int num) {
num += mIntField;
System.out.println("[Java] 调用 JniTest 对象的 callByNative() 方法,参数值为 " + num);
return num;
}

public void callTest() {
System.out.println("[Java] 调用 JniTest 对象的 callTest() 方法,mIntField值为 " + mIntField);
}
}

生成 JniFuncMain.h 头文件

因为 native 关键字在 JniFuncMain 类中,这里使用前文提到的 javah 命令生成该类的头文件:

生成头文件

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 JniFuncMain */

#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JniFuncMain
* Method: createJniObject
* Signature: ()LJniTest;
*/
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

观察上述的头文件,我们发现生成的函数原型的第二个参数类型为 jclass 而不是 jsobject。这是因为我们在 Java 中将这个方法声明为 static 静态方法,而静态方法是通过类而非对象来进行调用的。这个参数保存的是 JniFuncMain 类的引用而非类对象的引用。

实现函数原型 jnifunc.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "JniFuncMain.h"
#include <stdio.h>

JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
(JNIEnv *env, jclass clazz) {
jclass targetClass;
jmethodID mid;
jobject newObject;
jstring helloStr;
jfieldID fid;
jint sIntField;
jint result;

// 获取 JniFuncMain 类的 sIntField 变量值

// 获取 sIntField 变量的 jfieldID 值
fid = (*env)->GetStaticFieldID(env, clazz, "sIntField", "I");
// 根据 jfieldID 值在 jclass 查找指定的变量
// 若访问的变量为静态变量,则调用 GetStaticFieldID() 的 JNI 函数
// 若访问的变量为成员变量,则调用 GetFieldID() 的JNI函数
sIntField = (*env)->GetStaticIntField(env, clazz, fid);
printf("[C] 访问 JniFuncMain 类 的 sIntField 值为 %d\n", sIntField);

// 查找生成对象的类
// 若想获取指定类的 jclass 指,则调用 JNI 函数 FindClass() 即可
targetClass = (*env)->FindClass(env, "JniTest");

// 查找 JniTest 类的构造方法
mid = (*env)->GetMethodID(env, targetClass, "<init>", "(I)V");

// 生成 JniTest 对象(返回对象的引用)
printf("[C] 创建 JniTest 对象");
newObject = (*env)->NewObject(env, targetClass, mid, 2);

// 调用 JniTest 对象的方法
mid = (*env)->GetMethodID(env, targetClass, "callByNative", "(I)I");
// 若调用的 Java 方法是静态方法,则调用的函数形式为 CallStatic<type> Method()
// 若调用的 Java 方法是成员方法,则调用的函数形式为 Call<type> Method()
result = (*env)->CallIntMethod(env, newObject, mid, 1);

// 设置 JniObject 对象的 mIntField 值
fid = (*env)->GetFieldID(env, targetClass, "mIntField", "I");
// 若设置的 Java 变量是静态属性,则调用的函数形式为 setStatic<type> Field()
// 若调用的 Java 变量是成员属性,则调用的函数形式为 set<type> Field()
(*env)->SetIntField(env, newObject, fid, result);
printf("[C] 设置 JniTest 对象的 mIntField,值为 %d\n", result);

return newObject;
}

接下来生成动态链接库,如下:

1
gcc jnifunc.c -I /Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/include -I /Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/include/darwin -shared -o libjnifunc.jnilib

运行结果

可以从 C 代码观察到创建一个 Java 对象的顺序如下:

  1. 查找指定的类,并将查找到的类引用赋值给 jcalss 类型的变量(FindClass() 方法)。
  2. 查找 Java 类构造方法的 ID 值( GetMethodID() 方法,参数类型为 jmethodID,方法名为 <init>)。
  3. 生成 Java 类对象。

关于查找方法的补充说明:

如果是静态方法,则使用 GetStaticMethodID() ,方法的签名需要使用 javap 来获取。

获取方法描述符

局部引用与全局引用

在 JNI 本地函数中,由 FindClass()、GetObjectClass() 等 JNI 函数返回的 jclassjobject 等引用都是局部引用,此类引用的作用域只在 JNI 本地函数中。一旦 JNI 本地函数返回后,其内部引用就会失效。
如果想让该引用转换成全局引用,可以使用 NewGlobalRef()方法,之后将返回值存放在全局变量中,以便于在整个 C 代码中使用。如有要销毁该全局引用,请调用 DeleteGlobalRef() 方法。

Android 源码 中的 JNI

如果有兴趣了解 Android 底层是如何使用 JNI 的,可以查看下列源码:

1
2
3
frameworks\base\core\jni
frameworks\base\services\jni
frameworks\base\media\jni