jni介绍
jni全称java native interface,我把它分为三部分,java代表java语言,native代表当前程序运行的本地环境,一般指windows/linux,而这些操作系统都是通过C/C++实现的,所以native通常也指C/C++语言,interface代表java跟native两者之间的通信接口,jni可以实现java和C/C++通信。它是java生态的特征,所以定义在jdk标准当中。
使用场景和优势
java虽然跨平台,但仍然运行在具体平台(windows,linux)之上,对于需要操作硬件的功能,必须通过系统的C/C++方法对硬件进行直接操作,比如打开文件,java层必须调用系统的open方法(linux是open,windows是openFile)才能打开文件,这个时候就涉及到java代码如何调用C/C++代码的问题
在一些拥有复杂算法的场景(音视频编解码,图像绘制等),java的执行效率远低于C/C++的执行效率,使用jni技术,在java层调用C/C++代码,可以提高程序的执行效率,最大化利用机器的硬件资源。
native层的代码往往更加安全,反编译so文件比反编译jar文件要难得多,所以,我们往往把涉及到密码密钥相关的功能用C/C++实现,然后java层通过jni调用
通信原理
java运行在jvm,jvm本身就是使用C/C++编写的,因此jni只需要在java代码、jvm、C/C++代码之间做切换即可
使用步骤
基于ubuntu(之前基于windows,后来很多人对dll动态库感到很陌生,现在在linux平台也操作一遍,so动态库大家都熟),为了方便,我使用了idea+clion,读者需要能掌握这两个工具的基本使用,跟Android Studio差不多的。整个过程分为了十步,我称之为jni十步曲:
1.使用idea创建一个java工程,并创建JNIDemo.java文件
2.在JNIDemo.java文件中声明native方法helloJni()
java
体验AI代码助手
代码解读
复制代码
public class JNIDemo { public static native String helloJni(); }
3.使用javac命令编译JNIDemo.java,生成JNIDemo.class文件
4.使用javah命令生成JNIDemo.h文件
5.使用clion创建C++ library项目,并复制刚刚生成的com_jason_jni_JNIDemo.h头文件到项目根目录
库类型选择shared,表示编译生成动态库,static为静态库,动态库和静态库的最大区别就在于静态库会将目标代码以及所有需要依赖的库文件进行整体打包,执行时不再依赖外部环境。动态库则只会将目标代码打包,运行时需要依赖外部环境,所以一般来说,静态库往往比动态库要大。windows上的动态库为.dll文件,静态库为.lib文件。linux上的动态库为.so文件,静态库为.a文件。
6.创建JNIDemo.cpp文件,实现helloJni()方法
这里我直接返回了I am from c++字符串,同时要将JNIDemo.cpp文件添加到CMakeList.txt中
这个时候我们看到com_jason_jni_JNIDemo.h文件中有报错
这是因为无法从系统中找到jni.h头文件,这里我们可以手动导入jni.h到项目中,开头说了,jni是java的特征,所以jni.h文件在jdk当中
windows去本地jdk安装目中找
将这两个文件拷贝到项目根目录中,然后将#include
7.编译本地代码,生成libjnidemo.so文件
8.在刚刚的java项目的根目录中创建libs文件夹,并将其设置为资源文件夹,然后将生成的libjnidemo.so文件拷贝到该目录中
注意libs目录的图标一定要是资源文件夹的样式,不是普通文件夹的样式,然后将libjnidemo.so文件拷贝到该目录下
9.在java代码中通过System.loadLibrary()加载so文件
java
体验AI代码助手
代码解读
复制代码
public class JNIDemo { static { System.loadLibrary("jnidemo"); } public static native String helloJni(); public static void main(String[] args) { System.out.println(helloJni()); } }
10.将该libjnidemo.so库添加到虚拟机运行环境,然后运行java程序
值设置为-Djava.library.path=/home/q/IdeaProjects/JNIDemo/libs,等号后面为libjnidemo.so文件所在的路径
在main()函数处右键,运行该程序
成功输出I am from c++
上面通过一个简单的案例讲解了jni的使用流程,从中不难看出,大部分步骤都是固定的,唯一不固定的是JNIDemo.cpp的内容,这个取决于实际的需求。而在新版的Android Studio当中已经把这些固定流程封装成了模板操作,我们可以一键生成头文件和源文件,开发者只需要关注源文件的功能实现即可。
只需要在新建项目时选择Native C++即可,这里我就不做具体演示了,有兴趣的读者可以自行尝试。
API详解
刚刚我只是简单的返回了一个字符串,实际上我们还可以做很多事情,jni.h都给我们定义好了标准,我们按照它的标准来即可。
开头提到,java和C/C++通信是通过jni来完成的,那么在jni方法中就涉及到对java变量的访问(变量类型包括基本数据类型和引用数据类型),对java方法的调用,java对象的创建等,而java语法跟jni语法不一定是一 一对应的,比如,java中叫boolean,jni中叫jboolean,那怎么解决这个问题呢,jni给我们提供了若干个映射表,将java中的类型与jni中的类型进行了一 一映射,其中包括基本数据类型映射,引用数据类型映射,方法签名(包含参数和返回值)映射,以下是这三个映射表:
表1-基本数据类型映射表
表2-引用数据类型映射表
表3-方法签名
以上面Demo来分析
c++
体验AI代码助手
代码解读
复制代码
//Java方法 public static native String helloJni(); public static native float helloJni2(int age, boolean isChild); //jni方法 extern "C" JNIEXPORT jstring JNICALL Java_com_jason_jni_JNIDemo_helloJni (JNIEnv *env, jclass clazz){ return env->NewStringUTF("I am from c++"); } extern "C" JNIEXPORT jfloat JNICALL Java_com_jason_jni_JNIDemo_helloJni2 (JNIEnv *env, jclass clazz, jint age, jboolean isChild){ }
java方法helloJni()的返回值为String,映射到jni方法中的返回值即为jstring,我们新增一个方法helloJni2(int age, boolean isChild),增加了两个参数int和boolean,对应的映射为jint和jboolean,同时返回值float映射为jfloat。
解决了数据类型不一致的问题之后,接下来就可以在jni方法中访问java成员了,同样的,jni给我们提供了一系列访问java成员的API,具体如下:
jni访问调用对象
方法名作用GetObjectClass获取调用对象的类,我们称其为targetFindClass根据类名获取某个类,我们称其为targetIsInstanceOf判断一个类是否为某个类型IsSameObject是否指向同一个对象
jni访问java成员变量的值
方法名作用GetFieldId根据变量名获取target中成员变量的IDGetIntField根据变量ID获取int变量的值,对应的还有byte,boolean,long等SetIntField修改int变量的值,对应的还有byte,boolean,long等
jni访问java静态变量的值
方法名作用GetStaticFieldId根据变量名获取target中静态变量的IDGetStaticIntField根据变量ID获取int静态变量的值,对应的还有byte,boolean,long等SetStaticIntField修改int静态变量的值,对应的还有byte,boolean,long等
jni访问java成员方法
方法名作用GetMethodID根据方法名获取target中成员方法的IDCallVoidMethod执行无返回值成员方法CallIntMethod执行int返回值成员方法,对应的还有byte,boolean,long等
jni访问java静态方法
方法名作用GetStaticMethodID根据方法名获取target中静态方法的IDCallStaticVoidMethod执行无返回值静态方法CallStaticIntMethod执行int返回值静态方法,对应的还有byte,boolean,long等
jni访问java构造方法
方法名作用GetMethodID根据方法名获取target中构造方法的ID,注意,方法名传
jni创建引用
方法名作用NewGlobalRef创建全局引用NewWeakGlobalRef创建弱全局引用NewLocalRef创建局部引用DeleteGlobalRef释放全局对象,引用不主动释放会导致内存泄漏DeleteLocalRef释放局部对象,引用不主动释放会导致内存泄漏
除此之外,jni还提供了异常处理机制,处理方式跟java一样有两种,要么往上(java层)抛,要么自己捕获处理
方法名作用ExceptionOccurred判断是否有异常发生ExceptionClear清除异常Throw往上(java层)抛出异常ThrowNew往上(java层)抛出自定义异常
API有很多,上述只是列出了一些常用的,其他的可以自行到jni.h文件里去查看。
案例实战
以一个完整的demo来进行综合实战,在实战中感受jni的使用姿势,为了方便,我直接在Android Studio里面创建了一个Native工程。
需求:统计按钮的点击次数
代码如下:
java
体验AI代码助手
代码解读
复制代码
public class MainActivity extends AppCompatActivity { private static final String TAG = "jasonwan"; private TextView tv; static { System.loadLibrary("jni"); } private int num = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = findViewById(R.id.sample_text); tv.setOnClickListener(v -> { jnitTest() }); } //jni测试代码主要在这个方法里面 public native void jniTest(); }
c++
体验AI代码助手
代码解读
复制代码
#include
最后的效果是这样的
通过这样一个简单的案例,将大部分jni相关的API都练习了一遍,不难看出,java层能实现的功能,在native层一样可以实现,但这里仅仅是为了练习jni,实际项目中不会把一些无关紧要的功能写在native层,比如UI操作,因为同样的功能,java代码要简洁得太多。
上面我们在实现jniTest()时,可以看到c++里面的方法名很长Java_com_jason_jni_MainActivity_jniTest,这是jni静态注册的方式,按照jni规范的命名规则进行查找,格式为Java_类路径_方法名,这种方式在应用层开发用的比较广泛,因为Android Studio默认就是用这种方式,而在framework当中几乎都是采用动态注册的方式来实现java和c/c++的通信。比如之前研究过的《Android MediaPlayer源码分析》,里面就是采用的动态注册的方式。
在Android中,当程序在Java层运行System.loadLibrary("jnitest");这行代码后,程序会去载入libjnitset.so文件。于此同时,产生一个Load事件,这个事件触发后,程序默认会在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,与Load事件相对,在载入的.so文件被卸载时,Unload事件被触发。此时,程序默认会去载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,然后卸载.so文件。因此开发者经常会在JNI_OnLoad中做一些初始化操作,动态注册就是在这里进行的,使用env->RegisterNatives(clazz, gMethods, numMethods)
参数1:Java对应的类参数2:JNINativeMethod数组参数3:JNINativeMethod数组的长度,也就是要注册的方法的个数
JNINativeMethod是jni中定义的一个结构体
c++
体验AI代码助手
代码解读
复制代码
typedef struct { const char* name; //java中要注册的native方法名 const char* signature;//方法签名 void* fnPtr;//对应映射到C/C++中的函数指针 } JNINativeMethod;
相比静态注册,动态注册的灵活性更高,如果修改了native函数所在类的包名或类名,仅调整native函数的签名信息即可。上述案例改为动态注册,java代码不需要更改,只需要更改native代码
c++
体验AI代码助手
代码解读
复制代码
#include
注意,在Android工程中要排除对native方法以及所在类的混淆(java工程不需要),否则要注册的java类和java函数会找不到。proguard-rules.pro中添加
properties
体验AI代码助手
代码解读
复制代码
# 设置所有 native 方法不被混淆 -keepclasseswithmembernames class * { native
到这里,你应该了解jni的基本使用姿势了,剩下的就是不断的实践来巩固技能。附上Demo源码:gitee.com/jasonwan/JN…
参考文章
JNI方法注册源码分析
NDK 系列(5):JNI 从入门到实践,爆肝万字详解!
基础JNI语法和常见使用
链接:https://juejin.cn/post/7198434169982304316