JNI介绍(一)

JNI介绍

本章介绍Java Native Interface(JNI)。JNI是本机编程接口。它允许在Java虚拟机(VM)内运行的Java代码与使用其他编程语言(如C,C ++和汇编语言)编写的应用程序和库进行互操作。

JNI最重要的好处是它对底层Java VM的实现没有任何限制.Java VM供应商可以添加对JNI的支持,而不会影响VM的其他部分。

为什么要使用JNI?

  • 标准Java类库不支持应用程序所需的与平台相关的功能。
  • 您已经有一个用另一种语言编写的库,并希望通过JNI使其可以访问Java代码。
  • 您希望在较低级别的语言(如汇编语言)中实现一小部分时间关键代码。

JNI有哪些好处

扩展:JNI扩展了JVM能力,驱动开发,例如开发一个wifi驱动,可以将手机设置为无限路由;

高效: 本地代码效率高,游戏渲染,音频视频处理等方面使用JNI调用本地代码,C语言可以灵活操作内存;

复用: 在文件压缩算法 7zip开源代码库,机器视觉 OpenCV开放算法库等方面可以复用C平台上的代码,不必在开发一套完整的Java体系,避免重复发明轮子;

特殊: 产品的核心技术一般也采用JNI开发,不易破解;

用了JNI你可以在native中干嘛?

  • 创建,检查和更新Java对象(包括数组和字符串)。
  • 调用Java方法。
  • 捕获并抛出异常。
  • 加载类并获取类信息。
  • 实现运行时类型检查。

关于JNI的未来

JNI不被认为是给定Java VM支持的唯一native方法接口。标准接口使程序员受益,他们希望将native代码库加载到不同的Java VM中。在某些情况下,程序员可能必须使用较低级别的VM特定接口来实现最高效率。在其他情况下,程序员可能使用更高级别的界面来构建软件组件。实际上,随着Java环境和组件软件技术的日趋成熟,本机方法将逐渐失去意义。

JNI已经过时间测试,并确保不对您的VM实施施加任何开销或限制,包括对象表示,垃圾收集方案等。

JNI 接口方法和指针

native代码通过调用JNI函数来访问Java VM功能Jni方法通过interface points获得,interface points是一个指向指针的一个指针,这个指针指向一个指针数组,数组里面的每个指针指向一个接口函数,每个interface points都是在数组内的偏移量

Interface pointer

JNI使用这种结构类似于C++的虚拟方法表virtual function table或是像com interface好处是JNI name spacenative 代码的分离,这样虚拟机可以提供多个版本的虚拟方法表,例如虚拟机可以使用两个JNI方法表

例如:

  • 一个人执行彻底的非法参数检查,适合调试;
  • 另一个执行JNI规范所需的最小量检查,因此更有效。

注意事项:

JNI接口指针只能在当前线程生效
虚拟机可以在JNI指向的区域中分配和存储线程的本地数据

native 方法接收JNI 接口指针做为参数, java从一个线程多次调用JNI的话传递的是一个JNI接口指针,从多个线程调用native的话,调用的是多个JNI接口指针

关于线程

所有的线程都是linux通过内核调度的,他们通常用Thread.start创建,但是也可以用其他的方式创建,比如一个thread可以通过pthread_create启动,并且附加到JNI的AttachCurrentThread或者AttachCurrentThreadAsDaemon方法,在线程创建成功之前,JNIEnv方法是不能够调用的.

  • 从JNI创建的线程是加入到main thread中的

  • android不支持native挂起线程,如果正在进行垃圾收集,或者调试器已发出挂起请求,Android将在下次进行JNI调用时暂停该线程。

  • 通过JNI连接的线程必须在退出之前调用DetachCurrentThread,如果直接编码是不方便的,在Android 2.0(Eclair)及更高版本中你可以使用pthread_key_create来定义在线程退出之前调用的析构函数,并从那里调用DetachCurrentThread。(将keypthread_setspecific将JNIEnv存储在thread-local-storage;这样它将作为参数传递到析构函数中。)

静态注册

如下例子:使用System.loadLibrary定义方法

1
2
3
4
5
6
7
8
package pkg; 

class Cls {
native double f(int i, String s);
static {
System.loadLibrary(“pkg_Cls”);
}
}

System.loadLibrary,里面的参数是系统特定的方法例如:

系统将名称pkg_Cls转换为libpkg_Cls.so,而Win32系统将相同的pkg_Cls名称转换为pkg_Cls.dll。

解析Native方法名称

动态链接器根据名称来解析条目,一个native 名称根据以下规则来生成

  • 前缀JAVA_
  • 一个完全类名
  • 一个下划线_
  • 一个方法名
  • 对于重载的native方法,两条下划线后面加上参数名

虚拟机会首先检查短名,就是不包含参数的名称,之后检查带参数的名称,程序使用长名称只是在一个native方法重载于另一个方法的时候,但是非native方法不受影响

例子:

1
2
3
4
class Cls1 { 
int g(int i);
native int g(double d);
}

这个例子中g没有使用长名称,因为int g(int i)不是一个native方法

名称使用Unicode characters转义来转换C的方法名,我们用下划线_来代替全限定符类名中的/,因此不以数字开头,用_0_9来做转义序列.

转义序列 标识
_0XXXX a Unicode character XXXX. 小写是非转义序列,例如, _0abcd 并不是 _0ABCD.
_1 the character “_”
_2 the character “;”
_3 the character “[“

native方法参数

  • JNI 接口指针是native 方法的第一个参数,类型是JNIEnv
  • 第二个参数根据native方法是静态或者是非静态,非静态方法是该对象的引用,静态方法是java类的引用
1
2
3
4
5
6
package pkg; 

class Cls {
native double f(int i, String s);
// ...
}

比如说有重载的地方,则用__和长方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (

JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */

{
const char *str = env->GetStringUTFChars(s, 0);

// ...

env->ReleaseStringUTFChars(s, str);

// return ...
}

动态注册

直接告诉native方法jni的函数指针,通过JNINativeMethod来保存java方法和JNI函数的关联

  • 先编写 Java 的 native 方法;
  • 编写 JNI 函数的实现(函数名可以随便命名);
  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
  • 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
  • 在 JNI_OnLoad 方法中调用注册方法;
  • 在Java中通过System.loadLibrary加载完JNI动态库之后,会调用JNI_OnLoad函数,完成动态注册;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//JNINativeMethod结构体
typedef struct {
const char* name; //Java中native方法的名字
const char* signature; //Java中native方法的描述符
void* fnPtr; //对应JNI函数的指针
} JNINativeMethod;

/**
* @param clazz java类名,通过 FindClass 获取
* @param methods JNINativeMethod 结构体指针
* @param nMethods 方法个数
*/
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

//JNI_OnLoad
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);

java对象的引用

原始数据类型,比如integer,characters等在java和native之间有复制,其他的任何的java对象都需要通过引用来传递,VM会跟踪这些已经传递给native的对象,保证不会被释放,native也需要通过通知来告诉VM这些方法不需要了

Global 和Local引用

JNI划分native代码的引用分为两种,localglobal引用,本地引用会在调用持续时间内有效,返回结果的时候释放,全局的在主动释放前一直有效

什么情况下要主动释放local引用呢?

  • 大型的java对象
  • 循环遍历对象到native

本地引用不能跨线程

引用原理

为了实现native引用,Java VM为从Java到native方法的每次控制转换创建了一个注册表。注册表将不可移动的native引用映射到Java对象,并防止对象被垃圾回收。传递给native方法的所有Java对象都会自动添加到注册表中。在本机方法返回后删除注册表,允许其所有条目被垃圾回收。

获取非本地引用的唯一方法是通过函数 NewGlobalRefNewWeakGlobalRef

本地和全局引用通常在缓存从中返回的jclass时使用FindClass,例如:

1
2
jclass localClass = env - > FindClass (“MyClass” ); 
jclass globalClass = reinterpret_cast <jclass> (env - > NewGlobalRef (localClass ));

所有JNI方法都接受本地和全局引用作为参数。对同一对象的引用可能具有不同的值。例如,从NewGlobalRef对同一对象的连续调用的返回值可能不同。 要查看两个引用是否引用同一对象,必须使用该IsSameObject函数。 不要将引用与==native代码进行比较。

jfieldIDs和jmethodIDs是不透明类型,而不是对象引用,不能将其设置为 NewGlobalRef。而元数据的指针通过GetStringUTFCharsGetByteArrayElements返回也不是对象(他们可以在线程之间传递)

native访问字段或者方法

JNI允许本机代码访问字段并调用Java对象的方法。JNI通过符号名称和类型签名来标识方法和字段,例如调用cls类中的f方法,native需要首先获取方法id

jclass, jmethodID, 和 jfieldID

如果你想再native中访问Java对象,的话你需要关注以下几点

  • 获取类的类对象引用 FindClass
  • 获取该字段的字段ID GetFieldID
  • 使用适当的内容获取字段的内容,例如 GetIntField

如果您希望在加载类时缓存ID,并在卸载和重新加载类时自动重新缓存它们,初始化ID的正确方法是将一段代码添加到相应的代码中。

1
2
3
4
5
private static native void nativeInit();

static {
nativeInit();
}

nativeClassInit在C / C ++代码中创建一个执行ID查找的方法。

1
jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);

然后就可以使用方法id来调用了

1
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);

字段或者id是不会从虚拟机中unloading的类中获取的,在unloading之后,字段或者方法就无效了

JNI异常

jni不会进行空检查,java类和对象等检查,这是为什么呢?两个原因

  • 检查错误的方法可能会影响正确native方法的性能
  • 大多情况下,没有足够的类型信息来检查这些错误

那么异常的处理方法呢?

  • native方法可以马上返回,从而导致启动native方法的java方法异常
  • native代码可以通过调用异常清理ExceptionClear(),然后执行异常代码处理

JNI的类型和数据结构

原始类型

Java类型 native类型 描述
boolean jboolean 无符号8位
byte jbyte 有符号8位
char jchar 无符号16位
short jshort 有符号16位
int jint 有符号32位
long jlong 有符号64位
float jfloat 32位
double jdouble 64位
void void 不适用

为了方便,通常可以定义boolean的值

1
2
#define JNI_FALSE  0
#define JNI_TRUE 1

jzice 整数类型用于表示基本索引和大小

1
typedef jint jsize;

引用类型

JNI包含了多种不同java类型的引用类型,层次结构是

  • jobject
    • jclassjava.lang.Class对象)
    • jstringjava.lang.String对象)
    • jarray (arrays)
      • jobjectArray (对象数组)
      • jbooleanArrayboolean数组)
      • jbyteArraybyte数组)
      • jcharArraychar数组)
      • jshortArrayshort数组)
      • jintArrayint数组)
      • jlongArraylong数组)
      • jfloatArrayfloat数组)
      • jdoubleArraydouble数组)
    • jthrowablejava.lang.Throwable对象)

在C中,所以其他的JNI引用对象类似于jobject

1
typedef jobject jclass;

在C++中,JNI引入了虚拟类来强制声明了子类关系.

1
2
3
4
5
class _jobject {};
class _jclass : public _jobject {};
// ...
typedef _jobject *jobject;
typedef _jclass *jclass;

字段和方法id

字段和方法id是常规的C指针

1
2
3
4
5
struct _jfieldID;             
typedef struct _jfieldID *jfieldID; /* field IDs */

struct _jmethodID;
typedef struct _jmethodID *jmethodID; /* method IDs */

value类型

jvalue类型被声明为以下构造

1
2
3
4
5
6
7
8
9
10
11
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;

类型签名

JNI使用Java VM的类型签名表示。下表显示了这些类型签名

Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ; fully-qualified-class
[ type type[]
( arg-types ) ret-type method type

例如在java中有一个方法

1
long f (int n, String s, int[] arr);

对应的类型签名是

1
(ILjava/lang/String;[I)J

一般引用类型描述符的规则如下,注意不要丢掉“;”

1
L + 类描述符 + ;

如,String 类型的域描述符为:

1
Ljava/lang/String;

数组的域描述符特殊一点,如下,其中有多少级数组就有多少个“[”,数组的类型为类时,则有分号,为基本类型时没有分号

1
[ + 其类型的域描述符

对应之前的调用方法

1
jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);

关于JAVAVM和JNIENV

接下来两项介绍的一样JavaVMJniENV两者都是指向指针函数表的指针,JavaVM提供调用接口函数,允许创建和销毁JavaVM。从理论上讲,每个进程可以有多个JavaVM,但Android只允许一个。

JNIEnv提供了大多数JNI功能。native函数都接收JNIEnv作为第一个参数。

JNIEnv用于thread-local的存储,原因是不能在两个线程中分享一个JNIEnv,如果你拿不到JNIEnv,那么你就要共享你的JavaJvm了,然后用GetEnv方法来拿到你的JniEnv

JNI方法

接口方法表Interface Function Table

每个方法都可以通过JNIEnv参数以固定的偏移量访问。JNIEnv类型是一个指向存储所有JNI方法指的指针。定义如下:

1
typedef const struct JNINativeInterface *JNIEnv;

虚拟机初始化方法表,前三条NULL表示与COM的兼容性

函数表可以在所有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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
const struct JNINativeInterface ... = {

NULL,
NULL,
NULL,
NULL,
GetVersion,

DefineClass,
FindClass,

FromReflectedMethod,
FromReflectedField,
ToReflectedMethod,

GetSuperclass,
IsAssignableFrom,

ToReflectedField,

Throw,
ThrowNew,
ExceptionOccurred,
ExceptionDescribe,
ExceptionClear,
FatalError,

PushLocalFrame,
PopLocalFrame,

NewGlobalRef,
DeleteGlobalRef,
DeleteLocalRef,
IsSameObject,
NewLocalRef,
EnsureLocalCapacity,

AllocObject,
NewObject,
NewObjectV,
NewObjectA,

GetObjectClass,
IsInstanceOf,

GetMethodID,

CallObjectMethod,
CallObjectMethodV,
CallObjectMethodA,
CallBooleanMethod,
CallBooleanMethodV,
CallBooleanMethodA,
CallByteMethod,
CallByteMethodV,
CallByteMethodA,
CallCharMethod,
CallCharMethodV,
CallCharMethodA,
CallShortMethod,
CallShortMethodV,
CallShortMethodA,
CallIntMethod,
CallIntMethodV,
CallIntMethodA,
CallLongMethod,
CallLongMethodV,
CallLongMethodA,
CallFloatMethod,
CallFloatMethodV,
CallFloatMethodA,
CallDoubleMethod,
CallDoubleMethodV,
CallDoubleMethodA,
CallVoidMethod,
CallVoidMethodV,
CallVoidMethodA,

CallNonvirtualObjectMethod,
CallNonvirtualObjectMethodV,
CallNonvirtualObjectMethodA,
CallNonvirtualBooleanMethod,
CallNonvirtualBooleanMethodV,
CallNonvirtualBooleanMethodA,
CallNonvirtualByteMethod,
CallNonvirtualByteMethodV,
CallNonvirtualByteMethodA,
CallNonvirtualCharMethod,
CallNonvirtualCharMethodV,
CallNonvirtualCharMethodA,
CallNonvirtualShortMethod,
CallNonvirtualShortMethodV,
CallNonvirtualShortMethodA,
CallNonvirtualIntMethod,
CallNonvirtualIntMethodV,
CallNonvirtualIntMethodA,
CallNonvirtualLongMethod,
CallNonvirtualLongMethodV,
CallNonvirtualLongMethodA,
CallNonvirtualFloatMethod,
CallNonvirtualFloatMethodV,
CallNonvirtualFloatMethodA,
CallNonvirtualDoubleMethod,
CallNonvirtualDoubleMethodV,
CallNonvirtualDoubleMethodA,
CallNonvirtualVoidMethod,
CallNonvirtualVoidMethodV,
CallNonvirtualVoidMethodA,

GetFieldID,

GetObjectField,
GetBooleanField,
GetByteField,
GetCharField,
GetShortField,
GetIntField,
GetLongField,
GetFloatField,
GetDoubleField,
SetObjectField,
SetBooleanField,
SetByteField,
SetCharField,
SetShortField,
SetIntField,
SetLongField,
SetFloatField,
SetDoubleField,

GetStaticMethodID,

CallStaticObjectMethod,
CallStaticObjectMethodV,
CallStaticObjectMethodA,
CallStaticBooleanMethod,
CallStaticBooleanMethodV,
CallStaticBooleanMethodA,
CallStaticByteMethod,
CallStaticByteMethodV,
CallStaticByteMethodA,
CallStaticCharMethod,
CallStaticCharMethodV,
CallStaticCharMethodA,
CallStaticShortMethod,
CallStaticShortMethodV,
CallStaticShortMethodA,
CallStaticIntMethod,
CallStaticIntMethodV,
CallStaticIntMethodA,
CallStaticLongMethod,
CallStaticLongMethodV,
CallStaticLongMethodA,
CallStaticFloatMethod,
CallStaticFloatMethodV,
CallStaticFloatMethodA,
CallStaticDoubleMethod,
CallStaticDoubleMethodV,
CallStaticDoubleMethodA,
CallStaticVoidMethod,
CallStaticVoidMethodV,
CallStaticVoidMethodA,

GetStaticFieldID,

GetStaticObjectField,
GetStaticBooleanField,
GetStaticByteField,
GetStaticCharField,
GetStaticShortField,
GetStaticIntField,
GetStaticLongField,
GetStaticFloatField,
GetStaticDoubleField,

SetStaticObjectField,
SetStaticBooleanField,
SetStaticByteField,
SetStaticCharField,
SetStaticShortField,
SetStaticIntField,
SetStaticLongField,
SetStaticFloatField,
SetStaticDoubleField,

NewString,

GetStringLength,
GetStringChars,
ReleaseStringChars,

NewStringUTF,
GetStringUTFLength,
GetStringUTFChars,
ReleaseStringUTFChars,

GetArrayLength,

NewObjectArray,
GetObjectArrayElement,
SetObjectArrayElement,

NewBooleanArray,
NewByteArray,
NewCharArray,
NewShortArray,
NewIntArray,
NewLongArray,
NewFloatArray,
NewDoubleArray,

GetBooleanArrayElements,
GetByteArrayElements,
GetCharArrayElements,
GetShortArrayElements,
GetIntArrayElements,
GetLongArrayElements,
GetFloatArrayElements,
GetDoubleArrayElements,

ReleaseBooleanArrayElements,
ReleaseByteArrayElements,
ReleaseCharArrayElements,
ReleaseShortArrayElements,
ReleaseIntArrayElements,
ReleaseLongArrayElements,
ReleaseFloatArrayElements,
ReleaseDoubleArrayElements,

GetBooleanArrayRegion,
GetByteArrayRegion,
GetCharArrayRegion,
GetShortArrayRegion,
GetIntArrayRegion,
GetLongArrayRegion,
GetFloatArrayRegion,
GetDoubleArrayRegion,
SetBooleanArrayRegion,
SetByteArrayRegion,
SetCharArrayRegion,
SetShortArrayRegion,
SetIntArrayRegion,
SetLongArrayRegion,
SetFloatArrayRegion,
SetDoubleArrayRegion,

RegisterNatives,
UnregisterNatives,

MonitorEnter,
MonitorExit,

GetJavaVM,

GetStringRegion,
GetStringUTFRegion,

GetPrimitiveArrayCritical,
ReleasePrimitiveArrayCritical,

GetStringCritical,
ReleaseStringCritical,

NewWeakGlobalRef,
DeleteWeakGlobalRef,

ExceptionCheck,

NewDirectByteBuffer,
GetDirectBufferAddress,
GetDirectBufferCapacity,

GetObjectRefType
};

每个函数的意思戳这里

JNI调用虚拟机方法

允许软件供应商将Java VM加载到任意本机应用程序中。供应商可以提供支持Java的应用程序,而无需链接Java VM源代码.

那么代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <jni.h>       /* where everything is defined */
...
JavaVM *jvm; /* denotes a Java VM */
JNIEnv *env; /* pointer to native method interface */
JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=/usr/lib/java";
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
/* load and initialize a Java VM, return a JNI interface
* pointer in env */
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
delete options;
/* invoke the Main.test method using the JNI */
jclass cls = env->FindClass("Main");
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
env->CallStaticVoidMethod(cls, mid, 100);
/* We are done. */
jvm->DestroyJavaVM();

整个流程

  • 创建虚拟机
1
JNI_CreateJavaVM()

函数加载并初始化Java VM并返回指向JNI接口指针的指针。被调用的线程JNI_CreateJavaVM()被认为是 主线程

  • 附加到虚拟机

JNI接口指针(JNIEnv)仅在当前线程中有效。如果另一个线程需要访问Java VM,它必须首先调用 AttachCurrentThread()将自身附加到VM并获取JNI接口指针

  • 从VM分离

连接到VM的本机线程必须DetachCurrentThread()在退出之前调用 以分离自身。如果调用堆栈上有Java方法,则线程无法分离。

  • 卸载VM

JNI_DestroyJavaVM()函数卸载Java VM。

参考

oracle JNI

android official

JNI基础篇

谢谢您的鼓励~