闲来翻译了一篇官方的JNI Tips,网上看到的翻译版本要么是时间久了不同步了,要么翻译的过于生硬,看得我怀疑自己的中文是否及格,因此自己翻译再理解下,算是温故而知新。
JNI is the Java Native Interface. It defines a way for the bytecode that Android compiles from managed code (written in the Java or Kotlin programming languages) to interact with native code (written in C/C++). JNI is vendor-neutral, has support for loading code from dynamic shared libraries, and while cumbersome at times is reasonably efficient.
JNI 是Java Native Interface, 它定义了一种用于Android从托管代码(用Java或者Kotlin编写的代码)编译得到的字节码和native代码交互的方式。 JNI 是无厂商中立的,它支持动态地加载shared libraries(用C/C++编写的),虽然这样会稍显麻烦,但有时这是相当有效的。
Note: Because Android compiles Kotlin to ART-friendly bytecode in a similar manner as the Java programming language, you can apply the guidance on this page to both the Kotlin and Java programming languages in terms of JNI architecture and its associated costs. To learn more, see Kotlin and Android.
因为Android会编译kotlin会得到跟java一样对ART运行同样友好的字节码,所以这篇文章对你学习kotlin和java是一样有用的,如果想了解更多请查阅Kotlin和Android.
如果你对JNI还不是太熟悉,可以先通读Java Native Interface Specification这篇文章来对JNI如何工作以及哪些特性可用有个大致的印象。这种接口的一些方面不能立即一读就显而易见,所以你会发现接下来的几个章节很有用处。
To browse global JNI references and see where global JNI references are created and deleted, use the JNI heap view in the Memory Profiler in Android Studio 3.2 and higher.
想看全局的JNI references和了解全局JNI references是如何创建和删除的,可使用Memory Profiler的JNI heap view,它在至少Android Studio 3.2版本里提供。
General tips
Try to minimize the footprint of your JNI layer. There are several dimensions to consider here. Your JNI solution should try to follow these guidelines (listed below by order of importance, beginning with the most important):
关于如何尽量减少JNI层的占用空间,这里有几个需要遵循的准则,你的JNI应该尽可能按照这些指导方针来实现(下面以重要性列优先级,最上面的是最重要的)
- Minimize marshalling of resources across the JNI layer. Marshalling across the JNI layer has non-trivial costs. Try to design an interface that minimizes the amount of data you need to marshall and the frequency with which you must marshall data.
最小化跨JNI层的资源传输。跨越JNI层进行传输具有非常重要的成本。尝试设计一个界面,以最大限度地减少传输所需的数据量以及必须对数据进行传输的频率。
- Avoid asynchronous communication between code written in a managed programming language and code written in C++ when possible. This will keep your JNI interface easier to maintain. You can typically simplify asynchronous UI updates by keeping the async update in the same language as the UI. For example, instead of invoking a C++ function from the UI thread in the Java code via JNI, it's better to do a callback between two threads in the Java programming language, with one of them making a blocking C++ call and then notifying the UI thread when the blocking call is complete.
尽可能避免托管(Java或者Kotlin)代码与C++代码进行异步通信,这么做可以使你的JNI 接口更易于维护。你通常应通过使用与接口相同的语言保持异步更新来简化异步UI更新。例如,不应该在UI线程中用java封装代码通过JNI来调用C++函数,而是最好在Java编程语言中的两个线程之间建立回调,其中一个线程进行阻塞C++调用,然后当阻塞调用执行结束后再通知UI线程完成了。
- Minimize the number of threads that need to touch or be touched by JNI. If you do need to utilize thread pools in both the Java and C++ languages, try to keep JNI communication between the pool owners rather than between individual worker threads.
减少需要创建的线程数量,通过JNI创建的线程也要尽可能减少。如果你真要在Java和C++中使用线程池,尽量保持在同一个线程池创建的线程之间的JNI通信,而不是单个工作线程之间的通信。
- Keep your interface code in a low number of easily identified C++ and Java source locations to facilitate future refactors. Consider using a JNI auto-generation library as appropriate.
将你的JNI接口代码尽可能放在集中的地方,不要到处存放,这样方便以后进行代码重构,可以适当考虑自动生成JNI的库。
JavaVM and JNIEnv
JNI defines two key data structures, "JavaVM" and "JNIEnv". Both of these are essentially pointers to pointers to function tables. (In the C++ version, they're classes with a pointer to a function table and a member function for each JNI function that indirects through the table.) The JavaVM provides the "invocation interface" functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one.
JNI定义了两种关键数据结构,“JavaVM”和“JNIEnv”。它们本质上都是指向函数表指针的指针(在C++版本中,它们被定义为类,该类包含一个指向函数表的指针,以及一系列可以通过这个函数表间接地访问对应的JNI函数的成员函数)。JavaVM提供“调用接口(invocation interface)”函数, 允许你创建和销毁一个JavaVM。理论上你可以在一个进程中拥有多个JavaVM对象,但安卓只允许一个。
The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.
JNIEnv 提供了绝大部分JNI函数,你的native函数的第一个参数都是JNIEnv。
The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads. If a piece of code has no other way to get its JNIEnv, you should share the JavaVM, and use GetEnv to discover the thread's JNIEnv. (Assuming it has one; see AttachCurrentThread below.)
JNIEnv是用作线程局部存储。因此,你不能在线程间共享一个JNIEnv变量。如果在一段代码中没有其它办法获得它的JNIEnv,你可以共享JavaVM对象,使用GetEnv来取得该线程下的JNIEnv(如果该线程有一个JavaVM的话;见下面的AttachCurrentThread)。
The C declarations of JNIEnv and JavaVM are different from the C++ declarations. The "jni.h" include file provides different typedefs depending on whether it's included into C or C++. For this reason it's a bad idea to include JNIEnv arguments in header files included by both languages. (Put another way: if your header file requires #ifdef __cplusplus, you may have to do some extra work if anything in that header refers to JNIEnv.)
C语言对JNIEnv和JavaVM的定义和C++是不同的,头文件“jni.h”根据它是以C还是以C++模式包含来提供不同的类型定义(typedefs)。因此,不建议把JNIEnv参数放到可能被两种语言引入的头文件中(换一句话说:如果你的头文件需要#ifdef __cplusplus,你可能不得不在任何涉及到JNIEnv的内容处都要做些额外的工作)。
Threads
All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create can be attached with the JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls.
所有的线程都是Linux线程,都有linux内核统一调度。他们通常都是由托管代码(Java或Kotlin)启动(通过使用Thread.start),但它们也能够在其他任何地方创建,然后连接(attach)到JavaVM。例如,一个用pthread_create启动的线程能够使用JNI AttachCurrentThread 或 AttachCurrentThreadAsDaemon函数连接到JavaVM。在一个线程成功连接(attach)之前,它没有JNIEnv,不能够调用JNI函数。
Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main" ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread is a no-op.
连接一个本地环境创建的线程会触发构造一个java.lang.Thread对象,然后其被添加到主线程群组(main ThreadGroup),以让调试器可以探测到。对一个已经连接的线程使用AttachCurrentThread不做任何操作(no-op)。
Android does not suspend threads executing native code. If garbage collection is in progress, or the debugger has issued a suspend request, Android will pause the thread the next time it makes a JNI call.
Android不能中止正在执行本地代码的线程。如果正在进行垃圾回收,或者调试器已发出了中止请求,安卓会在下一次调用JNI函数的时候中止线程。
Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward, in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.)
连接过的(attached)线程在它们退出之前必须通过JNI调用DetachCurrentThread。如果你觉得直接这样编写不太优雅,在安卓2.0(Eclair)及以上, 你可以使用pthread_key_create来定义一个析构函数,它将会在线程退出时被调用,你可以在那儿调用DetachCurrentThread (使用生成的key与pthread_setspecific将JNIEnv存储到线程局部空间内;这样JNIEnv能够作为参数传入到析构函数当中去)。
jclass, jmethodID, and jfieldID
If you want to access an object's field from native code, you would do the following:
如果你想通过native代码访问对象的属性,你要做的事情应该如下:
- Get the class object reference for the class with FindClass
- Get the field ID for the field with GetFieldID
- Get the contents of the field with something appropriate, such as GetIntField
通过使用FindClass方法获取class对象的引用
通过使用GetFieldID方法获取field的ID
通过使用例如GetIntField这样的方法获取field的值
Similarly, to call a method, you'd first get a class object reference and then a method ID. The IDs are often just pointers to internal runtime data structures. Looking them up may require several string comparisons, but once you have them the actual call to get the field or invoke the method is very quick.
类似的要执行一个方法,你首先要获取class对象的引用,再然后就是方法的ID。ID通常就是指向内部运行结构体的指针,遍历寻找他们可能需要一些字符串比较工作,但是一旦找到它们,获取field值或者调用方法是非常快的。
If performance is important, it's useful to look the values up once and cache the results in your native code. Because there is a limit of one JavaVM per process, it's reasonable to store this data in a static local structure.
如果对性能要求很高,最好不要多次读取这些值,应当将读取的结果存在你的native代码中。因为每个进程JavaVM被限制为只能有一个。将它们存在本地静态struct中是推荐做法。
The class references, field IDs, and method IDs are guaranteed valid until the class is unloaded. Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android. Note however that the jclass is a class reference and must be protected with a call to NewGlobalRef (see the next section).
类引用(class reference),字段ID(field ID)以及方法ID(method ID)在类被卸载前都是有效的。如果与一个类加载器(ClassLoader)相关的所有类都能够被垃圾回收,但是这种情况在安卓上是罕见甚至不可能出现,只有这时类才被卸载。注意虽然jclass是一个类引用,但是必须要调用NewGlobalRef保护起来(见下个章节)。
If you would like to cache the IDs when a class is loaded, and automatically re-cache them if the class is ever unloaded and reloaded, the correct way to initialize the IDs is to add a piece of code that looks like this to the appropriate class:
当一个类被加载时如果你想缓存些ID,而后当这个类被卸载后再次载入时能够自动地更新这些缓存ID,正确做法是在对应的类中添加一段像下面的代码来初始化这些ID:
/*
* 我们在一个类初始化时调用本地方法来缓存一些字段的偏移信息
* 这个本地方法查找并缓存你感兴趣的class/field/method ID
* 失败时抛出异常
*/
private static native void nativeInit();
static {
nativeInit();
}
Create a nativeClassInit method in your C/C++ code that performs the ID lookups. The code will be executed once, when the class is initialized. If the class is ever unloaded and then reloaded, it will be executed again.
在C/C++里创建一个叫nativeClassInit的方法用于寻找那些ID。一旦代码初始化ok,这段代码就会执行,如果类被卸载然后又重新被加载这个方法会再次执行一次。
Local and global references
Every argument passed to a native method, and almost every object returned by a JNI function is a "local reference". This means that it's valid for the duration of the current native method in the current thread. Even if the object itself continues to live on after the native method returns, the reference is not valid.
每一个传入本地方法的参数和对象通过JNI方法返回的都是一个局部引用。这意味着当前本地方法所在的当前线程中这个本地引用是有效的。但是随后即使这个对象本身在本地方法返回之后仍然存在,这个引用也是无效的。
This applies to all sub-classes of jobject, including jclass, jstring, and jarray. (The runtime will warn you about most reference mis-uses when extended JNI checks are enabled.)
这个特性适用于所有jobject子类,包括jclass,jstring以及jarry。(当JNI扩展检查是打开的时候,运行时会警告你对大部分对象引用的误用)
The only way to get non-local references is via the functions NewGlobalRef and NewWeakGlobalRef.
唯一获得非局部引用(即更长时间引用)的办法是通过NewGlobalRef和NewWeakGlobalRef这两个方法。
If you want to hold on to a reference for a longer period, you must use a "global" reference. The NewGlobalRef function takes the local reference as an argument and returns a global one. The global reference is guaranteed to be valid until you call DeleteGlobalRef.
如果你想持有一个更长时间的引用,你不得不用全局引用。NewGlobalRef函数以一个局部引用作为参数并且返回一个全局引用。全局引用能够保证在你调用DeleteGlobalRef前都是有效的。
This pattern is commonly used when caching a jclass returned from FindClass, e.g.:
这种模式通常被用在缓存一个从FindClass返回的jclass对象的时候,例如:
jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast(env->NewGlobalRef(localClass));
All JNI methods accept both local and global references as arguments. It's possible for references to the same object to have different values. For example, the return values from consecutive calls to NewGlobalRef on the same object may be different. To see if two references refer to the same object, you must use the IsSameObject function. Never compare references with == in native code.
所有的JNI方法都接收局部引用和全局引用作为参数。相同对象的引用却可能具有不同的值。例如,用相同对象连续地调用NewGlobalRef得到返回值可能是不同的。为了检查两个引用是否指向的是同一个对象,你必须使用IsSameObject函数。绝不要在本地代码中用==符号来比较两个引用。
One consequence of this is that you must not assume object references are constant or unique in native code. The 32-bit value representing an object may be different from one invocation of a method to the next, and it's possible that two different objects could have the same 32-bit value on consecutive calls. Do not use jobject values as keys.
因此得出一个结论:你千万不能在本地代码中假设对象引用为常量或者唯一的,一个32位值代表一个对象的从方法,从第一次调用到下一次调用可能有不同的值。在连续的调用过程中两个不同的对象却可能拥有相同的32位值。因此,不要使用jobject的值作为key.
Programmers are required to "not excessively allocate" local references. In practical terms this means that if you're creating large numbers of local references, perhaps while running through an array of objects, you should free them manually with DeleteLocalRef instead of letting JNI do it for you. The implementation is only required to reserve slots for 16 local references, so if you need more than that you should either delete as you go or use EnsureLocalCapacity/PushLocalFrame to reserve more.
开发者们应该不要“过度分配”局部引用。在实际操作中这意味着如果你正在创建大量的局部引用,或许是通过对象数组,你应该使用DeleteLocalRef手动地释放它们,而不是寄希望JNI来为你做这些。实现上只预留了16个局部引用的空间,所以如果你需要更多,要么你删掉以前的,要么使用EnsureLocalCapacity/PushLocalFrame来预留更多。
Note that jfieldIDs and jmethodIDs are opaque types, not object references, and should not be passed to NewGlobalRef. The raw data pointers returned by functions like GetStringUTFChars and GetByteArrayElements are also not objects. (They may be passed between threads, and are valid until the matching Release call.)
注意jfieldID和jmethodID是映射类型(opaque types),不是对象引用,不应该被传入到NewGlobalRef。原始数据指针,像GetStringUTFChars和GetByteArrayElements的返回值,也都不是对象(它们能够在线程间传递,并且在调用对应的Release函数之前都是有效的)。
One unusual case deserves separate mention. If you attach a native thread with AttachCurrentThread, the code you are running will never automatically free local references until the thread detaches. Any local references you create will have to be deleted manually. In general, any native code that creates local references in a loop probably needs to do some manual deletion.
还有一种不常见的情况值得一提,如果你使用AttachCurrentThread连接(attach)了本地进程,正在运行的代码在线程分离(detach)之前决不会自动释放局部引用。你创建的任何局部引用必须手动删除。通常,任何在循环中创建局部引用的本地代码可能都需要做一些手动删除。
Be careful using global references. Global references can be unavoidable, but they are difficult to debug and can cause difficult-to-diagnose memory (mis)behaviors. All else being equal, a solution with fewer global references is probably better.
在使用全局引用时候需要小心对待,全局引用的使用是不可避免的,但是它们的调试比较困难,而且还会导致难以诊断的内存行为。其他条件一样,最少的全局引用来实现功能或许更好。
UTF-8 and UTF-16 strings
The Java programming language uses UTF-16. For convenience, JNI provides methods that work with Modified UTF-8 as well. The modified encoding is useful for C code because it encodes \u0000 as 0xc0 0x80 instead of 0x00. The nice thing about this is that you can count on having C-style zero-terminated strings, suitable for use with standard libc string functions. The down side is that you cannot pass arbitrary UTF-8 data to JNI and expect it to work correctly.
Java使用UTF-16格式,为了方便,JNI也提供了支持变形UTF-8(Modified UTF-8)的方法。这种变形编码对于C代码是非常有用的,因为它将\u0000编码成0xc0 0x80,而不是0x00。最惬意的事情是你能在具有C风格的以\0结束的字符串上计数,同时兼容标准的libc字符串函数。不好的一面是你不能传入随意的UTF-8数据到JNI函数而还指望它正常工作。
If possible, it's usually faster to operate with UTF-16 strings. Android currently does not require a copy in GetStringChars, whereas GetStringUTFChars requires an allocation and a conversion to UTF-8. Note that UTF-16 strings are not zero-terminated, and \u0000 is allowed, so you need to hang on to the string length as well as the jchar pointer.
如果可能的话,直接操作UTF-16字符串通常更快些。Android当前在调用GetStringChars时不需要拷贝,而GetStringUTFChars需要一次分配并且转换为UTF-8格式。注意UTF-16字符串不是以零终止字符串,\u0000是被允许的,所以你需要像对jchar指针一样地处理字符串的长度。
Don't forget to Release the strings you Get. The string functions return jchar* or jbyte*, which are C-style pointers to primitive data rather than local references. They are guaranteed valid until Release is called, which means they are not released when the native method returns.
不要忘记释放你获得的strings。那些strings函数返回的jchar* 或者 jbyte* 本质是C语言指向基本数据类型的指针,并不是局部引用。它们在被Release调用前都是有效的,这意味着当本地方法返回时它们并不主动释放。
Data passed to NewStringUTF must be in Modified UTF-8 format. A common mistake is reading character data from a file or network stream and handing it to NewStringUTF without filtering it. Unless you know the data is valid MUTF-8 (or 7-bit ASCII, which is a compatible subset), you need to strip out invalid characters or convert them to proper Modified UTF-8 form. If you don't, the UTF-16 conversion is likely to provide unexpected results. CheckJNI—which is on by default for emulators—scans strings and aborts the VM if it receives invalid input.
传入NewStringUTF函数的数据必须是变形UTF-8格式。一种常见的错误情况是,从文件或者网络流中读取出的字符数据,没有过滤直接使用NewStringUTF处理。除非你确定数据是7位的ASCII格式,否则你需要剔除超出7位ASCII编码范围(high-ASCII)的字符或者将它们转换为对应的变形UTF-8格式。如果你没那样做,UTF-16的转换结果可能不会是你想要的结果。JNI扩展检查将会扫描字符串,然后警告你那些无效的数据,但是它们将不会发现所有潜在的风险。
Primitive arrays
JNI provides functions for accessing the contents of array objects. While arrays of objects must be accessed one entry at a time, arrays of primitives can be read and written directly as if they were declared in C.
JNI提供了一系列方法用于访问数组内容。对象数组的访问只能一次一条,但如果原生类型数组以C方式声明,则能够直接进行读写。
To make the interface as efficient as possible without constraining the VM implementation, the Get
为了让接口更有效率而不受VM实现的制约,Get
ArrayElements系列调用允许运行时返回一个指向实际元素的指针,或者是分配些内存然后拷贝一份。不论哪种方式,返回的原始指针在相应的Release调用之前都保证有效(这意味着,如果数据没被拷贝,实际的数组对象将会受到牵制,不能重新成为整理堆空间的一部分)。你必须释放(Release)每个你通过Get得到的数组。同时,如果Get调用失败,你必须确保你的代码在之后不会去尝试调用Release来释放一个空指针(NULL pointer)。
You can determine whether or not the data was copied by passing in a non-NULL pointer for the isCopy argument. This is rarely useful.
你可以用一个非空指针作为isCopy参数的值来决定数据是否会被拷贝。这相当有用。
The Release call takes a mode argument that can have one of three values. The actions performed by the runtime depend upon whether it returned a pointer to the actual data or a copy of it:
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 copy is not freed. -
JNI_ABORT
Actual: the array object is un-pinned. Earlier writes are not aborted.
Copy: the buffer with the copy is freed; any changes to it are lost.
0
真实的:数组对象不受牵制
拷贝的:数据被复制,备份空间将会被释放JNI_COMMIT
事实上:什么都没做
拷贝的:数据被复制,备份空间不会被释放JNI_ABORT
实际上:数组对象不受牵连,之前的写入不会被取消
拷贝的:备份空间将会被释放;里面所有的变更都会丢失
One reason for checking the isCopy flag is to know if you need to call Release with JNI_COMMIT after making changes to an array — if you're alternating between making changes and executing code that uses the contents of the array, you may be able to skip the no-op commit. Another possible reason for checking the flag is for efficient handling of JNI_ABORT. For example, you might want to get an array, modify it in place, pass pieces to other functions, and then discard the changes. If you know that JNI is making a new copy for you, there's no need to create another "editable" copy. If JNI is passing you the original, then you do need to make your own copy.
检查isCopy标识的一个原因是对一个数组做出变更后确认你是否需要传入JNI_COMMIT来调用Release函数。如果你交替地执行变更和读取数组内容的代码,你也许可以跳过无操作(no-op)的JNI_COMMIT。检查这个标识的另一个可能的原因是使用JNI_ABORT可以更高效。例如,你也许想得到一个数组,适当地修改它,传入部分到其他函数中,然后丢掉这些修改。如果你知道JNI是为你做了一份新的拷贝,就没有必要再创建另一份“可编辑的(editable)”的拷贝了。如果JNI传给你的是原始数组,这时你就需要创建一份你自己的拷贝了。
It is a common mistake (repeated in example code) to assume that you can skip the Release call if *isCopy is false. This is not the case. If no copy buffer was allocated, then the original memory must be pinned down and can't be moved by the garbage collector.
另一个常见的错误(在示例代码中出现过)是认为当isCopy是false时你就可以不调用Release, 实际上是没有这种情况的。如果没有分配备份空间,那么初始的内存空间会受到牵制,位置不能被垃圾回收器移动。
Also note that the JNI_COMMIT flag does not release the array, and you will need to call Release again with a different flag eventually.
同时需要注意的是JNI_COMMIT flag不会释放数组,最终你需要用不同的flag再次释放一遍。
Region calls
There is an alternative to calls like Get
当你想做的只是拷出或者拷进数据时,可以选择调用像GetArrayElements和GetStringChars这类非常有用的函数。想想下面:
jbyte* data = env->GetByteArrayElements(array, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
}
This grabs the array, copies the first len byte elements out of it, and then releases the array. Depending upon the implementation, the Get call will either pin or copy the array contents. The code copies the data (for perhaps a second time), then calls Release; in this case JNI_ABORT ensures there's no chance of a third copy.
这里获取到了数组,从当中拷贝出开头的len个字节元素,然后释放这个数组。根据代码的实现,Get函数将会牵制或者拷贝数组的内容。上面的代码拷贝了数据(为了可能的第二次),然后调用Release;这当中JNI_ABORT确保不存在第三份拷贝了。
One can accomplish the same thing more simply:
另一种更简单的实现方式:
env->GetByteArrayRegion(array, 0, len, buffer);
This has several advantages:
它有几个优势:
- Requires one JNI call instead of 2, reducing overhead.
- Doesn't require pinning or extra data copies.
- Reduces the risk of programmer error — no risk of forgetting to call Release after something fails.
- 只需要调用一个JNI函数而不是2个,减少了开销
- 不需要牵扯到别的数据拷贝
- 减少了代码错误风险,没有在某些错误后忘记调用Release的风险
Similarly, you can use the Set
类似的,你可以用Set
ArrayRegion拷贝数据到数组中,同时利用GetStringRegion和GetStringUTFRegionc从strings中拷贝字符串。
Exceptions
You must not call most JNI functions while an exception is pending. Your code is expected to notice the exception (via the function's return value, ExceptionCheck, or ExceptionOccurred) and return, or clear the exception and handle it.
当异常发生时你一定不能调用大部分的JNI函数。你的代码收到异常(通过函数的返回值,ExceptionCheck,或者ExceptionOccurred),然后返回,或者清除异常,处理掉。
The only JNI functions that you are allowed to call while an exception is pending are:
当异常发生时候你仅仅能调用的函数如下:
- DeleteGlobalRef
- DeleteLocalRef
- DeleteWeakGlobalRef
- ExceptionCheck
- ExceptionClear
- ExceptionDescribe
- ExceptionOccurred
- MonitorExit
- PopLocalFrame
- PushLocalFrame
- ReleaseArrayElements
- ReleasePrimitiveArrayCritical
- ReleaseStringChars
- ReleaseStringCritical
- ReleaseStringUTFChars
Many JNI calls can throw an exception, but often provide a simpler way of checking for failure. For example, if NewString returns a non-NULL value, you don't need to check for an exception. However, if you call a method (using a function like CallObjectMethod), you must always check for an exception, because the return value is not going to be valid if an exception was thrown.
很多JNI的调用会抛出异常,但往往提供了一个类似的方式用于检查失败。比如,如果NewString返回了一个不为空的值,你无需检查错误。如果你调用一个方法(使用了一个类似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.
注意中断代码抛出的异常不会展开本地调用堆栈信息,Android也还不支持C++异常。JNI Throw和ThrowNew指令仅仅是在当前线程中放入一个异常指针。从本地代码返回到托管代码时,异常将会被注意到,得到适当的处理。
Native code can "catch" an exception by calling ExceptionCheck or ExceptionOccurred, and clear it with ExceptionClear. As usual, discarding exceptions without handling them can lead to problems.
本地代码能通过调用ExceptionCheck或ExceptionOccurred来catch异常,并可以通过ExceptionClear来清除异常。通常来说,不处理异常直接丢弃异常会导致问题。
There are no built-in functions for manipulating the Throwable object itself, so if you want to (say) get the exception string you will need to find the Throwable class, look up the method ID for getMessage "()Ljava/lang/String;", invoke it, and if the result is non-NULL use GetStringUTFChars to get something you can hand to printf(3) or equivalent.
没有内建的函数来处理Throwable对象自身,所以如果你想获取异常字符串你需要找到Throwable class,再找到getMessage()"()Ljava/lang/String;"这个方法的ID,最后执行此方法,如果结果是非空则调用GetStringUTFChars得到结果,你可以传到printf(3) 或者其它类似功能的函数输出。
Extended checking
JNI does very little error checking. Errors usually result in a crash. Android also offers a mode called CheckJNI, where the JavaVM and JNIEnv function table pointers are switched to tables of functions that perform an extended series of checks before calling the standard implementation.
JNI的错误检查很少, 错误发生时通常会导致崩溃。Android也提供了一种模式,叫做CheckJNI,这当中JavaVM和JNIEnv函数表指针被换成了函数表,它在调用标准实现之前执行了一系列扩展检查的。
The additional checks include:
这些额外的检查包含如下:
- Arrays: attempting to allocate a negative-sized array.
- Bad pointers: passing a bad jarray/jclass/jobject/jstring to a JNI call, or passing a NULL pointer to a JNI call with a non-nullable argument.
- Class names: passing anything but the “java/lang/String” style of class name to a JNI call.
- Critical calls: making a JNI call between a “critical” get and its corresponding release.
- Direct ByteBuffers: passing bad arguments to NewDirectByteBuffer.
- Exceptions: making a JNI call while there’s an exception pending.
- JNIEnvs: using a JNIEnv from the wrong thread.
- jfieldIDs: using a NULL jfieldID, or using a jfieldID to set a field to a value of the wrong type (trying to assign a StringBuilder to a String field, say), or using a jfieldID for a static field to set an instance field or vice versa, or using a jfieldID from one class with instances of another class.
- jmethodIDs: using the wrong kind of jmethodID when making a Call*Method JNI call: incorrect return type, static/non-static mismatch, wrong type for ‘this’ (for non-static calls) or wrong class (for static calls).
- References: using DeleteGlobalRef/DeleteLocalRef on the wrong kind of reference.
- Release modes: passing a bad release mode to a release call (something other than 0, JNI_ABORT, or JNI_COMMIT).
- Type safety: returning an incompatible type from your native method (returning a StringBuilder from a method declared to return a String, say).
- UTF-8: passing an invalid Modified UTF-8 byte sequence to a JNI call.
- 数组:试图分配一个长度为负的数组。
- 坏指针:传入一个不完整jarray/jclass/jobject/jstring对象到JNI函数,或者调用JNI函数时使用空指针传入到一个不能为空的参数中去。
- 类名:传入了除“java/lang/String”之外的类名到JNI函数。
- 关键调用:在一个“关键的(critical)”get和它对应的release之间做出JNI调用。
- 直接的ByteBuffers:传入不正确的参数到NewDirectByteBuffer。
- 异常:当一个异常发生时调用了JNI函数。
- JNIEnvs:在错误的线程中使用一个JNIEnv。
- jfieldIDs:使用一个空jfieldID,或者使用jfieldID设置了一个错误类型的值到字段(比如说,试图将一个StringBuilder赋给String类型的域),或者使用一个静态字段下的jfieldID设置到一个实例的字段(instance field)反之亦然,或者使用的一个类的jfieldID却来自另一个类的实例。
- jmethodIDs:当调用Call*Method函数时时使用了类型错误的jmethodID:不正确的返回值,静态/非静态的不匹配,this的类型错误(对于非静态调用)或者错误的类(对于静态类调用)。
- 引用:在类型错误的引用上使用了DeleteGlobalRef/DeleteLocalRef。
- 释放模式:调用release使用一个不正确的释放模式(其它非 0,JNI_ABORT,JNI_COMMIT的值)。
- 类型安全:从你的本地代码中返回了一个不兼容的类型(比如说,从一个声明返回String的方法却返回了StringBuilder)。
- UTF-8:传入一个无效的变形UTF-8字节序列到JNI调用。
(Accessibility of methods and fields is still not checked: access restrictions don't apply to native code.)
(方法和域的可访问性仍然没有检查:访问限制对于本地代码并不适用。)
There are several ways to enable CheckJNI.
有几种方式启动CheckJNI
If you’re using the emulator, CheckJNI is on by default.
如果你在使用模拟器,CheckJNI默认是启用着的。
If you have a rooted device, you can use the following sequence of commands to restart the runtime with CheckJNI enabled:
如果你有一个root后的设备,你可以使用以下命令流程来重新启动运行时,让CheckJNI启用。
adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
In either of these cases, you’ll see something like this in your logcat output when the runtime starts:
不管以上那种情况,你都可以在runtime启动后在logcat中查看是否启用了CheckJNI:
D AndroidRuntime: CheckJNI is ON
If you have a regular device, you can use the following command:
如果你有一个常规设备,你可以输入以下命令:
adb shell setprop debug.checkjni 1
This won’t affect already-running apps, but any app launched from that point on will have CheckJNI enabled. (Change the property to any other value or simply rebooting will disable CheckJNI again.) In this case, you’ll see something like this in your logcat output the next time an app starts:
这个操作不会影响已经运行起来的App,但是从这一刻开始启动的App它的CheckJNI就是开启的。(改变这个属性值为其它值或者重启设备将又再次关闭CheckJNI)在这种情况下,下次你将在logcat的输出中看到如下信息:
D Late-enabling CheckJNI
You can also set the android:debuggable attribute in your application's manifest to turn on CheckJNI just for your app. Note that the Android build tools will do this automatically for certain build types.
你也可以通过在manifest中设置android:debuggable为true来开启你的App的CheckJNI。需要注意的是,Android 编译工具将自动为特定的build type做这件事情。
Native libraries
You can load native code from shared libraries with the standard System.loadLibrary. The preferred way to work with native methods is:
你可以通过使用标准的方法(System.loadLibrary)从共享库中加载本地代码, 和本地代码交互的推荐的做法如下:
- Call System.loadLibrary from a static class initializer. The argument is the "undecorated" library name, so to load "libfubar.so" you would pass in "fubar".
- Provide a JNI_OnLoad function: JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
- In your JNI_OnLoad, register all of your native methods using RegisterNatives.
- Build with -fvisibility=hidden so that only your JNI_OnLoad is exported from your library. This produces faster and smaller code, and avoids potential collisions with other libraries loaded into your app (but it creates less useful stack traces if you app crashes in native code).
- 在一个静态类初始化时调用System.loadLibrary(见之前的一个例子中,当中就使用了nativeClassInit)。参数是“未加修饰(undecorated)”的库名称,因此要加载“libfubar.so”,你需要传入“fubar”。
- 提供一个叫JNI_OnLoad的函数:JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
- 在JNI_OnLoad中,注册所有你的本地方法。你应该声明方法为“静态的(static)”因此名称不会占据设备上符号表的空间。
- 以-fvisibility=hidden构建,为的是在你的Library中仅仅让你的JNI_OnLoad是暴露的。这将产生更快更少的代码,同时避免潜在的与加载到App中的别的library冲突(但它会有个缺陷:当App的本地代码崩溃时候会创建更少的有用错误堆栈信息)。
The static initializer should look like this:
标准的初始化代码如下:
static {
System.loadLibrary("fubar");
}
The JNI_OnLoad function should look something like this if written in C++:
以C++de写法JNI_OnLoad函数应该定义如下:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
// Get jclass with env->FindClass.
// Register methods with env->RegisterNatives.
return JNI_VERSION_1_6;
}
You can also call System.load with the full path name of the shared library. For Android apps, you may find it useful to get the full path to the application's private data storage area from the context object.
你也可以使用共享库的全路径来调用System.load。对于Android app,你也许会发现从context对象中得到应用私有数据存储的全路径是非常有用的。
Using JNI_OnLoad is the recommended approach, but not the only approach. Explicit registration of native methods with RegisterNatives is not required, nor is it necessary that you provide any JNI_OnLoad function. You can instead use "discovery" of native methods that are named in a specific way (see the JNI spec for details), though this means that if a method signature is wrong, you won't know about it until the first time the method is actually invoked.
使用JNI_OnLoad是推荐的做法,但不是唯一的做法。显式的通过RegisterNatives注册本地方法不是强制的,提供JNI_OnLoad方法也不是必须的。你可以使用基于特殊命名的“发现(discovery)”方式来注册本地方法(更多细节见:JNI spec), 虽然这意味着如果一个方法的签名错了,直到真正调用它之前你是不知道这里的错误的。
If you have only one class with native methods, it makes sense for the call to System.loadLibrary to be in that class. Otherwise you should probably make the call from Application so you know that it's always loaded, and always loaded early.
如果定义本地方法只在一个类里定义,那么在这个类里调用System.loadLibrary是没有问题的。否则你可能需要在Application里调用,至少你是知道它们肯定是会被加载的,且总是很早被加载。
One other note about JNI_OnLoad: any FindClass calls you make from there will happen in the context of the class loader that was used to load the shared library. Normally FindClass uses the loader associated with the method at the top of the interpreted stack, or if there isn't one (because the thread was just attached) it uses the "system" class loader. This makes JNI_OnLoad a convenient place to look up and cache class object references.
关于JNI_OnLoad另一点注意的是:任何你在JNI_OnLoad中对FindClass的调用都发生在用作加载共享库的类加载器的上下文(context)中。一般FindClass使用与“调用栈”顶部方法相关的加载器,如果当中没有加载器(因为线程刚刚连接)则使用“系统(system)”类加载器。这就使得JNI_OnLoad成为一个查寻及缓存类引用很便利的地方。
64-bit considerations
To support architectures that use 64-bit pointers, use a long field rather than an int when storing a pointer to a native structure in a Java field.
为了支持64位的架构平台,当存储指向本地结构的指针时候在Java中应使用long类型而不是int类型。
Unsupported features/backwards compatibility
All JNI 1.6 features are supported, with the following exception:
所有JNI 1.6版本都被支持,除了以下例外:
DefineClass is not implemented. Android does not use Java bytecodes or class files, so passing in binary class data doesn't work.
DefineClass没有实现,Android不使用Java字节码或者class文件,因此传入二进制class数据将不会有效。
For backward compatibility with older Android releases, you may need to be aware of:
对Android以前老版本的向后兼容性,你需要注意:
- Dynamic lookup of native functions
Until Android 2.0 (Eclair), the '$' character was not properly converted to "_00024" during searches for method names. Working around this requires using explicit registration or moving the native methods out of inner classes.
在Android 2.0(Eclair)之前,在搜索方法名称时,字符“$”不会转换为对应的“_00024”。要使它正常工作需要使用显式注册方式或者将本地方法的声明移出内部类。
- Detaching threads
Until Android 2.0 (Eclair), it was not possible to use a pthread_key_create destructor function to avoid the "thread must be detached before exit" check. (The runtime also uses a pthread key destructor function, so it'd be a race to see which gets called first.)
在Android 2.0(Eclair)之前,使用pthread_key_create析构函数来避免“退出前线程必须分离”检查是不可行的(运行时(runtime)也使用了一个pthread key析构函数,因此这是一场看谁先被调用的竞赛)。
- Weak global references
Until Android 2.2 (Froyo), weak global references were not implemented. Older versions will vigorously reject attempts to use them. You can use the Android platform version constants to test for support.
在Android 2.2(Froyo)之前,全局弱引用没有被实现。如果试图使用它们,老版本将完全不兼容。你可以使用Android平台版本号常量来测试系统的支持性。
Until Android 4.0 (Ice Cream Sandwich), weak global references could only be passed to NewLocalRef, NewGlobalRef, and DeleteWeakGlobalRef. (The spec strongly encourages programmers to create hard references to weak globals before doing anything with them, so this should not be at all limiting.)
在Android 4.0 (Ice Cream Sandwich)之前,全局弱引用只能传给NewLocalRef, NewGlobalRef, 以及DeleteWeakGlobalRef(强烈建议开发者在使用全局弱引用之前都为它们创建强引用hard reference,所以这不应该在所有限制当中)。 从Android 4.0 (Ice Cream Sandwich)起,全局弱引用能够像其它任何JNI引用一样使用了。
From Android 4.0 (Ice Cream Sandwich) on, weak global references can be used like any other JNI references.
从Android 4.0(Ice Cream Sandwich)开始,全局弱引用能像其他JNI引用一样使用了。
- Local references
Until Android 4.0 (Ice Cream Sandwich), local references were actually direct pointers. Ice Cream Sandwich added the indirection necessary to support better garbage collectors, but this means that lots of JNI bugs are undetectable on older releases. See JNI Local Reference Changes in ICS for more details.
在Android 4.0(Ice Cream Sandwich)之前,局部引用实际上是直接指针,为了支持更好的垃圾回收,Ice Cream Sandwich加入了间接指针,但这也意味着很多JNI bug在老的系统版本中不存在。详情查看JNI Local Reference在ICS系统上的改变。
In Android versions prior to Android 8.0, the number of local references is capped at a version-specific limit. Beginning in Android 8.0, Android supports unlimited local references.
在高于Android8.0的版本里,本地引用的数量限制在特定于版本。从Android 8.0开始支持不受限制的局部引用个数。
- Determining reference type with GetObjectRefType
Until Android 4.0 (Ice Cream Sandwich), as a consequence of the use of direct pointers (see above), it was impossible to implement GetObjectRefType correctly. Instead we used a heuristic that looked through the weak globals table, the arguments, the locals table, and the globals table in that order. The first time it found your direct pointer, it would report that your reference was of the type it happened to be examining. This meant, for example, that if you called GetObjectRefType on a global jclass that happened to be the same as the jclass passed as an implicit argument to your static native method, you'd get JNILocalRefType rather than JNIGlobalRefType.
在Android4.0(Ice Cream Sandwich)之前,因为使用直接指针导致实现GetObjectRefType是一件不可能的事情。相反,我们按顺序来查看弱全局表、参数表和局部表以及局部表。第一次匹配到你的直接指针时,就表明你的引用类型是当前正在检测的类型。这意味着,例如,如果你在一个全局jclass上使用GetObjectRefType,而这个全局jclass碰巧与作为静态本地方法的隐式参数传入的jclass一样的,你得到的结果是JNILocalRefType而不是JNIGlobalRefType。
FAQ: Why do I get UnsatisfiedLinkError?
When working on native code it's not uncommon to see a failure like this:
当与本地代码工作时候,经常碰到以下的错误:
java.lang.UnsatisfiedLinkError: Library foo not found
In some cases it means what it says — the library wasn't found. In other cases the library exists but couldn't be opened by dlopen(3), and the details of the failure can be found in the exception's detail message.
在一些情况下这意味着so库没有找到,另外一些情况意味着so库存在但是不能被dlopen(3)打开,错误消息异常消息中找到。
Common reasons why you might encounter "library not found" exceptions:
常见的发生 "library not found"的原因如下:
- The library doesn't exist or isn't accessible to the app. Use adb shell ls -l
to check its presence and permissions. - The library wasn't built with the NDK. This can result in dependencies on functions or libraries that don't exist on the device.
- so库不存在或者app不可访问。通过使用adb shell ls -s
检查so库是否存在以及访问权限。 - so库并没有通过NDK打包,这就导致设备上并不存在它所依赖的函数或者库。
Another class of UnsatisfiedLinkError failures looks like:
另外UnsatisfiedLinkError的错误如下:
java.lang.UnsatisfiedLinkError: myfunc
at Foo.myfunc(Native Method)
at Foo.main(Foo.java:10)
In logcat, you'll see:
在logcat中,你将看到:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
This means that the runtime tried to find a matching method but was unsuccessful. Some common reasons for this are:
这意味着运行时尝试去找匹配的方法但是失败了。一些常见原因如下:
The library isn't getting loaded. Check the logcat output for messages about library loading.
-
The method isn't being found due to a name or signature mismatch. This is commonly caused by:
- For lazy method lookup, failing to declare C++ functions with extern "C" and appropriate visibility (JNIEXPORT). Note that prior to Ice Cream Sandwich, the JNIEXPORT macro was incorrect, so using a new GCC with an old jni.h won't work. You can use arm-eabi-nm to see the symbols as they appear in the library; if they look mangled (something like _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass rather than Java_Foo_myfunc), or if the symbol type is a lowercase 't' rather than an uppercase 'T', then you need to adjust the declaration.
- For explicit registration, minor errors when entering the method signature. Make sure that what you're passing to the registration call matches the signature in the log file. Remember that 'B' is byte and 'Z' is boolean. Class name components in signatures start with 'L', end with ';', use '/' to separate package/class names, and use 'Entry;, say).
- 库文件没有得到加载。检查日志输出中关于库文件加载的信息。
- 由于名称或者签名错误,方法不能匹配成功。这通常是由于:
- 对于方法的懒查寻,使用 extern "C"和对应的可见性(JNIEXPORT)来声明C++函数没有成功。注意Ice Cream Sandwich之前的版本,JNIEXPORT宏是不正确的,因此对新版本的GCC使用旧的jni.h头文件将不会有效。你可以使用arm-eabi-nm查看它们出现在库文件里的符号。如果它们看上去比较凌乱(像_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass这样而不是Java_Foo_myfunc),或者符号类型是小写的“t”而不是一个大写的“T”,这时你就需要调整声明了。
- 对于显式注册,在进行方法签名时可能犯了些小错误。确保你传入到注册函数的签名能够完全匹配上日志文件里提示的。记住“B”是byte,“Z”是boolean。在签名中类名组件是以“L”开头的,以“;”结束的,使用“/”来分隔包名/类名,使用“Entry;)。
Using javah to automatically generate JNI headers may help avoid some problems.
使用javah自动生成JNI headers或许能避免很多问题。
FAQ: Why didn't FindClass find my class?
(Most of this advice applies equally well to failures to find methods with GetMethodID or GetStaticMethodID, or fields with GetFieldID or GetStaticFieldID.)
这种情况同样适用于使用GetMethodID或GetStaticMethodID,或使用GetFieldID或GetStaticFieldID的字段查找方法失败
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 of String would be [Ljava/lang/String;. If you're looking up an inner class, use '$' rather than '.'. In general, using javap on the .class file is a good way to find out the internal name of your class.
确保class名字是正确的格式,JNI class名以package名字开头且以斜线分割,例如:java/lang/String. 如果你寻找一个数组类,应当以一定数量的括号开始并且在class前加上‘L’和‘;’。因此,一维数组应当为[Ljava/lang/String;。如果你要寻找一个内部类,得使用“$”代替“.”。通常在.class上使用javap找出内部class的ame是一个好的方式。
If you're using ProGuard, make sure that ProGuard didn't strip out your class. This can happen if your class/method/field is only used from JNI.
如果你在使用ProGuard, 确保ProGuard不要混淆你的类。当你的class,method或者field在被JNI使用时候你需要这些防范操作。
If the class name looks right, you could be running into a class loader issue. FindClass wants to start the class search in the class loader associated with your code. It examines the call stack, which will look something like:
如果类名能找到,你将进入class加载环节。FindClass想要启动在class加载器中找到的关联你代码的类。检查调用堆栈,如下所示:
Foo.myfunc(Native Method)
Foo.main(Foo.java:10)
The topmost method is Foo.myfunc. FindClass finds the ClassLoader object associated with the Foo class and uses that.
最上面的方法是
Foo.myfunc
。FindClass发现关联Foo类的类加载器并使用它。
This usually does what you want. You can get into trouble if you create a thread yourself (perhaps by calling pthread_create and then attaching it with AttachCurrentThread). Now there are no stack frames from your application. If you call FindClass from this thread, the JavaVM will start in the "system" class loader instead of the one associated with your application, so attempts to find app-specific classes will fail.
一般运行如你所愿,但你也会陷入问题,如果你自己创建了一个线程(可能通过调用pthread_create然后又调用AttachCurrentThread)。这时候你的应用程序没有堆栈信息。如果你在线程中调用FindClass,JavaVM将启动系统级别的类加载器而不是与你应用程序关联的类加载器,因此这种情况尝试去找app指定的classes注定会失败。
There are a few ways to work around this:
围绕这个问题还有几个其他方式:
-
Do your FindClass lookups once, in JNI_OnLoad, and cache the class references for later use. Any FindClass calls made as part of executing JNI_OnLoad will use the class loader associated with the function that called System.loadLibrary (this is a special rule, provided to make library initialization more convenient). If your app code is loading the library, FindClass will use the correct class loader.
在JNI_OnLoad中只FindClass一次,并为了随后的使用缓存住这些引用。作为执行JNI_OnLoad的部分,任何FindClass的调用将使用与函数(called System.loadLibrary)关联的类加载器(这是一个特定规则,为了使共享库的初始化更加方便)。如果你的App正在加载共享库,FindClass将会使用正确的类加载器。
-
Pass an instance of the class into the functions that need it, by declaring your native method to take a Class argument and then passing Foo.class in.
通过定义一个含有Class作为参数的本地方法,以这种方式将一个类的实例进入本地方法。
-
Cache a reference to the ClassLoader object somewhere handy, and issue loadClass calls directly. This requires some effort.
在方便的地方缓存ClassLoader对象的引用,这样加载类就来的比较直接。不过这需要些工作。
FAQ: How do I share raw data with native code?
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层面也要在native层面访问一个大的数据。常见的案例是操作bitmap或者音频资源,以下有两种基本方案:
You can store the data in a byte[]. This allows very fast access from managed code. On the native side, however, you're not guaranteed to be able to access the data without having to copy it. In some implementations, GetByteArrayElements and GetPrimitiveArrayCritical will return actual pointers to the raw data in the managed heap, but in others it will allocate a buffer on the native heap and copy the data over.
你可以把数据存在byte[]中,这样你可以很快的从java代码访问它。在本地方法那边,然而,您不能保证在不复制数据的情况下访问数据。在部分实现中,GetByteArrayElements和GetPrimitiveArrayCritical返回的指针将会确切地指向Java堆栈数据,但是其他情况则是本地堆栈分配内存并做数据拷贝。
The alternative is to store the data in a direct byte buffer. These can be created with java.nio.ByteBuffer.allocateDirect, or the JNI NewDirectByteBuffer function. Unlike regular byte buffers, the storage is not allocated on the managed heap, and can always be accessed directly from native code (get the address with GetDirectBufferAddress). Depending on how direct byte buffer access is implemented, accessing the data from managed code can be very slow.
或者另一个替代选择是在direct byte buffer中存储数据。可以通过java.nio.ByteBuffer.allocateDirect或者JNI的NewDirectByteBuffer 函数来创建direct byte buffer。不同于常见的byte buffer,这种存储不会在java端堆栈开辟内存,并且它总能从本地代码端访问(通过GetDirectBufferAddress获取地址)。受限于于direct byte buffer的实现,从java端访问它的数据速度比较慢。
The choice of which to use depends on two factors:
这些方案纠结用哪种好取决于以下两个因素:
- Will most of the data accesses happen from code written in Java or in C/C++?
- If the data is eventually being passed to a system API, what form must it be in? (For example, if the data is eventually passed to a function that takes a byte[], doing processing in a direct ByteBuffer might be unwise.)
- 否绝大部分的数据访问发生Java端还是C/C++端?
- 如果数据最终被传到系统api,会以哪种形式存在?(比如:如果数据最终被传给一个方法并以byte[]传入,用direct ByteBuffer处理或许是不明智的)
If there's no clear winner, use a direct byte buffer. Support for them is built directly into JNI, and performance should improve in future releases.
如果没有明显的赢家,请还是使用direct byte buffer。因为JNI默认就是支持的并且在未来它的性能也会被得到提高。