NDK官方文档_详解
NDK官方示例_github
转:Android NDK开发扫盲及最新CMake的编译使用
转:JNI/NDK开发指南
JavaTM Native Interface(JNI)是Java平台一项强力的特性.使用JNI的应用既可以使用Java语言,也可以使用C和C++语言。JNI允许程序员在不需要废弃现有代码的情况下,就可以使用Java平台的能力。由于JNI是Java平台的一部分,所以程序员可以一次性解决互操作性问题,并期望他们的解决方案能够在所有的Java平台都是有效的。
本书既是JNI的编程指南,也是参考手册。本书包含了三个部分:
因为所写的程序包括了Java,C,C++语言,让我们先澄清这些语言在编程环境中的作用范围。Java平台是由Java虚拟机(Java VM)和Java API组成的编程环境,Java程序用Java语言编写,并编译成机器无关的二进制类,一个类可以在任何Java VM执行。任何Java平台的实现都保证了支持Java语言,VM和API。
术语本地环境是指主机操作系统,本地库的集合,和机器指令集合。本地应用程序是由本地语言如C/C++编写的,编译成主机相关的二进制代码,然后用本地库链接。例如,一个为特定主机编译的C语言程序,很大可能无法在另外的操作系统上运行。
Java平台一般是部署在本地环境的上层,例如,the Java Runtime Environment(JRE)是一项由Sun公司开发,可以在Solaris和Windows等操作系统上支持Java平台的产品。Java平台提供了一系列跟本地环境无关的特性给应用程序使用。
当Java平台部署在本地环境之上时,允许Java应用程序与用其他语言编写的本机代码紧密合作可能是可取的或必要的,程序员已经开始采用Java平台来构建传统上用C和C++编写的应用程序。由于对遗留代码的现有投资,Java应用程序将在未来许多年与C和C++代码共存。
JNI是一种功能强大的特性,它允许您利用Java平台,但仍然使用其他语言编写的代码。作为Java虚拟机实现的一部分,JNI是一个双向接口,允许Java应用程序调用本机代码,反之亦然。下图描绘了JNI扮演的角色:
JNI是设计来处理需要Java代码与本地代码一起编译的情况,作为一个双向接口,JNI支持两种类型的本地代码:本地库和本地应用程序。
记住,一旦使用了JNI,就面临着失去两项Java平台才有益处。第一,不再很容易地在各种系统上运行,即使Java层代码编写为可移植的,仍需要保证本地代码也是可移植的。第二,Java语言是类型安全的,本地语言C/C++不是,你使用JNI时需要格外小心,一段行为不当的本地代码可能会导致整个应用程序崩溃。因此,Java应用程序在调用JNI功能之前需要进行安全检查。一个通用的规则是,仔细地设计程序,本地代码越少越好,两者需要清晰地隔离。
在您使用JNI开始项目之前,值得花费时间来调查是否有更合适的替代解决方案。如上节提到的,相比纯Java程序,JNI程序有着固有的缺点,例如失去了Java语言保证的类型安全特性。也有许多允许Java应用程序与用其他语言编写的代码进行交互的替代方法, 例如:
The need for Java applications to interoperate with native code has been recognized since the very early days of the Java platform. The first release of the Java platform, Java Development Kit (JDK™) release 1.0, included a native method interface that allowed Java applications to call functions written in other languages such as C and C++. Many thirdparty applications, as well as the implementation of the Java class libraries (including, for example, java.lang , java.io , and java.net ), relied on the native method interface to access the features in the underlying host environment.
Unfortunately, the native method interface in JDK release 1.0 had two major problems:
- First, the native code accesses fields in objects as members of C structures.
However, the Java virtual machine specification does not define how objects are laid out in memory. If a given Java virtual machine implementation lays out objects in a way other than that assumed by the native method interface, then you have to recompile the native method libraries.- Second, the native method interface in JDK release 1.0 relies on a conserva-
tive garbage collector because native methods can get hold of direct pointers to objects in the virtual machine. Any virtual machine implementation that uses more advanced garbage collection algorithms cannot support the native method interface in JDK release 1.0.
The JNI was designed to overcome these problems. It is an interface that can be supported by all Java virtual machine implementations on a wide variety of host environments. With the JNI:- Each virtual machine implementor can support a larger body of native code.
- Development tool vendors do not have to deal with different kinds of native method interfaces.
- Most importantly, application programmers are able to write one version of their native code and this version will run on different implementations of the Java virtual machine.
The JNI was first supported in JDK release 1.1. Internally, however, JDK
release 1.1 still uses old-style native methods (as in JDK release 1.0) to implement the Java APIs. This is no longer the case in Java 2 SDK release 1.2 (formerly known as JDK release 1.2). Native methods have been rewritten so that they conform to the JNI standard.
The JNI is the native interface supported by all Java virtual machine implementations. From JDK release 1.1 on, you should program to the JNI. The old-style native method interface is still supported in Java 2 SDK release 1.2, but will not (and cannot) be supported in advanced Java virtual machine implementations in the future.
Java 2 SDK release 1.2 contains a number of JNI enhancements. The
enhancements are backward compatible. All future evolutions of JNI will maintain complete binary compatibility.
This book contains numerous example programs that demonstrate JNI features. The example programs typically consist of multiple code segments written in the Java programming language as well as C or C++ native code. Sometimes the native code refers to host-specific features in Solaris and Win32. We also show how to build JNI programs using the command line tools (such as javah ) shipped with JDK and Java 2 SDK releases.
Keep in mind that the use of the JNI is not limited to specific host environments or specific application development tools. The book focuses on writing the code, not on the tools used to build and run the code. The command line tools bundled with JDK and Java 2 SDK releases are rather primitive. Third-party tools
may offer an improved way to build applications that use the JNI. We encourage you to consult the JNI-related documentation bundled with the development tools of your choice.
You can download the source code of the examples in this book, as well as the latest updates to this book, from the following web address:
jni books online
本节写一个使用JNI的简单例子,使用Java语言调用C语言函数来打印"Hello world!".(省略了很多翻译)
package com.example.king.cmakedemo;
public class NativeUtil {
static {
System.loadLibrary("native-lib");
}
public native void stringFromJNI();
}
#include
#include
#include
#define ANDROID_LOG_TAG "native"
#define ANDROID_LOG_ERR "native"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,ANDROID_LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,ANDROID_LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,ANDROID_LOG_ERR,__VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_king_cmakedemo_NativeUtil_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
const char * hello = "Hello from C++";
LOGD(hello);
}
开发者问的最多问题是Java应用和本地代码交互时,数据类型是怎样从Java语言对应到本地C/C++等语言的。上一节的简单例子,我们没有传递参数,也没有返回值,本地函数只是简单地打印了一句话。
不过在实践中,大多数程序传递参数到本地代码,并接收返回值,本节会表明怎样做到这些,先从基本类型int和普通对象String,array等开始.下一节再讨论任意对象的处理方式,包括本地代码怎样访问Java对象的成员和方法.
如上面看到声明函数的方式,JNIEXPORT和JNICALL宏(在jni.h中定义)确保此函数是从本地库导出,并且C编译器生成可以正确调用此函数的代码.格式是以Java_开头,然后到类名,再到方法名.第一个参数JNIEnv,是一个结构体指针,该结构体内部包含了函数指针,每个函数指针都指向一个JNI函数.本地代码总是通过JNI函数来访问Java VM里面的数据结构.下图描绘了JNIEnv:
/**jni.h中的声明,可以看出实际C和C++最后都是调用了JNINativeInterface */
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
struct JNINativeInterface {
jint (*GetVersion)(JNIEnv *);
...省略...
};
struct _JNIEnv {
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
...省略...
#endif /*__cplusplus*/
};
第二个参数的含义取决于是成员方法还是静态方法,如果是成员方法,就是jobject代表调用该方法的当前对象的引用,如果是静态方法,就是jclass代表定义了此方法的这个class.
JNI函数声明中的参数类型,在本地编程语言中都有对应的类型.JNI定义了一系列C/C++语言和Java语言相对应的类型.Java语言有两种数据类型:基本类型int,float,char等等和引用类型classes,instances,arrays(strings是java.lang.String的实例).
JNI用不同的方式处理这两种类型:基本类型直接映射到本地,例如int直接对应jint(typedef int jint;), float对应jfloat(typedef float jfloat;),如下:
/**jni.h*/
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
引用类型对于本地代码来说是不透明的,引用类型是指向Java VM内部数据结构的指针,该数据结构的实际内存布局对开发者是不可见的,本地代码必须通过JNI函数即JNIEnv来操作这些引用类型.例如java.lang.String对应的jstring,本地代码是不知道其内容是什么,只能调用GetStringUTFChars等函数来获取其内容.
所有的引用类型都是jobject类型,为了便利性和加强类型安全,JNI定义了一系列引用类型的子类型.(A是B的子类型,则每个A的实例都是B的实例),这些子类型都对应了平时Java开发中的常用引用类型。例如jstring表示字符串String,jobjectArray表示object数组,如下图示:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
/* ERROR: incorrect use of jstring as a char* pointer */
printf("%s", prompt);
...
}
本地代码必须使用JNI函数来转换jstring到C/C++字符串,JNI支持从Unicode到UTF-8的相互转换,Unicode用16bit来表示字符,UTF-8用变长编码,其可以兼容7bit的ASCII编码.UTF-8类似C字符串以’\0’结束,即使里面包含了非ASCII编码字符.所有ASCII字符在UTF-8中的编码值跟原来一样.一个字节的最高位设置表示16位Unicode多字节编码的开始.
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_king_cmakedemo_NativeUtil_getLine(
JNIEnv* env,
jobject thiz, jstring prompt) {
/* ERROR: incorrect use of jstring as a char* pointer */
//printf("%s", prompt);
const char* str_prompt = env->GetStringUTFChars(prompt, NULL);
if (str_prompt == NULL) {
return NULL;
}
LOGD(str_prompt);
env->ReleaseStringUTFChars(prompt, str_prompt);
const char * hello = "Hello from C++";
return env->NewStringUTF(hello);
}
不要忘记检查GetStringUTFChars的返回值,Java VM需要分配内存来存放UTF-8字符串,有小概率内存分配会失败,这种情况下会返回NULL并且抛出OutOfMemoryError异常,在JNI抛出异常不同于在Java中抛出异常,在JNI抛出异常不会打断正在执行的控制流程,因此,我们需要显式地用return来跳过剩余代码流程并返回,返回后,在上层Java调用处会抛出异常.
When your native code finishes using the UTF-8 string obtained through GetStringUTFChars, it calls ReleaseStringUTFChars. Calling ReleaseStringUTFChars indicates that the native method no longer needs the UTF-8 string returned by GetStringUTFChars; thus the memory taken by the UTF-8 string can be freed. Failure to call ReleaseStringUTFChars would result in a memory leak, which could ultimately lead to memory exhaustion.
可以本地代码通过JNI函数NewStringUTF来创建java.lang.String的对象,NewStringUTF函数接收C语言的UTF-8字符串然后构造java.lang.String对象,新构建的Unicode字符串对象表示了跟UTF-8相同的序列.如果内存分配失败导致创建对象失败,将抛出OutOfMemoryError异常并返回NULL.本示例不需要检查,直接抛出异常到上层.
/**
* 说明:以 UTF-16 的编码方式创建一个 Java 的字符串(jchar 的定义为 uint16_t)
@Param unicodeChars:指向字符数组的指针
@Param len:字符数组的长度
*/
jstring NewString(const jchar* unicodeChars, jsize len)
/**
* 按Unicode格式来获取字符串长度
*/
jsize GetStringLength(jstring string)
/**
* 按UTF-8格式来获取字符串长度(也可以转换jstring后用C函数strlen获取)
*/
jsize GetStringUTFLength(jstring string)
/**
* 获取Unicode编码的字符串(本地环境支持Unicode时很有用)
*/
const jchar* GetStringChars(jstring string, jboolean* isCopy)
/**
* 释放指定的字符串指针,通常来说,Get 和 Release 是成对出现的
* @Param string: Java 风格的字符串
* @Param chars/utf: 对应的 C 风格的字符串
*/
void ReleaseStringChars(jstring string, const jchar* chars)
void ReleaseStringUTFChars(jstring string, const char* utf)
void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf)
void GetStringUTFRegion(jstring str, jsize start, jsize len, char* buf)
const jchar* GetStringCritical(jstring string, jboolean* isCopy)
void ReleaseStringCritical(jstring string, const jchar* carray)
其他函数待续...
上面的参数jboolean* isCopy,当获取的字符串是原来的一份副本时,它的值会被设为JNI_TRUE,当获取的字符串是直接指向原内容的指针时,它的值会被设为JNI_FALSE.当返回JNI_FALSE时,本地代码禁止修改字符串内容,否则原内容也会被修改,这违反了Java平台String实例是不可变的规范.大多数情况下,该参数传0即可.
int[] iarr;
float[] farr;
Object[] oarr;
int[][] arr2;
iarr, farr是primitive(基本类型)数组,oarr, arr2是object(引用类型)数组.在本地代码访问数组内容,也类似与访问字符串,需要用JNI函数.
/** 访问数组,错误代码 */
extern "C" JNIEXPORT jint JNICALL
Java_com_example_king_cmakedemo_NativeUtil_sumArray(
JNIEnv* env,
jobject /* this */,
jintArray arr) {
int i, sum = 0;
for (i = 0; i < 10; i++) {
sum += arr[i];
}
return sum;
}
/** 应该这样访问数组 */
extern "C" JNIEXPORT jint JNICALL
Java_com_example_king_cmakedemo_NativeUtil_sumArray(
JNIEnv* env,
jobject /* this */,
jintArray arr) {
jint buf[3];
jint i, sum = 0;
env->GetIntArrayRegion(arr, 0, 3, buf);
for (i = 0; i < 3; i++) {
sum += buf[i];
}
return sum;
}
/** 或者这样访问数组 */
extern "C" JNIEXPORT jint JNICALL
Java_com_example_king_cmakedemo_NativeUtil_sumArray(
JNIEnv* env,
jobject /* this */,
jintArray arr) {
jint * carr;
jint i, sum = 0;
carr = env->GetIntArrayElements(arr, NULL);
if (carr == NULL) {
return 0; /* exception occurred */
}
for (i=0; i<3; i++) {
sum += carr[i];
}
env->ReleaseIntArrayElements(arr, carr, 0);
return sum;
}
//复制primitive数组
GetArrayRegion (GetIntArrayRegion(), GetFloatArrayRegion(), GetBooleanArrayRegion() ...)
//设置primitive数组的数据
SetArrayRegion
//获取primitive数组的指针(有可能是返回其副本的指针)
GetArrayElements
ReleaseArrayElements
//获取数组的长度
jsize GetArrayLength(jarray array)
// 创建一个给定长度的primitive数组
NewArray
GetPrimitiveArrayCritical
ReleasePrimitiveArrayCritical
JNI提供了独立的函数来访问object数组,GetObjectArrayElement返回某个位置的元素,SetObjectArrayElement设置某个位置的数据,不像primitive数组,不能一次得到所有的数组内容.
/** 返回一个新创建的二维数组 */
extern "C" JNIEXPORT jobjectArray JNICALL
Java_com_example_king_cmakedemo_NativeUtil_initInt2DArray(
JNIEnv* env,
jobject /* this */,
jint size) {
jobjectArray result;
int i;
jclass intArrCls = env->FindClass("[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = env->NewObjectArray(size, intArrCls,
NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[size]; /* make sure it is large enough! */
int j;
jintArray iarr = env->NewIntArray(size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
env->SetIntArrayRegion(iarr, 0, size, tmp);
env->SetObjectArrayElement(result, i, iarr);
env->DeleteLocalRef(iarr);
}
return result;
}
现在你知道怎样用JNI来访问基本类型和引用类型比如string,array了,下一步就是与任意对象的成员和方法交互,除此之外,也包括怎样从本地代码调用Java语言的方法,一般称为回调.
先介绍可以支持访问成员和方法的JNI函数,稍后介绍怎样令这些操作效率更高的缓存技术,In the last
section, we will discuss the performance characteristics of calling native methods as well as accessing fields and calling methods from native code.
Java语言支持两种fields,每个类对象都有自己的instance fields的副本,所有的类对象都共享static fields,JNI提供了让本地代码可以获取和设置对象instance fields,static fields的函数.
/** Instance Fields Access */
public class NativeUtil {
static {
System.loadLibrary("native-lib");
}
public String s = "abc";
public native String stringFromJNI();
public native void accessField();
}
extern "C" JNIEXPORT void JNICALL
Java_com_king_cmakedemo_NativeUtil_accessField(
JNIEnv* env,
jobject obj) {
jfieldID fid; /* store the field ID */
jstring jstr;
const char *str;
/* Get a reference to obj’s class */
jclass cls = env->GetObjectClass(obj);
/* Look for the instance field s in cls */
fid = env->GetFieldID(cls, "s",
"Ljava/lang/String;");
if (fid == NULL) {
return; /* failed to find the field */
}
/* Read the instance field s */
jstr = static_cast(env->GetObjectField(obj, fid));
str = env->GetStringUTFChars(jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
LOGD(" c.s = \"%s\"\n", str);
env->ReleaseStringUTFChars(jstr, str);
/* Create a new string and overwrite the instance field */
jstr = env->NewStringUTF("123");
if (jstr == NULL) {
return; /* out of memory */
}
env->SetObjectField(obj, fid, jstr);
}
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
NativeType GetField(jobject obj, jfieldID fieldID) 例如: GetIntField, GetObjectField
NativeType GetStaticField(jobject obj, jfieldID fieldID) 例如: GetStaticIntField , GetStaticObjectField
void SetField(jobject obj, jfieldID fieldID, NativeType value) 例如: SetIntField , SetObjectField
void SetStaticField(jobject obj, jfieldID fieldID, NativeType value) 例如:SetStaticIntField, SetStaticObjectField
Java语言有几种方法,成员方法,静态方法,构造方法。
package com.king.cmakedemo;
public class Util {
public void callback() {
System.out.println("Util callback");
}
}
package com.king.cmakedemo;
public class NativeUtil {
static {
System.loadLibrary("native-lib");
}
public native void nativeMethod();
@Override
public void callback() {
System.out.println("NativeUtil callback");
}
public static void callback2() {
System.out.println("static callback");
}
public native String stringFromJNI();
}
jstring MyNewString(JNIEnv * env, const jchar* chars, jint len);
/**
* 调用成员方法, 调用静态方法,调用父类方法
*/
extern "C" JNIEXPORT void JNICALL
Java_com_king_cmakedemo_NativeUtil_nativeMethod(
JNIEnv* env,
jobject obj) {
jjclass cls = env->GetObjectClass(obj);
jmethodID mid =
env->GetMethodID(cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
// 调用成员方法,
env->CallVoidMethod(obj, mid);
mid = env->GetStaticMethodID(cls, "callback2", "()V");
if (mid == NULL) {
return; /* method not found */
}
// 调用静态方法
env->CallStaticVoidMethod(cls, mid);
// 调用父类方法
cls = env->GetSuperclass(cls);
mid = env->GetMethodID(cls, "callback", "()V");
env->CallNonvirtualVoidMethod(obj, cls, mid);
// 调用String类构造方法
const char content[] = "HelloNewString";
jstring str_content = env->NewStringUTF(content);
const jchar* jchar_content = env->GetStringChars(str_content, 0);
jstring s = MyNewString(env, jchar_content, env->GetStringLength(str_content));
LOGD("new create string=%s\n", env->GetStringUTFChars(s, 0));
}
// 调用String类构造方法
jstring MyNewString(JNIEnv * env, const jchar* chars, jint len) {
jclass stringClass;
jmethodID cid;
jcharArray elemArr;
jstring result;
stringClass = env->FindClass("java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
/* Get the method ID for the String(char[]) constructor */
cid = env->GetMethodID(stringClass,
"", "([C)V");
if (cid == NULL) {
return NULL; /* exception thrown */
}
/* Create a char[] that holds the string characters */
elemArr = env->NewCharArray(len);
if (elemArr == NULL) {
return NULL; /* exception thrown */
}
env->SetCharArrayRegion(elemArr, 0, len, chars);
/* Construct a java.lang.String object */
result = static_cast(env->NewObject(stringClass, cid, elemArr));
/* Free local references */
env->DeleteLocalRef(elemArr);
env->DeleteLocalRef(stringClass);
return result;
}
获取成员和方法的ID需要基于成员和方法的名称和描述符进行符号查找,这一过程较耗资源,本节介绍可以提高效率的技术。思路是获取到成员和方法ID后,把它们缓存起来,供后面可以重复使用。有两种缓存的方式,取决于是使用的时候才缓存,还是类静态初始化时就缓存起来。
...省略代码...
extern "C" JNIEXPORT void JNICALL
Java_com_king_cmakedemo_NativeUtil_accessField(
JNIEnv* env,
jobject obj) {
static jfieldID fid = NULL;
if (fid == NULL) {
fid = env->GetFieldID(cls, "s",
"Ljava/lang/String;");
}
}
...省略代码...
public class NativeUtil extends Util {
static {
System.loadLibrary("native-lib");
initIDs();
}
public native String nativeMethod();
@Override
public void callback() {
Log.e("kkunion", "NativeUtil callback");
}
public native String stringFromJNI();
public native void accessField();
private static native void initIDs();
}
jmethodID MID_InstanceMethodCall_callback;
extern "C" JNIEXPORT void JNICALL
Java_com_king_cmakedemo_NativeUtil_initIDs(
JNIEnv* env,
jclass cls) {
MID_InstanceMethodCall_callback = env->GetMethodID(cls, "callback", "()V");
}
Caching IDs at the point of use is the reasonable solution if the JNI programmer does not have control over the source of the class that defines the field or method. For example, in the MyNewString example, we cannot inject a custom initIDs native method into the java.lang.String class in order to precompute and cache the method ID for the java.lang.String constructor. Caching at the point of use has a number of disadvantages when compared with caching in the static initializer of the defining class.
• As explained before, caching at the point of use requires a check in the execution fast path and may also require duplicated checks and initialization of the same field or method ID.
• Method and field IDs are only valid until the class is unloaded. If you cache field and method IDs at the point of use you must make sure that the defining class will not be unloaded and reloaded as long as the native code still relies on the value of the cached ID. (The next chapter will show how you can keep a class from being unloaded by creating a reference to that class using the JNI.) On the other hand, if caching is done in the static initializer of the defining class, the cached IDs will automatically be recalculated when the class is unloaded and later reloaded. Thus, where feasible, it is preferable to cache field and method IDs in the static initializer of their defining classes.
After learning how to cache field and method IDs to enhance performance, you might wonder: What are the performance characteristics of accessing fields and calling methods using the JNI? How does the cost of performing a callback from
native code (a native/Java callback) compare with the cost of calling a native method (a Java/native call), and with the cost of calling a regular method (a Java/Java call)?
The answer to this question no doubt depends on how efficiently the underlying virtual machine implements the JNI. It is thus impossible to give an exact account of performance characteristics that is guaranteed to apply to a wide variety of virtual machine implementations. Instead, we will analyze the inherent cost of native method calls and JNI field and method operations and provide a general performance guideline for JNI programmers and implementors.
Let us start by comparing the cost of Java/native calls with the cost of Java/Java calls. Java/native calls are potentially slower than Java/Java calls for the following reasons:
- Native methods most likely follow a different calling convention than that used by Java/Java calls inside the Java virtual machine implementation. As a result, the virtual machine must perform additional operations to build arguments and set up the stack frame before jumping to a native method entry point.
- It is common for the virtual machine to inline method calls. Inlining Java/native calls is a lot harder than inlining Java/Java calls.
We estimate that a typical virtual machine may execute a Java/native call roughly two to three times slower than it executes a Java/Java call. Because a Java/Java call takes just a few cycles, the added overhead will be negligible unless the native method performs trivial operations. It is also possible to build virtual machine implementations with Java/native call performance close or equal to that of Java/Java calls. (Such virtual machine implementations, for example, may adopt the JNI calling convention as the internal Java/Java calling convention.)
The performance characteristics of a native/Java callback is technically similar to a Java/native call. In theory, the overhead of native/Java callbacks could also be within two to three times of Java/Java calls. In practice, however, native/Java callbacks are relatively infrequent. Virtual machine implementations do not usually optimize the performance of callbacks. At the time of this writing many production virtual machine implementations are such that the overhead of a native/Java callback can be as much as ten times higher than a Java/Java call.
JNI将类实例和数组类型(jobject, jclass, jstring, jarray等)当成不透明引用,本地代码从不直接查看这些引用的内容,而是用JNI函数来获取内容,你只需要关心JNI中的各种引用类型即可:
什么是局部引用,全局引用,它们有什么不同?下面将用一系列例子来说明。
大多数JNI函数会创建局部引用,涉及到所有jobject的子类,如jclass, jstring, and jarray等。一个局部引用只在本地函数内创建它时才可用,并且只能该函数内使用。所有在本地函数内创建的局部引用都会在该函数返回后释放(即使被引用的对象还一直存在,该局部引用也不可以继续使用了)。
不能保存局部引用在static变量中,并希望在后续调用中继续使用该局部引用。例如下面的例子是错误的:
/* This code is illegal */
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;
jmethodID cid;
jcharArray elemArr;
jstring result;
if (stringClass == NULL) {
stringClass = (*env)->FindClass(env,
"java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* It is wrong to use the cached stringClass here, because it may be invalid. */
cid = env->GetMethodID(stringClass, "", "([C)V");
...
elemArr = env->NewCharArray(len);
...
result = env->NewObject(stringClass, cid, elemArr);
env->DeleteLocalRef(elemArr);
return result;
}
这个函数结束后,stringClass变量存储的局部引用也会被释放,下次再调用这个函数尝试使用stringClass变量里的局部引用时会出现内存错误或崩溃了。
有两种方式另局部引用失效,一种是上面提到的,本地方法返回后局部引用自动释放,另一种是显式调用DeleteLocalRef。为什么需要显式地调用呢?局部引用在失效前会阻止垃圾收集器回收其引用的对象,显式调用后,可以立即释放。
局部引用不能在线程间共享,只能在创建它的那个线程中使用。
全局引用可以在多个函数间共用,也可以在多个线程间共用,直到显式释放。跟局部引用一样,全局引用失效前也会阻止垃圾收集器回收其引用的对象,但跟局部引用不同的是,很多JNI函数会创建局部引用,而全局引用只能通过NewGlobalRef来创建。
jstring MyNewString(JNIEnv * env, const jchar* chars, jint len) {
static jclass stringClass = NULL;
jmethodID cid;
jcharArray elemArr;
jstring result;
if (stringClass == NULL) {
jclass localRecCls = env->FindClass("java/lang/String");
if (localRecCls == NULL) {
return NULL;
}
stringClass = static_cast(env->NewGlobalRef(localRecCls));
env->DeleteLocalRef(localRecCls);
if (stringClass == NULL) {
return NULL;/* exception thrown */
}
}
......
}
用NewGlobalWeakRef创建弱全局引用,用DeleteGlobalWeakRef释放。跟全局引用一样,弱全局引用可以在多个函数间共用,也可以在多个线程间共用;跟全局引用不同的是,弱全局引用不会阻止垃圾收集器回收其引用的对象。
jstring MyNewString(JNIEnv * env, const jchar* chars, jint len) {
static jclass stringClass = NULL;
jmethodID cid;
jcharArray elemArr;
jstring result;
if (stringClass == NULL) {
jclass localRecCls = env->FindClass("java/lang/String");
if (localRecCls == NULL) {
return NULL;
}
stringClass = static_cast(env->NewWeakGlobalRef (localRecCls));
env->DeleteLocalRef(localRecCls);
if (stringClass == NULL) {
return NULL;/* exception thrown */
}
}
......
}
给定两个引用,可以检查是否指向同一个对象:
jboolean IsSameObject(jobject ref1, jobject ref2)
返回JNI_TRUE( 或者1 )表明是指向同一个,相反JNI_FALSE( 或者0 )。通过这个函数,可以检查引用是否指向空对象
jboolean ret = env->IsSameObject(ref, NULL);
或者
ref == NULL
IsSameObject对弱全局引用有特殊的作用,可以用来检查弱全局引用是否仍指向一个有效的活动对象,假设wobj是一个弱全局引用,
env->IsSameObject(wobj, NULL);
返回JNI_TRUE表明其引用的对象已经被回收,返回JNI_FALSE表明其仍然引用着一个有效的活动对象。
JNI的每个引用本身都会占用一定的内存,此外还有其引用的对象也占用着内存。作为JNI编程人员,你应该注意引用同时存在的数量,特别是注意引用数量的上限,过多地创建引用,会导致内存耗尽。
在大多数情况下,不需要担心本地函数中创建的局部引用,会被自动释放。然而,有时需要显式释放,以避免过多的内存占用。考虑下面的案例:
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
env->DeleteLocalRef(jstr);
}
/* A native method implementation */
JNIEXPORT void JNICALL
Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
lref = ... /* a large Java object */
... /* last use of lref */
(*env)->DeleteLocalRef(env, lref);
lengthyComputation(); /* may take some time */
return; /* all local refs are freed */
}
Java 2 SDK release 1.2开始另外提供了管理局部引用的一系列JNI函数: EnsureLocalCapacity, NewLocalRef, PushLocalFrame, PopLocalFrame。JNI规范指出,任何实现JNI规范的JVM,必须确保每个本地函数至少可以创建16个局部引用(可以理解为虚拟机默认支持创建16个局部引用)。实际经验表明,这个数量已经满足大多数不需要和JVM中内部对象有太多交互的本地方函数。如果需要创建更多的引用,可以通过调用EnsureLocalCapacity函数,确保在当前线程中创建指定数量的局部引用,如果创建成功则返回0,否则创建失败,并抛出OutOfMemoryError异常。EnsureLocalCapacity这个函数是1.2以上版本才提供的,为了向下兼容,在编译的时候,如果申请创建的局部引用超过了本地引用的最大容量,在运行时JVM会调用FatalError函数使程序强制退出。在开发过程当中,可以为JVM添加-verbose:jni参数,在编译的时如果发现本地代码在试图申请过多的引用时,会打印警告信息提示我们要注意。在下面的代码中,遍历数组时会获取每个元素的引用,使用完了之后不手动删除,不考虑内存因素的情况下,它可以为这种创建大量的局部引用提供足够的空间。由于没有及时删除局部引用,因此在函数执行期间,会消耗更多的内存。
/* The number of local references to be created is equal to
the length of the array. */
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {
... /* out of memory */
}
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
/* DeleteLocalRef is no longer necessary */
}
另外,我们也可以使用Push/PopLocalFrame来创建局部引用的内部作用域,例如把上面的例子重写:
#define N_REFS ... /* the maximum number of local references
used in each iteration */
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) < 0) {
... /* out of memory */
}
jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
(*env)->PopLocalFrame(env, NULL);
}
PushLocalFrame为指定数量的局部引用创建了新的作用域, PopLocalFrame则销毁最上层的作用域,释放该作用域所有的局部引用。使用Push/PopLocalFrame函数可以管理局部引用的生命周期,不用再时刻关注运行过程中创建的每个局部引用。在上面的例子中,如果在处理jstr的过程当中又创建了局部引用,则PopLocalFrame执行时,这些局部引用将全都会被销毁。在调用PopLocalFrame销毁当前frame中的所有引用前,如果第二个参数result不为空,会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame中。请看下面的示例:
// 函数原型
jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);
jstring other_jstr;
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
... /*内存溢出*/
}
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* 使用jstr */
if (i == 2) {
other_jstr = jstr;
}
other_jstr = (*env)->PopLocalFrame(env, other_jstr); // 销毁局部引用栈前返回指定的引用
}
当你写工具函数,需要返回局部引用时,NewLocalRef很有用.
EnsureLocalCapacity或PushLocalFrame可设置超过16个局部引用的默认数量,Java VM会尝试分配内存给超出的数量,但内存不一定足够可用,如果分配失败,VM自动退出。Java 2 SDK release 1.2支持命令行选项:-verbose:jni ,开启后如果发现本地代码在试图申请过多的引用时,会打印警告信息提示我们要注意.
DeleteGlobalRef, DeleteWeakGlobalRef
前面对三种引用已做了一个全面的介绍,下面来总结一下引用的管理规则和使用时的一些注意事项,使用好引用的目的就是为了减少内存使用和对象被引用保持而不能释放,造成内存浪费。
通常情况下,有两种本地代码使用引用时要注意:
while (JNI_TRUE) {
jstring infoString = GetInfoString(info);
... /* process infoString */
??? /* we need to call DeleteLocalRef, DeleteGlobalRef,
or DeleteWeakGlobalRef depending on the type of
reference returned by GetInfoString. */
}
函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用,我们改造一下MyNewString这个函数,演示一下这个函数的用法。下面的MyNewString是把一个被频繁调用的字符串“CommonString”缓存在了全局引用里:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jstring result;
/* wstrncmp compares two Unicode strings */
if (wstrncmp("CommonString", chars, len) == 0) {
/* refers to the global ref caching "CommonString" */
static jstring cachedString = NULL;
if (cachedString == NULL) {
/* create cachedString for the first time */
jstring cachedStringLocal = ... ;
/* cache the result in a global reference */
cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);
}
return (*env)->NewLocalRef(env, cachedString);
}
... /* create the string as a local reference and store in result as a local reference */
return result;
}
基于全局引用创建一个局引用返回,也同样会阻止GC回收所引用的这个对象,因为它们指向的是同一个对象。
The Push/PopLocalFrame functions are especially convenient for managing the lifetime of local references. If you called PushLocalFrame on entry to a native function, calling PopLocalFrame before the native function returns ensures that all local references created during native function execution will be freed. The Push/PopLocalFrame functions are efficient. You are strongly encouraged to use them.
If you call PushLocalFrame on function entry, remember to call PopLocalFrame in all function exit paths. For example, the following function has one call to PushLocalFrame but needs multiple calls to PopLocalFrame:
jobject f(JNIEnv *env, ...)
{
jobject result;
if ((*env)->PushLocalFrame(env, 10) < 0) {
/* frame not pushed, no PopLocalFrame needed */
return NULL;
}
...
result = ...;
if (...) {
/* remember to pop local frame before return */
result = (*env)->PopLocalFrame(env, result);
return result;
}
...
result = (*env)->PopLocalFrame(env, result);
/* normal return */
return result;
}
Failing to place PopLocalFrame calls properly would lead to undefined behavior, such as virtual machine crashes.
The above example also illustrates why it is sometimes useful to specify the second argument to PopLocalFrame. The result local reference is initially created in the new frame constructed by PushLocalFrame. PopLocalFrame converts its second argument, result, to a new local reference in the previous frame before popping the topmost frame.
在本地代码中,许多时候我们调用JNI函数并检查返回值看是否出错,本章将看看检查到错误后怎样从错误中恢复过来。
我们将关注JNI函数调用出现的错误,而不是本地代码发生的错误,如果本地代码有系统调用,怎样检查错误需要看其相应的文档。不过如果涉及到本地代码回调Java API的方法,必须遵照下面描述的步骤,适当地检查可能已发生的异常并从函数执行中恢复过来。
We introduce JNI exception handling functions through a series of examples.
下面的程序展示了怎样声明一个抛出异常的本地方法:
package com.king.cmakedemo;
public class ExceptionUtil {
static {
System.loadLibrary("exception-lib");
}
public native void doit() throws IllegalArgumentException;
private void callback() throws NullPointerException {
throw new NullPointerException("CatchThrow.callback");
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_king_cmakedemo_ExceptionUtil_doit(
JNIEnv* env,
jobject obj) {
jthrowable exc;
jclass cls = env->GetObjectClass(obj);
jmethodID mid =
env->GetMethodID(cls, "callback", "()V");
if (mid == NULL) {
return;
}
env->CallVoidMethod(obj, mid);
exc = env->ExceptionOccurred();
if (exc) {
/* We don't do much with the exception, except that
we print a debug message for it, clear it, and
throw a new exception. */
jclass newExcCls;
env->ExceptionDescribe();
env->ExceptionClear();
newExcCls = env->FindClass("java/lang/IllegalArgumentException");
if (newExcCls == NULL) {
/* Unable to find the exception class, give up. */
return;
}
env->ThrowNew(newExcCls, "thrown from C code");
}
}
运行后打印结果:
java.lang.NullPointerException:
at CatchThrow.callback(CatchThrow.java)
at CatchThrow.doit(Native Method)
at CatchThrow.main(CatchThrow.java)
In Java:
java.lang.IllegalArgumentException: thrown from C code
callback这个Java方法抛出NullPointerException异常,当CallVoidMethod返回后程序控制权交给本地代码,本地代码通过JNI函数ExceptionOccurred检查异常,当检测到异常时,用ExceptionDescribe输出一条描述异常的信息,用ExceptionClear清除异常,然后抛出IllegalArgumentException异常来代替。
一个JNI即将抛出的异常(通过ThrowNew)不会立即打断本地函数的执行,这不同于Java语言的异常的行为,当Java异常发生后,VM自动将控制流程转到try/catch对应的异常语句,相反,在异常发生后,JNI编程人员必须明确地实现控制流程。
当需要抛出自己的异常处理逻辑时,需要二步,调用FindClass找到异常处理类,然后调用ThrowNew抛出一个异常。为了简化操作步聚,我们写一个工具函数,根据一个异常类名专门用来生成一个指定名字的异常:
void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
jclass cls = (*env)->FindClass(env, name);
/* if cls is NULL, an exception has already been thrown */
if (cls != NULL) {
(*env)->ThrowNew(env, cls, msg);
}
/* free the local ref */
(*env)->DeleteLocalRef(env, cls);
}
JNI编程人员必须预知可能发生的异常并编写代码来检测和处理,合适的异常处理有时显得沉闷乏味,但对于提高应用的健壮性很有必要。
有两种方法可以检测是否发生了错误:
本地代码可以用两种方式处理即将发生的异常:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
const jchar *cstr = (*env)->GetStringChars(env, jstr);
if (c_str == NULL) {
return;
}
...
if (...) { /* exception occurred */
(*env)->ReleaseStringChars(env, jstr, cstr);
return;
}
...
/* normal return */
(*env)->ReleaseStringChars(env, jstr, cstr);
}
异常处理的相关JNI函数总结:
…