JNI开发中,你需要知道的一些建议

区间数组

当你想做的只是拷出或者拷进数据时,可以选择调用像GetArrayElements和GetStringChars这类非常有用的函数。想想下面:

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

if (data != NULL) {

memcpy(buffer, data, len);

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

}

这里获取到了数组,从当中拷贝出开头的len个字节元素,然后释放这个数组。根据代码的实现,Get函数将会牵制或者拷贝数组的内容。上面的代码拷贝了数据(为了可能的第二次),然后调用Release;这当中JNI_ABORT确保不存在第三份拷贝了。

另一种更简单的实现方式:

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

这种方式有几个优点:

  • 只需要调用一个JNI函数而是不是两个,减少了开销。

  • 不需要指针或者额外的拷贝数据。

  • 减少了开发人员犯错的风险-在某些失败之后忘记调用Release不存在风险。

类似地,你能使用SetArrayRegion函数拷贝数据到数组,使用GetStringRegion或者GetStringUTFRegion从String中拷贝字符。

异常

当异常发生时一定不能调用大部分的JNI函数。代码收到异常(通过函数的返回值,ExceptionCheck,或者ExceptionOccurred),然后返回,或者清除异常,处理掉。

当异常发生时你被允许调用的JNI函数有:

DeleteGlobalRef

DeleteLocalRef

DeleteWeakGlobalRef

ExceptionCheck

ExceptionClear

ExceptionDescribe

ExceptionOccurred

MonitorExit

PopLocalFrame

PushLocalFrame

ReleaseArrayElements

ReleasePrimitiveArrayCritical

ReleaseStringChars

ReleaseStringCritical

ReleaseStringUTFChars

许多JNI调用能够抛出异常,但通常提供一种简单的方式来检查失败。例如,如果NewString返回一个非空值,你不需要检查异常。然而,如果你调用一个方法(使用一个像CalllObjectMethod的函数),你必须一直检查异常,因为当一个异常抛出时它的返回值将不会是有效的。

注意中断代码抛出的异常不会展开Native调用堆栈信息,Android也还不支持C++异常。JNI Throw和ThrowNew指令仅仅是在当前线程中放入一个异常指针。从Native代码返回到Java代码时,异常将会被注意到,得到适当的处理。

Native代码能够通过调用ExceptionCheck或者ExceptionOccurred捕获到异常,然后使用ExceptionClear清除掉。通常,抛弃异常而不处理会导致些问题。

没有内建的函数来处理Throwable对象自身,因此如果想得到异常字符串,需要找出Throwable Class,然后查找到getMessage "()Ljava/lang/String;"的方法ID,调用它,如果结果非空,使用GetStringUTFChars,得到的结果可以传到printf(3) 或者其它相同功能的函数输出。

扩展检查

JNI的错误检查很少。错误发生时通常会导致崩溃。Android也提供了一种模式,叫做CheckJNI,这当中JavaVM和JNIEnv函数表指针被换成了函数表,它在调用标准实现之前执行了一系列扩展检查的。

额外的检查包括:

  • 数组:试图分配一个长度为负的数组。

  • 坏指针:传入一个不完整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的值)。

  • 类型安全:从你的Native代码中返回了一个不兼容的类型(比如说,从一个声明返回String的方法却返回了StringBuilder)。

  • UTF-8:传入一个无效的变形UTF-8字节序列到JNI调用。

(方法和域的可访问性仍然没有检查:访问限制对于Native代码并不适用。)

有几种方法去启用CheckJNI。

如果正在使用模拟器,CheckJNI默认是打开的。

如果有一台root过的设备,你可以使用下面的命令序列来重启运行时(runtime),启用CheckJNI。

adb shell stop

adb shell setprop dalvik.vm.checkjni true

adb shell start

随便哪一种,当运行时(runtime)启动时你将会在你的日志输出中见到如下的字符:

D AndroidRuntime: CheckJNI is ON

如果你有一台常规的设备,你可以使用下面的命令:

adb shell setprop debug.checkjni 1

这将不会影响已经在运行的app,但是从那以后启动的任何app都将打开CheckJNI(改变属性为其它值或者只是重启都将会再次关闭CheckJNI)。这种情况下,将会在下一次app启动时,在日志输出中看到如下字符:

D Late-enabling CheckJNI

Native库

可以使用标准的System.loadLibrary方法来从共享库中加载Native代码。在Native代码中较好的做法是:

在一个静态类初始化时调用System.loadLibrary(见之前的一个例子中,当中就使用了nativeClassInit)。参数是“未加修饰(undecorated)”的库名称,因此要加载“libfubar.so”,需要传入“fubar”。

提供一个Native函数:jint JNI_OnLoad(JavaVM vm, void reserved)

在JNI_OnLoad中,注册所有Native方法。应该声明方法为“静态的(static)”因此名称不会占据设备上符号表的空间。

JNI_OnLoad函数在C++中的写法如下:

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

JNIEnv* env;

if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {

return -1;

}

// 使用env->FindClass得到jclass

// 使用env->RegisterNatives注册Native方法

return JNI_VERSION_1_6;

}

可以使用共享库的全路径来调用System.load。对于Android app,会发现从context对象中得到应用私有数据存储的全路径是非常有用的。

上面是推荐的方式,但不是仅有的实现方式。显式注册不是必须的,提供一个JNI_OnLoad函数也不是必须的。你可以使用基于特殊命名的“发现(discovery)”模式来注册Native方法,虽然这并不可取。因为如果一个方法的签名错误,在这个方法实际第一次被调用之前是不会知道的。

关于JNI_OnLoad另一点注意的是:任何你在JNI_OnLoad中对FindClass的调用都发生在用作加载共享库的类加载器的上下文(context)中。一般FindClass使用与“调用栈”顶部方法相关的加载器,如果当中没有加载器(因为线程刚刚连接)则使用“系统(system)”类加载器。这就使得JNI_OnLoad成为一个查寻及缓存类引用很便利的地方。

64位机问题

Android当前设计为运行在32位的平台上。理论上它也能够构建为64位的系统,但那不是现在的目标。当与Native代码交互时,在大多数情况下这不是需要担心的,但是如果打算存储指针变量到对象的整型字段(integer field)这样的Native结构中,这就变得非常重要了。为了支持使用64位指针的架构,你需要使用long类型而不是int类型的字段来存储你的Native指针。

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

除了下面的例外,支持所有的JNI 1.6特性:

  • DefineClass没有实现。Android不使用Java字节码或者class文件,因此传入二进制class数据将不会有效。

对Android以前老版本的向后兼容性,你需要注意:

  • 分离线程 在Android 2.0(Eclair)之前,使用pthread_key_create析构函数来避免“退出前线程必须分离”检查是不可行的(运行时(runtime)也使用了一个pthread key析构函数,因此这是一场看谁先被调用的竞赛)。

  • 全局弱引用 在Android 2.0(Eclair)之前,全局弱引用没有被实现。如果试图使用它们,老版本将完全不兼容。你可以使用Android平台版本号常量来测试系统的支持性。 在Android 4.0 (Ice Cream Sandwich)之前,全局弱引用只能传给NewLocalRef, NewGlobalRef, 以及DeleteWeakGlobalRef(强烈建议开发者在使用全局弱引用之前都为它们创建强引用hard reference,所以这不应该在所有限制当中)。 从Android 4.0 (Ice Cream Sandwich)起,全局弱引用能够像其它任何JNI引用一样使用了。

  • 局部引用 在Android 4.0 (Ice Cream Sandwich)之前,局部引用实际上是直接指针。Ice Cream Sandwich为了更好地支持垃圾回收添加了间接指针,但这并不意味着很多JNI bug在老版本上不存在。

  • 使用GetObjectRefType获得引用类型 在Android 4.0 (Ice Cream Sandwich)之前,使用直接指针(见上面)的后果就是正确地实现GetObjectRefType是不可能的。我们可以使用依次检测全局弱引用表,参数,局部表,全局表的方式来代替。第一次匹配到你的直接指针时,就表明你的引用类型是当前正在检测的类型。这意味着,例如,如果你在一个全局jclass上使用GetObjectRefType,而这个全局jclass碰巧与作为静态Native方法的隐式参数传入的jclass一样的,你得到的结果是JNILocalRefType而不是JNIGlobalRefType。

FAQ: 为什么出现了UnsatisfiedLinkError?

当使用Native代码开发时经常会见到像下面的错误:

java.lang.UnsatisfiedLinkError: Library foo not found

这表示和它提示的一样—未找到库。但有些时候库确实存在但不能被dlopen(3)找开,更多的失败信息可以参见异常详细说明。

你遇到“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)

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

美团面试经验

美团面试
字节面试经验
字节面试
菜鸟面试经验
菜鸟面试
蚂蚁金服面试经验
蚂蚁金服
唯品会面试经验
唯品会

因篇幅有限,图文无法详细发出

-cCjx8dgK-1710696632880)]

最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

美团面试经验

[外链图片转存中…(img-Bw9V7ppw-1710696632880)]
字节面试经验
[外链图片转存中…(img-Oprp5bwx-1710696632881)]
菜鸟面试经验
[外链图片转存中…(img-Anv8q1Na-1710696632881)]
蚂蚁金服面试经验
[外链图片转存中…(img-qRfIDxVd-1710696632882)]
唯品会面试经验
[外链图片转存中…(img-EodbeDpt-1710696632882)]

因篇幅有限,图文无法详细发出

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值