JNI编程经验(JNI Tips)

翻译原文来自:http://developer.android.com/intl/zh-cn/training/articles/perf-jni.html

JNI全称是Java Native Interface, 它是一种使用java语言和原生C/C++语言相互调用,混合编程的方法它支持从动态链接库中加载代码并能使用C/C++的高效的特性

如果你之前对这个还不熟悉完整的读一遍Java Native Interface文档(http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)可以对JNI的功能有个基本的了解有些功能在你读第一遍时还没什么特别的感觉但你可能会发现有些功能使用起来会很便利.

Java虚拟机和JNIEnv

--------------------------------------------------------------------------------

JNI定义了2个数据结构, java虚拟机和JNIEnv, 这两个本质上都是指向函数表的指针. (C++版本中他们是2个类包含了成员函数这些成员函数都是JNI的函数.) java虚拟机提供"调用接口"函数它允许你创建和销毁java虚拟机理论上你可以在一个进程里有多个java虚拟机android只允许1.

JNIEnv提供了大部分的JNI函数C语言里你所有的native函数都要接收JNIEnv作为第一个参数.

JNIEnv用在线程本身的存储这样会导致你不能在多个线程里共享JNIEnv, 如果部分代码没有办法获得JNIEnv, 那么可以把java虚拟机共享通过java虚拟机来获得这个线程的JNIEnv.

C语言声明的JavaVMJNIEnvC++声明的不同, "jni.h"头文件里使用宏定义把CC++的声明作了区别基于这原因JNIEnv作为参数放在头文件里是不好的方式这导致在CC++都用这个头文件时会有很大的麻烦.(也可以这么说如果你的头文件需要定义#ifdef __cplusplus, 那么你必须对这个指向JNIEnv的头文件作一些改动来同时适应CC++语言.

线程

--------------------------------------------------------------------------------

JNI里的所有的线程都是linux的线程由系统内核进行调度他们通常由Thread.start来启动, 但也可以在通过关联上Java虚拟机后在native语言中启动。 例如,使用C语言的pthread_create也可以创建线程,但在使用JNI方法AttachCurrentThreadAttachCurrentThreadAsDaemon来关联之前, 这个线程是没有JNIEnv的。同时也不能调用JNI的接口。

关联上native语言创建的线程会导致一个java.lang.Thread对象被创建同时添加到"main"线程组里,这样也可以被调试器检测到。如果再使用AttachCurrentThread来关联一个已经被关联的线程将不会有任何动作。

Android不能暂停线程去执行native的代码, 如果一个GC在运行或者调试器触发了一个暂停请求,Android将会暂停在下一个JNI的调用,而不是当前的native代码。

使用AttachCurrentThreadAttachCurrentThreadAsDaemon关联到JNI的线程在结束前必须调用DetachCurrentThread来解除关联。假如直接写DetachCurrentThread来取消关联会比较别扭,在android2.0之后,你可以用pthread_key_create来定义一个线程的析构方法,并且在这个析构方法里调用DetachCurrentThread(使用pthread_setspecific来把JNIEnv保存在线程本地的存储里, 这个将会作为参数传到析构方法里.)

jclass, jmethodIDjfieldID

--------------------------------------------------------------------------------

如果你想在native代码里访问一个对象的属性,你需要按照以下几步来做:

 • 使用FindClass获取类对象的引用

 • 使用GetFieldID获取属性的ID

 • 使用恰当的方法来获取属性的值,例如GetIntField

类似的,如果要调用一个方法,你首先需要获得类对象的引用, 然后是方法的ID,这些ID通常是指向内部运行的数据结构,要获得这些ID可能需要多次字符串的比较,但是一旦你获得这些ID,那么调用这些方法或者获取属性将会非常快。

如果你追求高性能,最好是做一次查找,然后这些ID存起来,由于android有每个进程只能有一个Java虚拟机的限制,这是个很好的理由把这些数据存在本地。

类的引用,属性的ID和方法的ID能保证在类卸载前一直有效。类只有在GC时没有发现任何使用者时才可能卸载,这个在android中基本不可能发生。不管怎么样,jclass作为类引用必须使用NewGlobalRef来保护。

如果你喜欢在加载一个类时把那些ID保存起来并且在类卸载和重新加载时重新保存他们,正确的方法是像下面的代码这样来初始化这些ID

/*     

 * 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.     

 */    

 private static native void nativeInit();    

 static {        

nativeInit();    

 }

临时和全局引用

--------------------------------------------------------------------------------

每个传给原生方法的参数,和几乎所有由JNI方法返回的对象都是临时引用,这意味这个引用的有效期与当前原生方法在当前线程的执行过程是一样长的,所以在原生方法返回后,这个引用将不在有效。

这个规则也可以适用在所有jobject的子类,包括jclass, jstringjarray.(如果扩展JNI检测被设上后,在运行时会警告你大部分的引用使用错误)

只有使用NewGlobalRef 和 NewWeakGlobalRef可以获得全局的引用。

如果你想保持一个引用更长的时间, 你必须使用"global"引用.NewGlobalRef方法接收一个临时引用的参数,然后返回一个全局引用。全局引用一直有效,直到你使用DeleteGlobalRef来删除它。

全局引用通常用来保存使用FindClass返回的jclass.例如:

    jclass localClass = env->FindClass("MyClass");

    jclass globalClass = reinterpret_cast(env->NewGlobalRef(localClass));

所有JNI方法接受临时和全局引用作为参数。引用到同一个对象却有多个值是可能的。例如,连续使用NewGlobalRef来引用同一个对象的返回值可能是不一样的。要比较2个引用是不是指向同一个对象,必须使用IsSameObject方法来判断,永远不要在原生代码中用==来判断两个引用是否一样。

所以你不能假设原生代码中对象的引用是常量或者唯一的。不能用jobject的值来作为对象的key

我们要求开发者不要过分的使用临时引用,这意味着如果你创建了很多临时引用,你最好使用DeleteLocalRef来释放这些临时引用,而不是让JNI来做这事。因为有的系统只允许使用16个临时引用,滥用临时引用会导致内存溢出。如果你需要更多的临时引用,要么你把之前的释放掉,要么使用EnsureLocalCapacity/PushLocalFrame/PopLocalFrame来增加临时引用的最大数目。

注意:jfieldIDjmethodID是不透明的类型,而不是对象引用,所以不能用NewGlobalRef方法来调用。由某些方法例如GetStringUTFCharsGetByteArrayElements返回的原始数据指针也不是对象.(这些类型可以在线程间传递,并且在相应的对象释放前一直有效.)

有一个不常见的用例值得单独提到,如果你使用AttachCurrentThread关联一个线程,线程中代码生成的临时引用要到线程取消关联后才会自动释放.所以你需要手动释放你创建的临时引用,通常要遵循谁创建,谁释放的规则。

 

UTF-8UTF-16字符串

--------------------------------------------------------------------------------

java语言用的是UTF-16编码字符串. 为了便利,JNI也提供了修改Mod UTF-8字符串的方法修改Mod编码的方法在C语言中很有用,因为它把\u0000编码为0xc0 0x80而不是0x00. 更好的是你可以处理\0结尾的C语言格式字符串这适用于那些使用标准libc的字符串函数.不好的一面是你不能随意的把UTF-8数据传给JNI就期望它能正常工作。

如果可能,一般处理UTF-16的字符串会比较快, Android现在不要求在GetStringChars中进行拷贝,而GetStringUTFChars要求一次分配内存和转换成UTF-8字符串注意UTF-16字符串不是\0结尾的,并且\u0000是允许的。所以你需要跟jchar指针一样获取字符串的长度。

使用Get获得的字符串记得要释放这些字符串函数返回jchar*或者jbyte*, 这种C语言格式的指针指向原始数据比临时引用好,他们保证在释放前是有效的,这意味着在原生方法返回后他们还是有效的,未释放的。

 传给NewStringUTF的参数必须是Mod UTF-8格式的字符串, 一个容易犯的错误是把从文件或网络流中读到字符串传给NewStringUTF而没有过滤他,除非你知道这个数据是7ASCII,你需要去掉高位ASCII字符或者转换他们为正确的Mod UTF-8格式.如果你没有这样做,默认UTF-16的转换应该不是你所期望的.扩展的JNI检查会扫描字符串和警告是无效数据,但他们不会捕获所有的异常。

 

原始数组

--------------------------------------------------------------------------------

JNI提供了方法来方法数组对象的内容,但必须一次访问一个数组对象的数据。原始数组可以像C语言那样直接读和写。

为了使接口高效而且不受虚拟机实现的制约, GetArrayElements系列函数允许返回一个指向实际元素的指针或者分配一些内存来拷贝数据无论哪种方式返回的原始数据的指针在调用释放方法前是保证一直有效的(这意味着如果数据没有被拷贝这个对象数组将被限制在压缩堆数据时不能移动). 你必须自己释放每个你获取的数组同时如果Get方法失败的话,你的代码一定不能尝试释放一个NULL指针.

最有用的是你可以通过传一个非空指针作为isCopy的参数来决定是否拷贝数据.

那些释放方法有一个mode参数这个参数有3个值根据这个参数这个方法在运行时会根据指针是指向原始数据还是拷贝的数据有不同的表现.

•0

原始数据对象数组将不会被限制

拷贝数据数据将会拷贝回原始数据同时释放拷贝数据

•JNI_COMMIT

原始数据什么都不作

拷贝数据数据将会拷贝回原始数据不释放拷贝数据.

•JNI_ABORT

原始数据对象数组将不会被限制之前的数据操作有效.. 

拷贝数据释放拷贝数据之前的任何数据操作会丢弃

如果你对对象数组做了修改并且你有可能会根据对象数组的内容来决定是修改还是继续执行其他代码那么

检查isCopy参数是你是否需要用JNI_COMMIT来调用释放方法的一个理由你可以不做操作另一个可能的原因,检查该参数是为了使用JNI_ABORT的高效处理.例如你可能需要获得一个数组临时修改一下,然后将部分数据传给其他方法最后丢弃之前的修改如果你知道JNI是作了拷贝数据的Get, 那就不需要另外创建一份拷贝来修改但如果JNI是把原始数据传给你那么你需要自己拷贝一份数据来进行修改.

一个最容易犯的错误是如果isCopyfalse, 有人以为可以不调用释放方法(这在例子代码中有很多这样的错误). 因为即使没有拷贝数据原始数据也是被限制不能回收的.所以要主动调用释放方法来释放.

同时要注意JNI_COMMIT标志不会释放数组你终究需要再次用另外一个标志来调用释放方法.

局部方法

--------------------------------------------------------------------------------

当你想对数组进行写入或读出这还有一个像GetArrayElementsGetStringChars非常有用的方式:

    jbyte* data = env->GetByteArrayElements(array, NULL);    

if (data != NULL) {

    memcpy(buffer, data, len);        

    env->ReleaseByteArrayElements(array, data, JNI_ABORT);    

}

这个方式是获取了数组然后拷贝了len长度的byte, 最后释放掉数组根据上面的实现, Get方法会限制或者拷贝数据在单独的拷贝数据后(也许就是第二次), 就调用释放方法来释放数据在这种情形下, JNI_ABORT保证这里没有机会有第三个拷贝.

还有一个更简单实现同样功能的方式:

    env->GetByteArrayRegion(array, 0, len, buffer);

这种方式有几个好处:

  •只需要一个JNI调用而不是两个减少开销.

  •不需要对原始数据进行限制或者额外的拷贝数据

  •减少开发者的风险(不会有在某些出错后忘记释放的风险)

 

同样的你可以用SetArrayRegion系列方法来拷贝数据到一个数组, GetStringRegion或者GetStringUTFRegion是从一个String中拷贝字符.

异常

--------------------------------------------------------------------------------

你不能在有异常等待处理时调用大多数的JNI方法。你的代码应该要预期到有异常(通过函数的返回值,ExceptionCheckExceptionOccurred)并返回,或清除异常并处理它。

你只可以在异常等待处理时调用下面的JNI方法:

 •DeleteGlobalRef

 •DeleteLocalRef

 •DeleteWeakGlobalRef

 •ExceptionCheck

 •ExceptionClear

 •ExceptionDescribe

 •ExceptionOccurred

 •MonitorExit

 •PopLocalFrame

 •PushLocalFrame

 •ReleaseArrayElements

 •ReleasePrimitiveArrayCritical

 •ReleaseStringChars

 •ReleaseStringCritical

 •ReleaseStringUTFChars 

很多JNI方法会跑出异常但通常会提供一个简单的方法来检查失败例如,如果NewString返回一个非空值你不需要去检查异常然而,如果你调用一个方法(比如CallObjectMethod), 你必须检查异常因为返回值在异常发生时是不正确的.

在解释代码抛出异常时不会放松本地的栈帧, android现在还不支持C++异常. JNIThrowThrowNew只是在当前线程设置一个异常指针直到从原生代码返回到java代码异常才会被抛出或者正确的处理.

原生代码可以用ExceptionCheck或者ExceptionOccurred来捕获一个异常并使用ExceptionClear来清除它通常丢弃异常没有处理它们可能导致问题

这里没有内置的方法来控制Throwable对象所以如果你想获得异常的描述你需要找到Throwable查找它的 getMessage "()Ljava/lang/String;"方法的ID, 然后调用它如果结果不是空,那么用GetStringUTFChars来获取内容,这个内容可以用printf来显示.

扩展的检查

--------------------------------------------------------------------------------

JNI会做一些错误检查错误通常结果是崩溃. Android也提供了一个名为CheckJNI模式其中的JavaVMJNIEnv的函数表指针调用标准实现之前会切换到扩展系列的检查功能表执行检查。

附加的检查包括:

 •数组尝试分配一个大小为负值的数组

 •错误的指针传一个错误的jarray/jclass/jobject/jstringJNI方法或者传一个空指针连同一个非空的参数到JNI方法

 •类名:传一个不是“java/lang/String”风格的类名到JNI方法里.

 •重要的调用在一个重要的GetRelease之间调用一个JNI方法.

 •直接的Byte缓存:传一个错误的参数给NewDirectByteBuffer

 •异常:在有异常等待处理时调用一个JNI方法

 •JNIEnv*: 在不合适的线程中使用JNIEnv*

 •jfieldIDs: 使用一个NULL jfieldID, 或者使用一个jfieldID来设置错误类型的值(试图给一个String属性赋StringBuilder的值), 或使用jfieldID的静态字段设置一个实例字段或反之亦然或使用一个类中jfieldID到另一个类的实例里.

 •jmethodIDs:在调用JNI方法时使用错误的jmethodID类型不正确的返回类型静态和非静态类型的错误匹配, "this"的错误类型(非静态调用)或者错误的类(静态调用).

 •References(引用):对不正确的引用使用DeleteGlobalRef/DeleteLocalRef.

 •Release modes(释放模式):传一个错误的释放模式来调用释放方法(比如0, JNI_ABORT或者JNI_COMMIT).

 •类型安全检查:从原生方法返回一个不兼容的类型(从一个声明返回值为String的原生方法里返回一个StringBuilder).

 •UTF-8:传一个无效的Mod UTF-8字节序列到JNI方法里.

 

(方法和属性的访问限制不做检查:访问限制并不适用于原生代码)

这里有几个方法来设置CheckJNI

如果你使用的是模拟器, CheckJNI默认是开启的

如果你root了一个设备你可以用下列命令来重起runtime开启CheckJNI:

adb shell stop

adb shell setprop dalvik.vm.checkjni true

adb shell start

在这两种情况下,你将会在logcat中看到runtime启动的log:

D AndroidRuntime: CheckJNI is ON

如果你有一个没root的设备你可以用下列命令:

adb shell setprop debug.checkjni 1

这个不会影响已经运行的应用但从这之后运行的应用将是开启CheckJNI.(把这个值改为其他任何值或者重起设备将会关闭CheckJNI.)这种情况下你会看到logcat在应用运行时显示

D Late-enabling CheckJNI 

原生库

--------------------------------------------------------------------------------

你可以使用标准System.loadLibrary来从共享库里加载原生代码推荐的实现代码如下:

 • 在类的静态构造方法里调用System.loadLibrary(见之前的例子那个使用nativeClassInit的例子.) 参数是未修饰库名如果库文件是"libfubar.so", 那么这里传"fubar".

 • 提供一个原生方法:jint JNI_OnLoad(JavaVM* vm, void reserved)

 • JNI_OnLoad注册所有的原生方法你应该把这些方法声明为"static", 这样这些方法名就不会在设备符号表里占用空间.

 

JNI_OnLoad方法在C++中应该像下面这样写:

 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;

 }

 

这个是推荐的方式但不是唯一的方式显示的注册方法不是必须的而且JNI_OnLoad方法也不是必须的你可以用将方法名按一定的规则来命名的方式实现同样的功能(JNI文档).虽然这是不太理想的,因为如果一个方法的签名是错的,你不会知道而是直到第一次实际使用方法的时候才知道。

另外一个关于JNI_OnLoad要注意的是任何调用FindClass方法都需要跟加载动态库的过程在同一个上下文里通常FindClass使用的类加载器是在解释栈顶或者那里没有加载器的话(线程没关联之前),就用系统的类加载器这就是为什么最适合在JNI_OnLoad里进行FindClass操作的原因.

64位机器的注意事项

--------------------------------------------------------------------------------

Android当前是主要为在32位系统上运行而设计的理论上它也可以在64位系统上运行但目前这个不是Android的首要目标64位机器很多地方你不需要担心跟原生代码的交互但如果你计划在原生数据结构中保存指针那么这就是一个要注意的问题.为了支持使用64位的指针你需要把你的指针保存在long类型中而不是int类型.

不支持的特性和向后的兼容性

--------------------------------------------------------------------------------

所有JNI 1.6的特性都支持包括下列异常:

 •DefineClass没有实现. Android不使用Java字节编码或类文件所以传入二进制类数据是不支持的.

 

为了向后兼容旧的Android版本你需要注意的是:

 •动态查找原生方法

 直到Android2.0(Eclair), 在搜索方法名时, '$'字符无法正确转换成"_00024". 解决方案是使用显示注册方法或者把原生方法从内嵌里移出来.

 

取消关联线程

直到Android2.0(Eclair),Android不支持使用pthread_key_create设置线程析构方法来避免"线程必须在退出前取消关联"的检查.(runtime也会使用线程析构方法所以这个比谁先获得调用的竞赛.)

弱全局引用

直到Android2.2(Froyo), 弱全局引用没有实现旧版本将会在使用它时提示拒绝你可以使用Android平台版本常量来测试是否支持.

直到Android4.0(Ice Cream sandwich), 弱全局引用只能传给NewLocalRef, NewGlobalRefDeleteWeakGlobalRef.(文档强烈建议开发者把弱全局引用赋给强引用后再开始做相应工作所以这不应该是在所有限制)

Android4.0(Ice Cream sandwich)开始弱全局引用可以像其他JNI引用一样使用.

临时引用

直到Android4.0(Ice Cream sandwich), 临时引用实际上就是直接指针. 4.0添加了必要的间接指针来支持更好的GC, 但这意味着很多JNI bug无法在旧版本上检测到请看下面的链接获取详细信息 http://android-developers.blogspot.com/2011/11/jni-local-reference-changes-in-ics.html

 •使用GetObjectRefType确定引用类型

直到Android4.0(Ice Cream sandwich),作为使用直接指针的一个结果它是不可能正确实现GetObjectRefType.相应的是,我们使用了启发式,透过弱全局引用表参数临时引用表和全局引用表的各个指针地址来查找引用类型当第一次找到直接指针它将会把他检测到的类型返回给你这个意思是例如如果你对一个全局jclass调用GetObjectRefType, 恰好当时jclass传了一个显示参数到一个静态原生方法你将会得到的返回值是JNILocalRefType而不是JNIGlobalRefType.

FAQ: 为什么会碰到UnsatisfiedLinkError错误?

--------------------------------------------------------------------------------

当执行到原生代码时你可能会碰到这个错误log:

 java.lang.UnsatisfiedLinkError: Library foo not found

在某些情况下这意味着就是库找不到其他情况是库存在但不能被dlopen打开并且详细的错误可以在异常的详细信息读到.

 "library not found"异常发生的原因通常是:

 •库不存在或app没权限访问它使用adb shell ls -l 去检查库的状态和访问权限.

 •库不是由NDK编成的这可能会导致在没有该设备上存在的库函数的依赖关系。

 

还有一个UnsatisfiedLinkError的错误是这样的:

java.lang.UnsatisfiedLinkError: myfunc        

at Foo.myfunc(Native Method)        

at Foo.main(Foo.java:10)

logcat你会看到:

 W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

这意味着runtime尝试匹配方法但没匹配到通常的原因是:

 • 库没有被加载检查logcat关于加载库的信息.

 • 由于方法名或签名的不匹配导致找不到方法这是最常见的原因:

    ◦ 对于懒得方法查找算法没有对C++方法加上extern "C"的声明和适当的可见性(JNIEXPORT).注意在4.0之前, JNIEXPORT宏是不正确的所以使用一个新的GCC和旧的jni.h文件来编译是不能正常工作的.你可以使用arm-eabi-nm来看库中的符号表.如果你看到错位的符号(比如看到的是_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass而不是Java_Foo_myfunc), 或者符号类型是小写't'而不是大写的'T', 那么你需要调整声明.

◦ 对于显示注册的方式最小的错误是方法签名的错误确定你调用注册方法时传的签名是正确的记得'B'byte类型和'Z'boolean类型.类名组件在签名中是以'L'开始, ';'结束使用'/'来分隔包名和类名使用'$'来分隔内嵌类名(例如 Ljava/util/Map$Entry;).

使用javah来自动生成JNI头文件会对某些问题有帮助.

FAQ: 为什么FindClass找不到我的类?

--------------------------------------------------------------------------------

确定类名有正确的格式. JNI类名字由包名开始'/'分隔例如 java/lang/String. 如果你查找一个数组类类名应该由中括号开始,里面使用'L'';'包含的类名所以一个一维String数组定义为[Ljava/lang/String;.

如果类名正确找到了你可能碰到一个加载类的问题. FindClass需要在一个类加载器里进行类的搜索它会检查调用栈这个栈会像这样:

 Foo.myfunc(Native Method)    

 Foo.main(Foo.java:10)    

 dalvik.system.NativeStart.main(Native Method)

 最上面的方法是Foo.myfunc. FindClass找到类加载对象关联的Foo类名并使用它.

 

这通常就是你所需要的但如果你在线程中使用FindClass可能会使你碰到麻烦这时调用栈应该是这样的:

     dalvik.system.NativeStart.run(Native Method)

最上面的方法是NativeStart.run, 这个不是你程序中的代码如果你在这个线程中使用FindClass, Java虚拟机将会在"system"类加载器而不是你自己应用对应的类加载器中进行查找所以这时查找一个你应用的类时是会失败的.

 针对这种情况这里有几个解决方案:

 • JNI_OnLoad里只做一次FindClass, 然后保存这些类的引用JNI_OnLoad里使用FindClass是能保证使用应用关联的类加载器

 • 传一个类的实例到方法里通过声明原生方法接收一个类作为参数然后把Foo.class传入.

 • 保存类加载器的引用然后直接使用这个类加载器这需要作额外的工作.

 

 

FAQ: 我在原生代码中怎样分享原始数据?

--------------------------------------------------------------------------------

你可能发现有这么一个情形你有一个大的原始数据你需要在java和原生代码中都访问这个数据而不是把原始数据转换成java的数据来处理常见的情况是一个处理图片或声音数据这里有2种方式来实现.

你可以把数据存在byte[]这个可以在java中有非常快的访问速度在原生代码中你不一定能保证不拷贝就能访问它在某些实现中, GetByteArrayElementsGetPrimitiveArrayCritical将返回指向实际数据的指针但其他实现将会原生代码层分配一块内存来把数据拷贝进去.

还有一个方法是保存数据在直接byte缓存这个可以用java.nio.ByteBuffer.allocateDirect来创建或者用JNINewDirectByteBuffer方法不像通常的byte缓存这个存储不在java的堆上分配空间并可以在原生代码中直接使用访问(通过GetDirectBufferAddress获得地址). 由于依赖直接byte缓存的实现在有些系统上从java层访问这个数据有可能会很慢.

可以依据下面2个因素来决定选择哪个方案:

 1. 大部分的访问是在Java层还是原生代码层?

 2. 如果数据最终传给系统调用那么最终是以什么格式传入?(例如如果最终数据是以byte[]类型传给方法那么使用直接byte缓存来保存将不是个明智的选择.)

 

如果没有一个明显的因素那么使用直接byte缓存这个的支持是直接内置在JNI里的而且将来还可能会被优化.

 

你可能感兴趣的:(JNI,NDK,Android,C/C++)