1. JNI 概念
1.1 概念
JNI 全称 Java Native Interface,Java 本地化接口,可以通过 JNI 调用系统提供的 API。操作系统,无论是 Linux,Windows 还是 Mac OS,或者一些汇编语言写的底层硬件驱动都是 C/C++ 写的。Java和C/C++不同 ,它不会直接编译成平台机器码,而是编译成虚拟机可以运行的Java字节码的.class文件,通过JIT技术即时编译成本地机器码,所以有效率就比不上C/C++代码,JNI技术就解决了这一痛点,JNI 可以说是 C 语言和 Java 语言交流的适配器、中间件,下面我们来看看JNI调用示意图:来自JNI开发系列①JNI概念及开发流程 - 简书
JNI技术通过JVM调用到各个平台的API,虽然JNI可以调用C/C++,但是JNI调用还是比C/C++编写的原生应用还是要慢一点,不过对高性能计算来说,这点算不得什么,享受它的便利,也要承担它的弊端。
1.2 JNI 与 NDK 区别
- JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;
- NDK: NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发;
2. JNI 作用
- 扩展:JNI扩展了JVM能力,驱动开发,例如开发一个wifi驱动,可以将手机设置为无限路由;
- 高效: 本地代码效率高,游戏渲染,音频视频处理等方面使用JNI调用本地代码,C语言可以灵活操作内存;
- 复用: 在文件压缩算法 7zip开源代码库,机器视觉 OpenCV开放算法库等方面可以复用C平台上的代码,不必在开发一套完整的Java体系,避免重复发明轮子;
- 特殊: 产品的核心技术一般也采用JNI开发,不易破解;
JNI在Android中作用:
JNI可以调用本地代码库(即C/C++代码),并通过 Dalvik 虚拟机与应用层和应用框架层进行交互,Android中JNI代码主要位于应用层和应用框架层;
- 应用层: 该层是由JNI开发,主要使用标准JNI编程模型;
- 应用框架层: 使用的是Android中自定义的一套JNI编程模型,该自定义的JNI编程模型弥补了标准JNI编程模型的不足;
补充知识点:
Java语言执行流程:
- 编译字节码:Java编译器编译 .java源文件,获得.class 字节码文件;
- 装载类库:使用类装载器装载平台上的Java类库,并进行字节码验证;
- Java虚拟机:将字节码加入到JVM中,Java解释器和即时编译器同时处理字节码文件,将处理后的结果放入运行时系统;
- 调用JVM所在平台类库:JVM处理字节码后,转换成相应平台的操作,调用本平台底层类库进行相关处理;
Java一次编译到处执行: JVM在不同的操作系统都有实现,Java可以一次编译到处运行,字节码文件一旦编译好了,可以放在任何平台的虚拟机上运行;
3. 查看 jni.h 文件源码方法
jni.h 头文件就是为了让 C/C++ 类型和 Java 原始类型相匹配的头文件定义。
可以通过点击 Android项目的含有#include <jni.h>
的头文件或 C/C++ 文件跳转到 jni.h 头文件查看;
如果没有这样的文件的话,可以在 Android Studio 上新建一个类,随便写一个 native 方法,然后点击红色的方法,AS 会自动生成一个对应的 C 语言文件jnitest.c
,就可以找到 jni.h 文件了
或者,通过 javah 命令javah cn.cfanr.testjni.JniTest
,就可以生成对应头文件cn_cfanr_testjni_JniTest.h
:
4. JNI 数据类型映射
由头文件代码可以看到,jni.h有很多类型预编译的定义,并且区分了 C 和 C++的不同环境。
1 |
|
当是C++环境时,jobject, jclass, jstring, jarray 等都是继承自_jobject
类,而在 C 语言环境是,则它的本质都是空类型指针typedef void* jobject;
4.1 基本数据类型
下图是Java基本数据类型和本地类型的映射关系,这些基本数据类型都是可以直接在 Native 层直接使用的:
4.2 引用数据类型
另外,还有引用数据类型和本地类型的映射关系:
需要注意的是,
- 1)引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用;
- 2)多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;
例如,二维整型数组就是指向一位数组的数组,其声明使用方式如下:
1 | //获得一维数组的类引用,即jintArray类型 |
4.3 方法和变量 ID
同样不能直接在 Native 层使用。当 Native 层需要调用 Java 的某个方法时,需要通过 JNI 函数获取它的 ID,根据 ID 调用 JNI 函数获取该方法;变量的获取也是类似。ID 的结构体如下:
1 | struct _jfieldID; /* opaque structure */ |
5. JNI 描述符
5.1域描述符
1)基本类型描述符
下面是基本的数据类型的描述符,除了 boolean 和 long 类型分别是 Z 和 J 外,其他的描述符对应的都是Java类型名的大写首字母。另外,void 的描述符为 V
2)引用类型描述符
一般引用类型描述符的规则如下,注意不要丢掉“;”
1 | L + 类描述符 + ; |
如,String 类型的域描述符为:
1 | Ljava/lang/String; |
数组的域描述符特殊一点,如下,其中有多少级数组就有多少个“[”,数组的类型为类时,则有分号,为基本类型时没有分号
1 | [ + 其类型的域描述符 |
例如:
1 | int[] 描述符为 [I |
对应在 jni.h 获取 Java 的字段的 native 函数如下,name为 Java 的字段名字,sig 为域描述符
1 | //C |
具体使用,后面会讲到
5.2 类描述符
类描述符是类的完整名称:包名+类名,java 中包名用 . 分割,jni 中改为用 / 分割
如,Java 中 java.lang.String 类的描述符为 java/lang/String
native 层获取 Java 的类对象,需要通过 FindClass() 函数获取, jni.h 的函数定义如下:
1 | //C |
字符串参数就是类的引用类型描述符,如 Java 对象 cn.cfanr.jni.JniTest,对应字符串为Lcn/cfanr/jni/JniTest; 如下:
1 | jclass jclazz = env->FindClass("Lcn/cfanr/jni/JniTest;"); |
详细用法的例子,后面会讲到。
5.3 方法描述符
方法描述符需要将所有参数类型的域描述符按照声明顺序放入括号,然后再加上返回值类型的域描述符,其中没有参数时,不需要括号,如下规则:
1 | (参数……)返回类型 |
例如:
1 | Java 层方法 ——> JNI 函数签名 |
另外,对应在 jni.h 获取 Java 方法的 native 函数如下,其中 jclass 是获取到的类对象,name 是 Java 对应的方法名字,sig 就是上面说的方法描述符
1 | //C |
不过在实际编程中,如果使用 javah 工具来生成对应的 native 代码,就不需要手动编写对应的类型转换了。
6. JNIEnv 分析
JNIEnv 是 jni.h 文件最重要的部分,它的本质是指向函数表指针的指针(JavaVM也是),函数表里面定义了很多 JNI 函数,同时它也是区分 C 和 C++环境的(由上面介绍描述符时也可以看到),在 C 语言环境中,JNIEnv 是strut JNINativeInterface*
的指针别名。
1 | struct _JNIEnv; |
6.1 JNIEnv 特点
- JNIEnv 是一个指针,指向一组 JNI 函数,通过这些函数可以实现 Java 层和 JNI 层的交互,就是说通过 JNIEnv 调用 JNI 函数可以访问 Java 虚拟机,操作 Java 对象;
- 所有本地函数都会接收 JNIEnv 作为第一个参数;(不过 C++ 的JNI 函数已经对 JNIEnv 参数进行了封装,不用写在函数参数上)
- 用作线程局部存储,不能在线程间共享一个 JNIEnv 变量,也就是说 JNIEnv 只在创建它的线程有效,不能跨线程传递;相同的 Java 线程调用本地方法,所使用的 JNIEnv 是相同的,一个 native 方法不能被不同的 Java 线程调用;
6.2 JavaEnv 和 JavaVM 的关系
- 1)每个进程只有一个 JavaVM(理论上一个进程可以拥有多个 JavaVM 对象,但 Android 只允许一个),每个线程都会有一个 JNIEnv,大部分 JNIAPI 通过 JNIEnv 调用;也就是说,JNI 全局只有一个 JavaVM,而可能有多个 JNIEnv;
- 2)一个 JNIEnv 内部包含一个 Pointer,Pointer 指向 Dalvik 的 JavaVM 对象的 Function Table,JNIEnv 内部的函数执行环境来源于 Dalvik 虚拟机;
- 3)Android 中每当一个Java 线程第一次要调用本地 C/C++ 代码时,Dalvik 虚拟机实例会为该 Java 线程产生一个 JNIEnv 指针;
- 4)Java 每条线程在和 C/C++ 互相调用时,JNIEnv 是互相独立,互不干扰的,这样就提升了并发执行时的安全性;
- 5)当本地的 C/C++ 代码想要获得当前线程所想要使用的 JNIEnv 时,可以使用 Dalvik VM 对象的 JavaVM* jvm->GetEnv()方法,该方法会返回当前线程所在的 JNIEnv*;
- 6)Java 的 dex 字节码和 C/C++ 的 .so 同时运行 Dalvik VM 之内,共同使用一个进程空间;
6.3 C 语言的 JNIEnv
由上面代码可知,C 语言的JNIEnv 就是const struct JNINativeInterface*
,而 JNIEnv* env
就等价于JNINativeInterface** env
,env 实际是一个二级指针,所以想要得到 JNINativeInterface 结构体中定义的函数指针,就需要先获取 JNINativeInterface 的一级指针对象*env,然后才能通过一级指针对象调用 JNI 函数,例如:(*env)->NewStringUTF(env, "hello")
1 | struct JNINativeInterface { |
6.4 C++的 JNIEnv
由typedef _JNIEnv JNIEnv;
可知,C++的 JNIEnv 是 _JNIEnv 结构体,而 _JNIEnv 结构体定义了 JNINativeInterface 的结构体指针,内部定义的函数实际上是调用 JNINativeInterface 的函数,所以C++的 env 是一级指针,调用时不需要加 env 作为函数的参数,例如:env->NewStringUTF(env, "hello")
1 | struct _JNIEnv { |
7. JNI 的两种注册方式
Java 的 native 方法是如何链接 C/C++中的函数的呢?可以通过静态和动态的方式注册JNI。
7.1 静态注册
原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:
- 先编写 Java 的 native 方法;
- 然后用 javah 工具生成对应的头文件,执行命令
javah packagename.classname
可以生成由包名加类名命名的 jni 层头文件,或执行命名javah -o custom.h packagename.classname
,其中 custom.h 为自定义的文件名; - 实现 JNI 里面的函数,再在Java中通过System.loadLibrary加载 so 库即可;
静态注册的方式有两个重要的关键词 JNIEXPORT 和 JNICALL,这两个关键词是宏定义,主要是注明该函数式 JNI 函数,当虚拟机加载 so 库时,如果发现函数含有这两个宏定义时,就会链接到对应的 Java 层的 native 方法。
由前面3. 查看 jni.h 文件源码方法
生成头文件的方法,重新创建一个cn.cfanr.test_jni.Jni_Test.java
的类
1 | public class Jni_Test { |
用 javah 工具生成以下头文件:
1 |
|
可以看出 JNI 的调用函数的定义是按照一定规则命名的:JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数);
其中 Java_ 是为了标识该函数来源于 Java。经检验(不一定正确),如果是重载的方法,则有“参数签名”,否则没有;另外如果使用的是 C++,在函数前面加上 extern “C”(表示按照 C 的方式编译),函数命名后面就不需要加上“参数签名”。
另外还需要注意几点特殊规则:参考:官方JNI规范翻译 | linlinjava的博客 2.2.1 本地方法名解析
- 1. 包名或类名或方法名中含下划线 _ 要用 _1 连接;
- 2. 重载的本地方法命名要用双下划线 __ 连接;
- 3. 参数签名的斜杠 “/” 改为下划线 “_” 连接,分号 “;” 改为 “_2” 连接,左方括号 “[” 改为 “_3” 连接;
另外,对于 Java 的 native 方法,static 和非 static 方法的区别在于第二个参数,static 的为 jclass,非 static 的 为 jobject;JNI 函数中是没有修饰符的。
优点:
实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的native层代码的函数;
缺点:
- javah 生成的 native 层函数名特别长,可读性很差;
- 后期修改文件名、类名或函数名时,头文件的函数将失效,需要重新生成或手动改,比较麻烦;
- 程序运行效率低,首次调用 native 函数时,需要根据函数名在 JNI 层搜索对应的本地函数,建立对应关系,有点耗时;
7.2 动态注册
原理:直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系,步骤:
- 先编写 Java 的 native 方法;
- 编写 JNI 函数的实现(函数名可以随便命名);
- 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
- 利用
registerNatives(JNIEnv* env)
注册类的所有本地方法; - 在 JNI_OnLoad 方法中调用注册方法;
- 在Java中通过System.loadLibrary加载完JNI动态库之后,会调用JNI_OnLoad函数,完成动态注册;
1 | //JNINativeMethod结构体 |
由于篇幅原因,具体的静态注册、动态注册、数据类型映射和描述符的练习放到下一篇文章。
注:文章通过阅读 JNI 的文档和参照网上的博客总结出来的,如有错误,还望指出!
参考:
JNI开发系列①JNI概念及开发流程 - 简书
JNI 数据类型映射、域描述符说明 - qinjuning - CSDN博客
Android JNI 之 JNIEnv 解析 - 韩曙亮 - CSDN博客
Android 开发 之 JNI入门 - NDK从入门到精通 -韩曙亮 - CSDN博客
JNI 两种注册过程实战 - Android - 掘金
Andoid NDK编程 注册native函数 // Coding Life
扩展阅读:
JNI 使用指南-胡凯
JNI 常用函数大全 qinjuning- CSDN博客
Android JNI原理分析 - Gityuan博客 | 袁辉辉博客
JNI API 文档 (Java8): Java Native Interface Specification
官方JNI API 规范翻译 | linlinjava的博客