JNI 学习笔记(一)
JNI 学习笔记(一)
JNI层面的知识是Android进阶必备的知识之一,下面我们就来看一下这里面都有些什么吧
¶1. 简介
JNI (Java Native Interface),简单翻译就是Java本地接口,作为程序员,对于接口还是比较敏感的,这可能代表会有一些功能提供给我们调用,至少大概的印象是这样的
根据官方给出的解释,JNI 算得上是一种编程框架,能够使得JVM去调用本地的应用或者库,或者被其他程序调用,其中,这些本地代码通过其他语言书写,最常见的是C/C++
至此,我们可以大概梳理一下 JNI 在我们脑海里模糊的影像
似乎就像是一个桥梁,建立了 Java 代码(或者说是 JVM )与本地应用程序(或者通俗些,C/C++ 代码)之间的联系,你可以调我,反过来,我也可以调你
¶2. JNI 的用途
简单了解了 JNI 是什么之后,我们又不禁想要知道为什么我们需要 JNI ? 技术的产生总是带有目的性的,那么,JNI 是用于解决什么问题的呢?
- 操作硬件相关内容(操作系统API往往基于C/C++)
- 提升处理性能(音视频、渲染、大量计算操作等)
- 提升安全性(Java的字节码文件更容易被反编译)
¶3. 简单例子
看概念类的东西很花时间,也容易遗忘,因此还是自己动手,通过实际编写代码来感悟,能更好加深我们的印象
首先,新建一个类,定义了一个native
方法
1 | public class JNITest { |
使用的时候,可以直接实例化,调用定义的public
方法,不过目前还用不了
1 | public class Main { |
接下来,我们使用javac
命令,生成对应于 JNITest
类的 C/C++ 头文件
1 | javac -h . JNITest.java |
现在目录下会多出两个文件,其中.class
后缀的是源文件对应的字节码文件,而另一个.h
后缀的便是我们需要的头文件
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
其中,Java_jni_JNITest_test()
对应的便是我们先前定义的test()
简单了解一下声明的Java_jni_JNITest_test()
当中的参数:
JNIEnv *
明显是一个指针类型,JNIEnv
本身内部提供了很多函数,从名字也能够看出,它代表的应该是一个“环境”
在 JDK 的 include/
目录下可以找到 jni.h
头文件
JNIEnv
指向JNINativeInterface_
结构体,而这个结构体定义许多环境相关的函数
环境的建立是后续各种操作的前提,因此JNIEnv
实例作为第一个参数传递给所有的函数调用方
接下来再看,jobject
是指向this
的 Java 对象,代表自身的实例,如果将native
方法改为static
类型,对应的将是jclass
类型参数,代表 Java 的类
extern "C"
告诉编译器以 C 的方式编译函数,以便于其他 C 程序链接和访问
与 C++ 不同,C 的函数直接依据函数名识别,而 C++ 中由于涉及到了函数重载,因此需要结合函数名、参数列表、返回值三者共同组合进行识别,最终编译而成的目标代码在命名协议上会存在差异,这也是容易引起问题的地方
剩下来JNICALL
和JNIEXPORT
,这是两个宏,在Linux平台的定义如下:
1 | // 保证在本动态库中声明的方法 , 能够在其他项目中可以被调用 |
接下来,在有了头文件的基础上,进一步去实现头文件中声明的 JNICALL Java_jni_JNITest_test
函数
我们添加一个.c
的C语言源文件,进行函数实现
1 |
|
然后进行代码的编译、执行
1 | gcc -I"C:\Program Files\Java\jdk-17\include" -I"C:\Program Files\Java\jdk-17\include\win32" -shared -o test.dll JNITest.c |
由于用的是Windows系统,因此对应生成的是.dll
的动态链接库
把.dll
文件拷贝到 JDK 的/bin
目录下,然后运行代码
可以看到我们实现的 C 代码被执行了,那么这种方式是所谓静态注册
大概整理一下先前我们都做了些什么:
- Java 代码加载库,定义
native
方法,以及对应的方法调用 - 包含
native
方法的 Java 类生成对应的 C/C++ 头文件 - 对应 C/C++ 头文件中声明的方法进行方法的实现
- 链接生成库,Windows下是
.dll
,Linux下是.so
¶3. 动态注册
除了静态注册,JNI 还有一种称为动态注册的使用方式
1 | public class DynamicJNITest { |
这次我们添加了几个重载方法,显得内容更加丰富,其他好像没有太大区别
不过,这次我们不用生成头文件,而是直接编写 C 的源代码文件
1 |
|
这一段的内容比较长,不过我们可以将其主要分为3个部分
首先看第一部分的主要代码(模板就省去了,像#include
和extern
等)
1 | JNIEXPORT void JNICALL c_test1 // test() |
这边的几个函数对应的便是在 Java 当中定义的几个native
方法,而参数中的jint
、jstring
代表的是 C 当中对应 Java 类型的表示,在函数的实现逻辑当中进行一些函数签名的输出,方便校验,这就算是进行了简单的函数实现
接下来,需要对于函数进行关联,用到的是JNINativeMethod
数组,将函数间的对应关系进行绑定
1 | // 方法名映射 |
比如第一个元素,“test"是 Java 中native
方法的函数名,”()V"表示它不接受入参,返回类型为void
,这种表示方法可以参考下表,是对于函数签名的表示,将函数的入参、出参及对应类型简化为标识
类型标识 | 对应 Java 类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L包名/类名; | 各种引用类型 |
V | void |
右边则相对统一,(void*)c_test1
其实就是将我们在 C 文件中定义的名称与 Java 中对应的方法建立关联关系,像c_test1
这种方法名是我们自己写的,并非系统生成
最后,则是添加一个JNI_OnLoad()
函数,光看参数名称也可以知道这与 Java 虚拟机密切相关
1 | // 获取JNI env变量 |
这一步,通过GetEnv()
获取JNIEnv
,之前也说了,这是 JNI 方法调用的前提,环境OK才能进行下面的工作
1 | // 获取native方法所在类 |
再接下来,是根据 Java 的全类名,通过FindClass()
获取包含native
方法的类对象,clazz
表示对应的类
1 | // 动态注册native方法 |
最后,将类信息、方法信息、方法数量信息等内容传给RegisterNatives()
函数,完成最终注册
1 | gcc -I"C:\Program Files\Java\jdk-17\include" -I"C:\Program Files\Java\jdk-17\include\win32" -shared -o test.dll DynamicJNITest.c |
执行gcc
命令生成.dll
,打包替换原来的test.dll
1 | public class Main { |
执行测试用例,各个native
方法都依次成功调用了
回顾一下,动态注册我们主要做了以下几件事:
- Java 类中声明并调用
native
方法 - 编写 C 代码,包括方法实现、映射关系建立、类方法信息注册(没有编译生成头文件)
- 生成
.dll
文件
其实这么看,好像静态、动态各有优势,应当如何选择呢?随着往后的学习我们来解答
(31条消息) 【Android NDK 开发】JNI 方法解析 ( JNIEXPORT 与 JNICALL 宏定义作用 )_韩曙亮的博客-CSDN博客