原创作品,转载请注明:http://blog.csdn.net/fangwei1235/article/details/30485635
译文出处:http://developer.android.com/training/articles/perf-jni.html
JNI是Java Native Interface的缩写。他定义了Java代码和c/c++代码的交互方法。他是厂商中立的,支持从动态链接库载入代码,虽然麻烦但有时候很高效。如果你对他不熟悉,请参看Java Native Interface Specification。
JNI定义了两个主要数据结构,JavaVM和JNIEnv。两者本质上都是指向函数表指针的指针。C++版本中,他们被实现为类,包含函数表指针,以及每个JNI函数对应的成员函数(通过间接调用函数表的函数)。JavaVM提供调用接口函数,来创建和销毁JavaVM。理论上一个进程中可以有多个JavaVM,但是Android中只允许一个。
JNIEnv提供了大多数的JNI函数,native函数都会接收JNIEnv作为第一个参数。JNIEnv是线程本地存储的。因此,你不能在线程间共享JNIEnv。如果在某段代码中没有办法获得JNIEnv,你应该共享JavaVM,然后利用GetEnv函数来获得。对于JavaVM和JNIEnv,C的声明与C++不同。jni.h头文件根据他被包含进c或者c++的不同,提供了不同的typedef。因此,包含JNIEnv参数的头文件(可能被c或者c++工程包含)是不推荐的。
所有的线程都是Linux线程,被内核所调度。通常都是通过Java代码启动(Thread.start)。但是,也可以在任何地方(native)创建,然后attach到JavaVM。例如,通过pthread_create创建的线程可以使用AttachCurrentThread或者AttachCurrentThreadAsDeamon来attach。如果线程没有被attach,那么他没有JNIEnv,所以不能执行JNI调用。
attach 本地创建(native-created)的线程会导致java.lang.Thread对象被创建,并被加入“main”线程组,使他对调试器可见。对一个已经attach的线程调用AttachCurrentThread是一个no-op(什么也不做)。
Android不会暂停执行本地代码(native code)的线程。如果垃圾回收在执行,或者调试器发起暂停请求,Android会在该线程下次调用JNI call的时候暂停他。attach的线程必须在退出之前调用DetachCurrentThread。在Android2.0版本以上,可以使用pthread_key_create来定义一个销毁函数,该函数在线程退出之前自用调用。可以把用DetachCurrentThread放在这里。
如果你想要在本地代码中访问Java对象的域,你需要这么做:
获得类的引用:FindClass
获得域ID:GetFieldID
获得域的值,如:GetIntField
类似地,为了调用某个成员函数,你需要获类的引用和成员函数ID。这个ID通常是指向内部runtime数据结构的指针。查找会需要一些字符串比较操作,但是一旦你获得他们,调用会非常快。如果性能对你很重要,你可以实现查找好所有需要的ID,并在本地代码中cache他们。因为有一个进程只包含一个JavaVM的限制,这样做也是合理的。
在类被unload之前,类引用,域ID,成员函数ID被保证是有效的。类只会在以下下情况下被unload:一个ClassLoader的所有类都可以被垃圾回收。这种情况不会再Android中发生。然而,jclass是一个类引用,必须通过调用NewGlobalRef来保护。
如果你想要在当一个类被加载后,cache这些ID,并且在该类被重新加载后re-cache他们,可以在Java类中加入如下代码:
/*
* We use a class initializer to allow the native code to cache some
* field offsets. This native function looks up and caches interesting
* class/field/method IDs. Throws on failure.
*/
privatestaticnativevoid nativeInit();
static{
nativeInit();
}
在C/C++代码中创建nativeClassInit函数来执行ID查找任务。这些代码会在类被初始化的时候执行仅一次。如果被重新加载,它会被再次执行。
每一个传给本地函数(native method)的参数,和几乎每一个通过JNI函数返回的对象都是本地引用(Local Reference)。这意味着它的有效期是执行进程的该本地函数。即使对象本身在该本地函数返回后是有效的,该引用也是无效的。这一规则适用于多有jobject的子类,包括jclass, jstring, jarray。(如果扩展JNI检查被打开,runtime会提示你这些问题。)
得到非本地引用的唯一方法是,使用NewGlobalRef和NewWeakGlobalRef。如果你想要在较长时间内都拥有引用,使用全局引用。NewGlobalRef使用本地引用作为参数,返回一个全局引用。全局引用在调用DeleteGlobalRef之前都是有效的。如:
jclass localClass= env->FindClass("MyClass");
jclass globalClass=reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
所有JNI函数都支持本地引用和全局引用。同一个对象的引用具有不同的值是可能的。例如,对于同一个对象的连续NewGlobalRef调用的返回值有可能是不同的。因此,如果要检查两个引用是否指向同一个对象,必须使用isSameObject函数,而不是==。你也不能假设对象的引用是唯一的。这个代表一个对象的32bit值可能是不同的,而且代表不同对象的32bit值可能相同的。不要把jobject值当做key使用。
不要大量申请本地引用。如果你这样做了,可能你需要自己手动DeleteLocalRef而不是让JNI替你做。系统默认保留了16个本地变量的slot。如果需要更多,你要么删除正在使用的,要么使用EnsureLocalCapacity/PushLocalFrame来预留更多。
jfieldID和jmethodID是opaque type,不是对象引用,所以不能传给NewGlobalRef。GetStringUTFChars和GetByteArrayElements的返回值也不是对象引用。(他们可以在线程中传递,在Release调用前一直有效。)
有一个特殊情况需要特别注意。如果你使用AttachCurrentThread来attach一个本地线程,该线程不会释放本地引用,直到线程被detach。你需要手动删除他们。一般来讲,在本地代码中,在loop中创建的本地引用都需要手动删除。
Java使用UTF-16。为了方便起见,JNI也提供Modified UTF-8兼容的函数。Modified UTF-8对C很有用,因为它把\u0000编码成0xc0 0x80而不是0x00。它的优点是,你可以使用C风格的NULL-terminated字符串。缺点是,你不能传递任意的UTF-8数据给JNI。
通常使用UTF-16字符串会更快。Android在GetStringChars中不需要一个拷贝,而在GetStringUTFChars中需要创建拷贝并转化为UTF-8。UTF-16字符串不是NULL-terminated的,\u0000是允许的。
不要忘记Release你Get的字符串。字符串函数返回jchar*或者jbyte*。他们是C风格的指向原始数据的指针,而不是对象引用。在Release之前会一直有效。这意味着他们不会再本地函数返回时释放。
传递给NewStringUTF的数据必须是Modified UTF-8格式。一个普遍的错误是,从文件或者网络流读取数据,直接传给NewStringUTF,而不去过滤他们。除非你知道数据是7bit ASCII,否则你需要转换他们为Modified UTF-8格式。如果你不这么做,UTF-16转换可能会发生错误。扩展JNI检查会给你发出警告。
JNI提供了访问数组对象的方法。对象数组一次只能访问一个条目,primitive数组则可以被直接访问,就好像他们是被C声明的。
为了使接口尽可能高效,Get<PrimitiveType>ArrayElements调用族允许runtime要么返回指针,要么申请内存并且构造一个拷贝。返回的指针在Release调用之前一直有效(这意味着,如果返回的是指针,数组对象会一直保留,且不能relocate)。你必须Release所有Get的数组,同时,必须保证不能Release NULL指针。
你可以通过传递一个non-NULL指针给isCopy参数,表示数据需要拷贝而不是返回指针。这不是很常用。
Release调用使用mode参数,它的实现根据是否有拷贝而不同。如下:
0
Actual: the array object is un-pinned.
Copy: data is copied back. The buffer with the copy is freed.
JNI_COMMIT
Actual: does nothing.
Copy: data is copied back. The buffer with the copyis not freed.
JNI_ABORT
Actual: the array object is un-pinned. Earlier writes arenot aborted.
Copy: the buffer with the copy is freed; any changes to it are lost.
check isCopy的一个原因是:是否需要在对对象做更改后调用Release(JNI_COMMIT)。如果你交错更改动作和使用该数组的代码,你可以跳过no-op commit。另一个原因是为了有效处理JNI_ABORT。例如,你可能想要获得一个数组,直接修改它,传递给其它函数,最后丢弃更改。如果你知道JNI为你生成了拷贝,就不需要再去创建一个可编辑的拷贝。如果JNI传递给你原始数组,你需要生成自己的拷贝。
一个普遍的错误是,认为如果isCopy是false,可以不需要Release。如果没有生成拷贝,原来的数组内存会被pin down,不能被垃圾回收移动。JNI_COMMIT不会释放拷贝,你最终还是需要使用另外的flag释放一次。.
如果你想对Get<Type>ArrayElements和GetStringChars拷贝数据,有一种方法会很有用:
jbyte* data = env->GetByteArrayElements(array, NULL);
if(data!= NULL){
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
}
以上代码会获取数组,拷贝len byte数据,然后释放数组。Get调用要么pin要么拷贝数组内容。JNI_ABORT保证不会有第三个拷贝。以下代码可以达到相同目的:
env->GetByteArrayRegion(array,0, len, buffer);
它有以下优点:
一个JNI调用而不是2个,减少开销
不需要pin和额外拷贝
降低错误的可能性
同样地,你可以使用Set<Type>ArrayRegion把数据拷贝到数组对象,GetStringRegion 或GetStringUTFRegion从String对象拷贝字符。
在例外没有处理之前,你不能调用大多数JNI函数。你的代码应该意识到例外(通过函数的返回值,ExceptionCheck或者ExceptionQccured),然后返回,或者清除并处理例外。当例外发生时你能调用的JNI函数如下:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
很多JNI函数可能抛出例外,但是提供非常简单的方法来检测失败。例如,如果NewString返回non-NULL,你不需要检查例外。然而,如果你调用一个成员函数(使用CallObjectMethod之类),你必须总是检查例外,因为如果发生例外,返回值会无效。
Note that exceptions thrown by interpreted code do not unwind native stack frames, and Android does not yet support C++ exceptions. The JNI Throw and ThrowNew instructions just set an exception pointer in the current thread. Upon returning to managed from native code, the exception will be noted and handled appropriately.
Native code can "catch" an exception by callingExceptionCheck or ExceptionOccurred, and clear it withExceptionClear. As usual, discarding exceptions without handling them can lead to problems.
请注意,interpreted代码抛出的异常不会unwind本地栈帧,Android现在也不支持c++例外。JNI的Throw和ThrowNew只是在当前进程中设置例外指针。当从本地代码(C/C++)返回至Java,里外会被发,现并被合适的处理。
本地代码可以通过ExceptionCheck和ExceptionOccured捕获异常,然后通过ExceptionClear清理。丢弃未处理的异常会导致问题。
没有内建函数来处理Throwable对象。如果你想得到异常的字符串,你需要找到Throwable类,得到getMessage "()Ljava/lang/String;"的函数ID,调用它,如果不为空,使用GetStringUTFChars来获取。
JNI很少做error检查。error经常导致crash。Android提供CheckJNI模式。在这种模式下,JavaVM和JNIEnv函数表指针被转换为另外的函数表,新函数表的函数会在执行标准功能之前检查。
有几种方式可以开启CheckJNI。如果使用模拟器,CheckJNI默认开启。如果你有rooted设备,执行一下命令来开启CheckJNI:
adb shell stop
adb shell setprop dalvik.vm.checkjnitrue
adb shell start
当CheckJNI开启,你应该会在logcat输出中看到以下log:
DAndroidRuntime:CheckJNIis ON
如果拥有regular设备,可以使用命令:
adb shell setprop debug.checkjni1
这不会影响已经在运行的app,只会影响之后运行的。这种情况下,你会在新app运行时发现以下log:
DLate-enablingCheckJNI
使用System.loadLibrary调用加载本地代码。推荐的方法是:
在static块中调用System.loadLibrary。参数例:传递“fubar”来加载“libfubar.so”
提供本地函数:jint JNI_OnLoad(JavaVM* vm, void* reserved)
在JNI_OnLoad中,注册所有的本地函数。你应该把函数声明为static,使名字不会占用设备上符号表的空间。
JNI_OnLoad函数例:
jint JNI_OnLoad(JavaVM* vm,void* reserved)
{
JNIEnv* env;
if(vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)!= JNI_OK){
return-1;
}
// Get jclass with env->FindClass.
// Register methods with env->RegisterNatives.
return JNI_VERSION_1_6;
}
你也可以调用System.load(“libfubar.so”)。对android app,从context对象中得到应用的私有数据存储区域很有用。
这是推荐的方法,但不是唯一的。显示的注册不是必要的,JNI_OnLoad函数也不是必要。你能使用本地函数的“discovery”。但是不推荐。因为,如果函数简明错误,直到该函数被调用之前你都不会知道。
另外一个关于JIN_OnLoad的点:在这里的所有FindClass调用将会在ClassLoader的context内发生。该ClassLoader用来加载本地代码共享库。通常,FindClass使用解释栈顶端的方法的loader,如果没有,使用“system”ClassLoader。这使得JNI_OnLoad成为查找和cache类对象引用的方便的场所。
Android现在在32bit平台上运行。理论上它可以被编译成64bit兼容,但是不是现在的目标。一边情况下你不需要考虑这些。为了支持64bit指针,你需要把native执行很放进long而不是int。
当使用本地代码时,下述错误非常常见:
java.lang.UnsatisfiedLinkError:Library foo not found
在很多情况下,它的意思就是:库找不到。或者另一种情况:库存在但是不能被dlopen打开。错误的具体信息能在例外的log中找到。"library not found"例外的普遍原因是:
库不存在,或者不能被app访问。使用adb shell -l <path>来确认他是否存在和权限问题。
库不是通过NDK生成的。这可能是因为对不存在的函数或库有依赖。
另外一种UnsatisfiedLinkError 错误如下:
java.lang.UnsatisfiedLinkError: myfunc
atFoo.myfunc(NativeMethod)
atFoo.main(Foo.java:10)
通过logcat, 你能发现:
W/dalvikvm( 880):No implementation found fornativeLFoo;.myfunc()V
这意味着runtime尝试找匹配的函数,但是失败了。普遍的原因是:
库没有被载入。可以检查logcat输出来确认这一点。
函数没有被找到是因为名字签名不匹配。可能的原因是:
因为lazy method lookup的原因,不能通过extern "C" 和JNIEXPORT声明C++函数。在Ice Cream Sandwich版本之前,JNIEXPORT宏是不正确的,所以旧的jni.h和新的GCC一起不能正常工作。你可以使用arm-eabi-nm来查看库里面的符号。如果他们看起来很乱(比如_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass 而不是Java_Foo_myfunc),或者符号类型是“t”而不是“T”,那么你需要调整这些声明。
对于明确注册(本地函数),可能会出现函数签名拼写错误的情况。
使用javah来自动生成JNI头文件能避免很多问题。
Make sure that the class name string has the correct format. JNI class names start with the package name and are separated with slashes, such as java/lang/String. If you're looking up an array class, you need to start with the appropriate number of square brackets and must also wrap the class with 'L' and ';', so a one-dimensional array ofString would be [Ljava/lang/String;.
首先确认类名字符串没有拼写错误。JNI类名以包名开始,使用“/”隔开,如java/lang/String。如果是数组类,你需要在类名前使用“[”,并以“L”和“;”包装。如,一维String数组的名字是[Ljava/lang/String;。
如果类名没有问题,可能是类装载器问题。FindClass会从和你的程序关联的类装载器开始查找类。它会检查调用栈,例如:
Foo.myfunc(NativeMethod)
Foo.main(Foo.java:10)
dalvik.system.NativeStart.main(NativeMethod)
顶端的方法是Foo.myfunc.FindClass 找到与Foo关联的ClassLoader对象并使用它。通常情况下,这么做正是你想要的。但是,如果你自己创建线程(比如使用pthread_create创建,然后AttachCurrentThread来attach),就会遇到问题。现在调用栈会是:
dalvik.system.NativeStart.run(NativeMethod)
顶端函数是NativeStart.run, 他并不是你程序的一部分。如果你在这个线程中调用FindClass,JavaVM会使用系统的类装载器,而不是和你的程序关联的类装载器,所以试图查找程序相关的类会失败。
以下方法可以解决这个问题:
在JNI_OnLoad中执行FindClass一次,然后将类引用cache以备以后使用。所有在JNI_OnLoad中执行的FindClass会使用与调用System.loadlibrary函数相关联的类加载器(这是一个特例,是为了使库初始化更加方便)。如果你的代码加载这个库,FindClass就会使用与你的代码关联的类加载器。
将类的一个实例传给该本地函数。声明本地函数接受一个Class参数,将Foo.class传给他。
在某处缓存类装载器对象的引用,然后直接调用loadClass。这会比较费力。
You may find yourself in a situation where you need to access a large buffer of raw data from both managed and native code. Common examples include manipulation of bitmaps or sound samples. There are two basic approaches.
你可能会碰到需要在Java端和本地端同时访问原生数据缓冲。如声音取样和bitmap数据。有两种基本方法。
你可以用byte[]存储数据。Java层的访问非常快。在本地端,如果你不拷贝,就不能保证能够访问这些数据。在某些实现中,GetByteArrayElements和GetPrimitiveArrayCritical会返回Java heap中的原生数据指针,但是在另外的实现中,会在本地层申请一个缓冲并拷贝数据。
另一种方法是将数据存储在直接ByteBuffer中。他可以通过java.nio.ByteBuffer.allocateDirect或JNI函数NewDirectByteBuffer创建。与普通byte缓冲不同,内存不是在Java Heap中申请的,所以能够在本地代码中直接访问(通过GetDirectBufferAddress获取地址)。但是,在Java层的访问可能非常慢(取决于java.nio.ByteBuffer.allocateDirect的实现)。
选择哪一个取决于两个因素:
操作数据最频繁的是Java端还是本地端(C++)?
如果数据最终被传递给系统API,需要什么样的形式?(比如,如果数据最终以byte[]的形式传递给一个函数,使用ByteBuffer就不合适)