we new

深入理解Android - JNI

       (原文地址) JNI,是Java Native Interface的缩写,中文为Java本地调用。通俗地说,JNI是一种技术,通过这种技术可以做到以下两点:

  • Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
  • Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。

        Keywords: JNI

Java层

  • 加载对应的JNI库;
  • 声明由关键字native修饰的函数;

JNI层

注册JNI函数

静态方法:根据函数名来找对应的JNI函数

流程如下:

  1. 先编写Java代码,然后编译生成.class文件。
  2. 使用Java的工具程序javah,如javah–o output packagename.classname ,这样它会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。

动态方法:使用JNINativeMethod结构来保存Java native函数数和JNI函数的关联关系

JNINativeMethod结构:

1
2
3
4
5
typedef struct {
const char* name; //Java中native函数的名字,不用携带包的路径。例如“native_init“。
const char* signature; //Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
void* fnPtr; //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;

动态注册主要的两个函数:

1
2
3
4
5
6
7
8
/*
env指向一个JNIEnv结构体,classname为对应的Java类名,由于
JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。
*/
jclass clazz = (*env)->FindClass(env, className);
//调用JNIEnv的RegisterNatives函数,注册关联关系。
(*env)->RegisterNatives(env, clazz, gMethods,numMethods);

Java层通过System.loadLibrary加载完JNI动态库后,这时这些动态注册的函数就会被JNI_OnLoad调用。

数据类型转换

基本类型:
基本类型
引用类型:
引用类型
除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。

JNIEnv介绍

JNIEnv是一个和线程相关的,代表JNI环境的结构体。
JNIEnv的内部结构:
JNIEnv的内部结构
JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数可以做到:

  • 调用Java的函数;
  • 操作jobject对象等很多事情。

JavaVM与JNIEnv的关系:

  • 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了;
  • 另外,后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。

通过JNIEnv操作jobject

在JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数,它们通过JNIEnv的下面两个函数可以得到:

1
2
3
// jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

通过JNIEnv输出的CallMethod,再把jobject、jMethodID和对应参数传进去,JNI层就能够调用Java对象的函数了。

1
2
3
4
5
6
7
/*
调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:
第一个是代表MediaScannerClient的jobject对象,
第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。
*/
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,
lastModified, fileSize);

JNIEnv输出了一系列类似CallVoidMethod的函数,形式如:NativeType CallMethod(JNIEnv *env,jobject obj,jmethodID methodID, …)。其中Type是对应Java函数的返回值类型,例如CallIntMethod、CallVoidMethod等。
上面是针对非static函数的,如果想调用Java中的static函数,则用JNIEnv输出的CallStaticMethod系列函数
通过jfieldID操作jobject的成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获得fieldID后,可调用Get<Type>Field系列函数获取jobject对应成员变量的值。
NativeType Get<Type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
//或者调用Set<Type>Field系列函数来设置jobject对应成员变量的值。
void Set<Type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)
//下面我们列出一些参加的Get/Set函数。
GetObjectField() SetObjectField()
GetBooleanField() SetBooleanField()
GetByteField() SetByteField()
GetCharField() SetCharField()
GetShortField() SetShortField()
GetIntField() SetIntField()
GetLongField() SetLongField()
GetFloatField() SetFloatField()
GetDoubleField() SetDoubleField()

jstring介绍

  • 调用JNIEnv的NewString(JNIEnv env, const jcharunicodeChars,jsize len),可以从Native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String。但由于Java String存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
  • 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。在实际工作中,这个函数用得最多。
  • 上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。其中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。
  • 另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars或ReleaseStringUTFChars函数对应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系,读者写代码时务必注意这个问题。

垃圾回收

JNI规范为了解决野指针被使用,提供了三种类型的引用:

  • Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
  • Global Reference:全局引用,这种对象如不主动释放,就永远不会被垃圾回收。
  • Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。

对于Global Reference:每当JNI层想要保存Java层中的某个对象时,就可以使用Global Reference,使用完后释放它就可以了。
对于LocalReference:如果不调用DeleteLocalRef,LocalReference将在函数返回后被回收。如果调用DeleteLocalRef的话,LocalReference会立即被回收。这两者看起来没什么区别,但是可能出现的问题是虚拟机的内存就会被很快被耗尽。所以没有及时回收的LocalReference或许是进程占用过多的一个原因,请务必注意这一点。

JNI中的异常处理

JNI层函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:

  • ExceptionOccured函数,用来判断是否发生异常。
  • ExceptionClear函数,用来清理当前JNI层中发生的异常。
  • ThrowNew函数,用来向Java层抛出异常。

声明: 本文转载前需与作者联系并标明出处
分享到: