Android官方开发文档Training系列课程中文版:Android的JNI相关

原文地址:http://android.xsoftlab.net/training/articles/perf-jni.html

JNI的全称为Java Native Interface,中文意思是Java本地接口。它定义了Java代码与C/C++代码之间的交互方式。它是两者的桥梁,支持从动态共享库中加载代码。虽然有些复杂,但是它的执行效率还是蛮高的。

如果你对JNI还不太熟悉,那么可以通过Java Native Interface Specification来了解一下JNI的大致工作流程以及JNI的特性。

JavaVM与JNIEnv

JNI定义了两个关键的数据结构:”JavaVM”与”JNIEnv”。这两个函数本质上都为指向函数指针的指针表。JavaVM提供了”接口调用”功能,该功能允许创建、销毁JavaVM。理论上每个进程可以拥有多个虚拟机,但是在Android中只允许出现一个。

JNIEnv提供了大部分的JNI功能。任何本地方法都以JNIEnv为第一回调参数。

JNIEnv用于线程局部存储。正出于这个原因,所以不能在线程间共享JNIEnv。如果不能够通过其它方式获取其对应的JNIEnv对象,那么应该先共享JavaVM,然后通过GetEnv函数获取该线程对应的JNIEnv(假设该线程拥有一个JNIEnv,具体请往下看)。

C与C++对JNIEnv和JavaVM的声明方式并不相同。头文件”jni.h”针对C或者C++提供了不同的类型定义。正因为这个原因,在头文件中包含JNIEnv参数并不是个明智的主意。

线程

Android中所有的线程都是Linux线程,都由内核执行。通常由受控代码启动(比如Thread.start),但是也可以由别的地方创建,然后再附加到JavaVM上启动。举个例子,线程可以由pthread_create函数创建,然后通过AttachCurrentThread或AttachCurrentThreadAsDaemon将其附加到JavaVM上执行。

Android并不会挂起正在执行本地代码的线程。如果垃圾收集正在进行,或者调试器发起了挂起请求,那么Android会在下次JNI调用时暂停线程。

通过JNI所附加的线程在退出前必须调用DetachCurrentThread函数

jclass, jmethodID, 及jfieldID

如果需要在本地代码中访问对象的属性,那么需要执行以下操作:

  • 通过FindClass获取类对象的引用
  • 通过GetFieldID获得属性的ID
  • 通过对应的方法获取对象的内容,比如GetIntField

相应的,如果要调用一个方法,首先获取类对象的引用,其次获取该方法的ID。ID通常只是指向了一个内部的运行时数据结构。查找这些方法通常需要进行若干次字符串比对,但是一旦找到,那么后期的获取属性或者方法调用都会非常的迅速。

如果性能对你很重要,那么在找到这些属性或者方法之后,应该将其缓存起来。因为Android中只允许每个进程有一个JavaVM的存在,所以将这些数据缓存在一个静态本地结构中是合理的。

类的引用、属性的ID、方法的ID在这个类被卸载之前都可以保证它们有效。一个类只有在这种情况下才会被卸载:该类所关联的ClassLoader也能被回收。虽然这几率很低,但是在Android中不是没有可能的。

如果想在类加载的时候将这些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();}

在C/C++代码中创建一个名为nativeClassInit的方法,用于ID的查找与缓存。该方法会在类初始化的时候执行一次。就算是类被卸载后又重新加载,那么这个方法还是会被执行一次。

局部引用,全局引用

每个被回调到本地方法的参数,以及几乎所有的通过JNI方法返回的对象都是局部变量。这意味着当前线程中该方法内的所有局部变量都是合法的。在本地方法返回之后,虽然对象仍然存活,但是引用却是无效的。

这适用于jobject所有的子类:jclass, jstring, 以及jarray。

获取非局部变量的唯一方式就是通过NewGlobalRef及NewWeakGlobalRef函数获得。

如果需要长时间持有一段引用,那么必须使用全局引用。NewGlobalRef函数会将一个局部引用转换为一个全局引用。在调用DeleteGlobalRef方法之前,该全局引用一直有效。

这种模式通常用于缓存一个由FindClass返回的一个jclass对象:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

所有的JNI方法都可以以这两种引用为参数。不过引用相同的值可能有不同的结果。举个例子,以同一个引用为参数连续调用两次NewGlobalRef可能会得到不同的值。如果要查看两个引用是否指向了同一个对象,必须使用IsSameObject函数。绝不要在本地代码中使用”==”比较两个引用。

绝不要认为在本地代码中的对象引用是个常量或者是唯一的。一个32位的值所代表的对象的方法调用可能与下次调用就有所不同,这可能是因为两个不同的对象拥有相同的32位值。不要将jobject的值当做键使用。

程序员经常被要求不要过度的申请局部变量。这意味着如果你创建了大量的局部变量,那么应当通过DeleteLocalRef函数手动的释放它们,而不是让JNI为你做这些事情。

要注意jfieldIDs、jmethodID并不是对象引用,所以不能够将它们传给NewGlobalRef函数使用。GetStringUTFChars函数与GetByteArrayElements函数所返回的原始数据指针也同样不是对象。

一个不寻常的情况需要单独说明一下:如果通过AttachCurrentThread函数attach到了一个本地线程上,那么在该线程被detache之前,代码中所有的局部变量都不会被自动释放。任何创建的局部变量都需要手动删除。

UTF-8与UTF-16字符串

Java语言使用的是UTF-16字符串。为了方便起见,JNI所提供的方法工作在Modified UTF-8字符串下。修正后的编码对于C语言代码很有用,因为它将\u0000编码为了0xc0 0x80。

不要忘记释放你所获得的字符串。字符串函数会返回jchar* 或 jbyte*,它们是指向原始数据的指针,而不是本地引用。它们在被释放之前一直有效,这意味着在本地方法返回后,它们并没有被释放。

传给NewStringUTF函数的数据必须是Modified UTF-8格式。一个常见的错误就是从文件流或者网络流中读取字符串数据,然后没有过滤就直接交给了NewStringUTF函数进行处理。除非你知道这些数据是7位的ASCII,否则你需要剔除高位的ASCII字符串或者将它们转换为正确的Modified UTF-8格式。如果你不这么做,那么转换的结果可能不是你想看到的。额外的JNI检查会扫描字符串并会警告你这是无效的数据,但是它们不会捕获任何事情。

原始数组

JNI提供了用于访问对象数组的功能。然而,同一时间只能对一个元素进行访问,可以直接对数组今夕读写操作,就好像直接在C中声明的一样。

为了使JNI接口尽可能的高效,也不受虚拟机实现的限制,调用GetArrayElements的相关函数可以返回一个指向实际值的指针,或者可以申请一些内存以完成复制。无论哪种方法,所返回的指针都可以保证是有效的,直到相应的释放方法被触发。必须释放你所取得的每个数组。如果Get方法调取失败,也需要保证不要去释放一个空的指针对象。

你可以通过isCopy参数来检测一个数组是否是由指针所拷贝过来的,这一点很有用。

Release方法需要一个mode参数,这个参数有三种值。运行时执行的操作取决于它返回指向实际数据的指针或者指针的副本:

  • 0
    • 实际指针:非final修饰的数组对象
    • 指针副本:拷贝后的数组数据,拷贝的缓冲区会被释放
  • JNI_COMMIT
    • 实际指针:不做任何事情
    • 指针副本:拷贝后的数组数据,拷贝的缓冲区不会被释放
  • JNI_ABORT
    • 实际指针:非final修饰的数组对象。早些写入不会被中止。
    • 指针副本:所拷贝的缓冲区被释放;缓冲区内的任何变更都会丢失。

检查isCopy标志的其中一个原因是需要知道在对数组作出变更之后是否需要调用JNI_COMMIT的相关释放方法,如果要更改一个正在作出变更以及读取数组内容的操作,那么可以根据该标志跳过这次操作。另一个可能的原因就是用于有效的处理JNI_ABORT。举个例子,你可能想要得到一个数组,然后对其修改之后将其传给一个函数。如果你知道JNI会为你做一个副本的话,那么就不需要创建另外的可编辑副本了。如果JNI传回的是原始数据,那么你自己需要创建一个副本。

一个常见的错误就是如果*isCopy是false,那么可以不调用相关释放方法。但是事实并非如此,如果没有申请拷贝缓冲区,那么原始数据内存必定会被一直占用,也不会被垃圾收集器回收。

还要注意的是,JNI_COMMIT并不会释放数组,你需要在另外的标志执行后再执行一次释放。

方法调用

JNI在方法使用上有两种方式,一种如下所示:

    jbyte* data = env->GetByteArrayElements(array, NULL);if (data != NULL) {memcpy(buffer, data, len);env->ReleaseByteArrayElements(array, data, JNI_ABORT);}

上面这段代码首先得到了一个数组,然后拷贝出len个字节的元素,最后将这个数组释放。根据实现的不同,Get调用会返回原始数据或者数据副本。在这个案例中,JNI_ABORT可以确保不出现第三个副本。

另一种实现则要更简单一些:

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

对于此有若干建议:
- 减少JNI调用可以节省开销。
- 不要原始数据或者额外的数据拷贝。
- 降低程序员出错的风险–他们会在某些操作失败后忘记调用相关的释放方法。

类似的,你可以使用SetArrayRegion函数将数据拷贝到一个数组中,GetStringRegion函数或GetStringUTFRegion可以从String拷贝任意长度的字符。

异常

当异常出现时,请不要继续向下执行。代码应当注意到这些异常并返回,或者处理这些异常。

当异常发生时,只有以下JNI方法允许调用:

  • DeleteGlobalRef
  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • ReleaseArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

很多JNI函数都会抛出异常,不过只提供了一种很简单的检查方法。比如,如果NewString函数返回了一个非空的值,那么就不需要检查异常。然而,如果你调用一个方法,比如CallObjectMethod,那么就需要每次都检查一下异常,因为如果异常被抛出后,返回值是无效的。

主要注意的是,由中断所抛出的异常不会释放本地栈帧,Android目前也不支持C++异常。JNI的Throw与ThrowNew结构也只是在当前的线程设置了一个异常指针。当异常发生时也只是返回到代码调用处,异常也不会被正确的注意与处理。

本地代码可以通过ExceptionCheck函数或ExceptionOccurred函数捕获异常,并可以通过ExceptionClear函数清理这些异常。通常情况下,不处理这些异常会导致一些问题的出现。

JNI中并没有与Throwable相对应的映射函数,所以,如果你想获得异常字符串,那么就需要先找到Throwable类,然后查找相关的getMessage “()Ljava/lang/String;”方法ID,然后调用这些方法,如果返回的值是非空的话,再调用GetStringUTFChars函数来获得你想得到的异常字符串,最后将这些异常打印出来。

本地库

你可以通过标准的System.loadLibrary函数加载共享库中的本地代码。推荐获取本地代码的方法有:

  • System.loadLibrary(),该方法唯一的参数是一个简要的库名,所以如果要加载”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<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函数外加库的全限定名来加载本地库。

使用JNI_OnLoad另一个需要注意的是:任何FindClass调用都会发生在类加载器的上下文环境中,该类加载器用于加载共享库。通常情况下,FindClass所用到的加载器位于解释栈的顶端,如果还没有加载器,那么它会使用系统的加载器。

64位的注意事项

Android目前运行于32位的平台上。虽然理论上可以为64位的平台构建系统,但是目前它不是主要的目标。大多数情况下,这不是你需要担心的事情,但是如果要将指针存储于本地结构中的一个对象的Int属性上,那么这就很值得关注了。为了支持64位指针结构,你需要将本地指针存储于一个Long属性中

不支持特性与向后兼容

支持所有的JNI1.6特性,以及以下异常:
- DefineClass 还没有实现。Android并没有使用Java的字节码以及类文件,所以传入二进制的类数据是不会被执行的。

如果需要兼容Android老的版本,那么应该检查以下部分:

  • 动态查询本地函数
    • 在Android 2.0之前,字符’$’在查找方法时不会被正确的转换为”_00024”。所以使用有关方法需要明确注册或者将内部类方法移出。
  • 分离线程
    • 在Android 2.0之前,无法使用pthread_key_create析构函数来避免”在退出之前必须分离线程”这项检查。
  • 弱的全局引用
    • 在Android 2.2之前,弱的全局引用还没有实现。之前的版本会拒绝使用它们。你可以使用Android平台版本来检测是否支持。
    • 在Android 4.0之前,弱的全局引用只能被传入NewLocalRef, NewGlobalRef, 以及 DeleteWeakGlobalRef这几个函数。
    • 从Android 4.0开始,弱的全局引用可以像其它JNI引用一样使用。
  • 本地引用
    • 在Android 4.0之前,本地引用实际上就是指针。在Android 4.0之后添加了必要的中间角色,以便更好的支持垃圾回收器的工作,不过这意味着有很多JNI的bug在老版本上无法察觉。查看JNI Local Reference Changes in ICS获取更多信息。
  • 通过GetObjectRefType检查引用类型
    • 在Android 4.0之前,由于直接指针的使用,无法正确的实现GetObjectRefType。我们通过弱的全局表、参数、本地表以及全局表进行查找。首先它会找到你的直接指针,并返回它所检查的引用类型。这意味着,如果你在全局的jclass上作用GetObjectRefType,而这个jclass以一个隐性参数传给了一个静态本地方法,那么你将会获得JNILocalRefType而不是JNIGlobalRefType。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478784.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

拒绝暴力调参!推荐一个模型Debug神器!

近些年深度学习在视觉、自然语言处理、语音等各个技术方向都诞生了不少创新应用&#xff0c;如智能识别医疗图像中的病灶&#xff0c;辅助医生做病情诊断&#xff1b;智能判别生产线上有质量问题的产品&#xff0c;减轻人工质检压力&#xff1b;对政务、金融等流程中的证件票据…

TIFF图像被转换为​​JPEG格式的图像

TIFF图像被转换为​​JPEG格式的图像。 for name in glob.glob(root_dirimg_dir*.tif): im Image.open(name) name str(name).rstrip(".tif") name str(name).lstrip(root_dir) name str(name).lstrip(img_dir) im.save(final_root_di…

Android官方开发文档Training系列课程中文版:Android的安全建议

原文地址&#xff1a;http://android.xsoftlab.net/training/articles/security-tips.html Android系统内置的安全策略可以有效的降低应用程序的安全问题。所以默认创建的应用程序已经包含了一定程度的安全保护措施。 Android所包含的安全策略有&#xff1a; 应用程序沙箱&a…

论文浅尝 - IJCAI2020 | Mucko:基于事实的多层跨模态知识推理视觉问答

论文笔记整理&#xff1a;陈卓&#xff0c;浙江大学计算机科学与技术系&#xff0c;博士研究生。论文链接&#xff1a;https://arxiv.org/pdf/2006.09073代码&#xff1a;https://github.com/astro-zihao/mucko发表会议&#xff1a;IJCAI 2020任务定义及背景VQA&#xff08;视觉…

LeetCode 40. 组合总和 II(排列组合 回溯)

1. 题目 给定一个数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 说明&#xff1a; 所有数字&#xff08;包括目标数&#xff09;都是正整数。 解集不能包含重…

质量运营在智能支付业务测试中的初步实践

背景 毋庸置疑&#xff0c;质量是决定产品能否成功、企业能否持续发展的关键因素之一。对于“质量时代”下的互联网企业&#xff0c;如何在快速迭代的节奏中兼顾质量&#xff0c;真正落地“人人重视质量、人人创造质量、人人享受质量”&#xff0c;这是对QA的要求&#xff0c;也…

新手手册:Pytorch分布式训练

文 | 花花机器学习算法与自然语言处理单位 | SenseTime 算法研究员目录0X01 分布式并行训练概述0X02 Pytorch分布式数据并行0X03 手把手渐进式实战A. 单机单卡B. 单机多卡DPC. 多机多卡DDPD. Launch / Slurm 调度方式0X04 完整框架 Distribuuuu0X05 Reference文中所有教学代码和…

Hotel booking酒店预订——数据分析与建模

Hotel booking酒店预订——数据分析与建模&#xff1a;https://zhuanlan.zhihu.com/p/196757364?utm_sourcewechat_session 写文章Hotel booking酒店预订——数据分析与建模&#xff08;转载翻译自kaggle&#xff09;海上泊舟数据分析师数据源&#xff1a;https://www.science…

Android官方开发文档Training系列课程中文版:Activity测试之测试环境配置

原文地址&#xff1a;http://android.xsoftlab.net/training/activity-testing/index.html 引言 开发者应当将测试作为应用开发周期的一部分。良好的测试用例可以帮助开发者及早的发现Bug&#xff0c;同时也可以增强开发者对代码的信心。 测试用例定义了一系列对象与方法&am…

论文浅尝 - ICLR2020 | Pretrained Encyclopedia: 弱监督知识预训练语言模型

论文笔记整理&#xff1a;陈想&#xff0c;浙江大学博士&#xff0c;研究方向为自然语言处理&#xff0c;知识图谱。Wenhan Xiong, Jingfei Du, William Yang Wang, Veselin Stoyanov.Pretrained Encyclopedia: Weakly Supervised Knowledge-Pretrained Language Model来源&…

LeetCode 216. 组合总和 III(排列组合 回溯)

1. 题目 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数&#xff0c;并且每种组合中不存在重复的数字。 说明&#xff1a; 所有数字都是正整数。 解集不能包含重复的组合。 示例 1: 输入: k 3, n 7 输出: [[1,2,4]]示例 2: 输入: k 3, n 9 输出…

“小众”之美——Ruby在QA自动化中的应用

前言 关于测试领域的自动化&#xff0c;已有很多的文章做过介绍&#xff0c;“黑科技”也比比皆是&#xff0c;如通过Java字节码技术实现接口的录制&#xff0c;Fiddler录制内容转Python脚本&#xff0c;App中的插桩调试等&#xff0c;可见角度不同&#xff0c;对最佳实践的理解…

软考中级 软件设计师资料(考点分析+复习笔记+历年真题+电子版课本)

软考中级 软件设计师资料&#xff08;考点分析复习笔记历年真题电子版课本&#xff09;&#xff1a; https://blog.csdn.net/weixin_44754772/article/details/113763165 软件设计师是软考中级职称&#xff0c;相比高级的难度而言&#xff0c;中级难度较低&#xff0c;每个人花…

关于NLP相关技术全部在这里:预训练模型、图神经网络、模型压缩、知识图谱、信息抽取、序列模型、深度学习、语法分析、文本处理...

神器推荐NLP近几年非常火&#xff0c;且发展特别快。像BERT、GPT-3、图神经网络、知识图谱等技术应运而生。我们正处在信息爆炸的时代、面对每天铺天盖地的网络资源和论文、很多时候我们面临的问题并不是缺资源&#xff0c;而是找准资源并高效学习。但很多时候你会发现&#xf…

Android官方开发文档Training系列课程中文版:Activity测试之创建运行测试

原文地址&#xff1a;http://android.xsoftlab.net/training/activity-testing/activity-basic-testing.html 为了验证在布局与功能上没有差池&#xff0c;很重要的一点就是需要为每个Activity创建对应的测试类。对每个测试类还需要创建单独的测试用例&#xff0c;这其中包含测…

LeetCode 77. 组合(回溯)

1. 题目 给定两个整数 n 和 k&#xff0c;返回 1 … n 中所有可能的 k 个数的组合。 示例:输入: n 4, k 2 输出: [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ]来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode-cn.com/problems/combin…

论文浅尝 - AAAI2020 | 从异质外部知识库中进行基于图的推理实现常识知识问答...

会议&#xff1a;AAAI2020论文链接&#xff1a;https://arxiv.org/pdf/1909.05311.pdf摘要常识问答旨在回答需要背景知识的问题&#xff0c;而背景知识并未在问题中明确表达。关键的挑战是如何从外部知识中获取证据并根据证据做出预测。最近的研究要么从昂贵的人类注释中生成证…

机器学习算法中的准确率、精确率、召回率和F值

机器学习算法中的准确率、精确率、召回率和F值&#xff1a;https://www.jianshu.com/p/d400a821ef3d

Android官方开发文档Training系列课程中文版:Activity测试之UI组件测试

原文地址&#xff1a;http://android.xsoftlab.net/training/activity-testing/activity-ui-testing.html 一般来说&#xff0c;正因为Activity含有UI组件&#xff0c;所以使得用户可以与程序交互。这节课将会介绍如何测试Activity中的Button组件。在课程学习之后便可以使用相…

插件化、热补丁中绕不开的Proguard的坑

文章主体部分已经发表于《程序员》杂志2018年2月期&#xff0c;内容略有改动。 ProGuard简介 ProGuard是2002年由比利时程序员Eric Lafortune发布的一款优秀的开源代码优化、混淆工具&#xff0c;适用于Java和Android应用&#xff0c;目标是让程序更小&#xff0c;运行更快&…