一.JNI概述
JNI即java native interface,通俗的说,JNI是一种技术,通过这种技术可以做到以下两点:
- java程序中的函数可以调用C/C++编写的函数
- C/C++程序可以通过它调用java层的函数
JNI主要是完成java和C/C++代码和交互,但是java为什么要调用C/C++程序哪,这是不是破坏了java的平台无关特性,其实java需要调用C/C++程序需要是出于以下几点:
早在java语言诞生前,很多程序都是由C/C++语言编写的,他们遍布在软件世界的各个角落。虽然java出世后备受追捧,但是并不能完全替代C/C++,很多功能还需要调用它们,重复制造轮子会影响用户的体验,而JNI技术则解决了这个问题。
C/C++程序的运行效率是高于java的(虽然后期推出了JIT技术),在很多在运行速度上要求比较高、调用比较频繁的场合就可能选用C/C++语言。
有时需要和平台代码通信而又没有对应的Java API的时候,也需要使用C/C++代码编写好相应的代码供Java层使用。
二.SWIG概述
SWIG是一个软件开发工具,它将用C和C ++编写的程序与各种高级编程语言连接起来。SWIG与不同类型的目标语言一起使用。SWIG可以作为测试和原型化C / C ++软件的工具。SWIG通常用于解析C / C ++接口并生成各种语言调用C / C ++代码所需的“粘合代码”。转化代码时是非常方便实用的,而且用起来也不是太复杂,最主要的是写一次代码可以为多种语言调用。工具是开源的,学习手册请查看 http://www.swig.org/
三.示例
俗话说,百闻不如一见,我们先实现一个JNI调用的例子,然后再解析其原理。以下操作是在ubuntu环境下进行的。
-
搭建编译环境
Android studio,android 原生开发工具包(NDK),CMake,LLDB 调试工具。
可以使用SDK 管理器安装这些组件: -
在打开的项目中,从菜单栏选择Tools > Android > SDK Manager。
-
点击SDK Tools 标签。
-
选中LLDB、CMake 和NDK 旁的复选框。
-
点击Apply,然后在弹出式对话框中点击OK。
-
安装完成后,点击Finish,然后点击OK。
- 编写C代码
- 编写SWIG的转化文件
-
生成JNI调用所需的文件
使用命名swig -java -package com.example.jni add.i,会生成三个文件
add_wrap.c:用于和java 对接的JNI函数
addJNI.java:用于和JNI 对接的java 本地接口
add.java:java 本地调用的接口,他调用addJNI.java 中的内容
-
生成C库
cat CMakeLists.txt
注意两点:
1.将刚生成都add_wrap.c文件添加到库,如果没有源码可以将add_wrap.c编译为独立的动态库文件
2.添加jni.h文件(安装NDK后就会存在)到可查找的头文件目录中,add_wrap.c使用了jni.h文件
生成的目录如下
build
├── arm64-v8a
│ └── lib
│ └── libadd.so
├── armeabi-v7a
│ └── lib
│ └── libadd.so
├── x86
│ └── lib
│ └── libadd.so
└── x86_64
└── lib
└── libadd.so
将so文件移到lib同级目录下,删除lib文件(和Android Studio配置一致)
-
Android studio测试运行
4.测试程序如下,测试成功
1.Android studio创建新的项目,在向导的Configure your new project部分,选中Include C++ Support 复选框。
2.将生成的so 文件复制到app\src\main\jniLibs 目录下(四种库和文件夹一起复制)
3.将SWIG生成的add.java 和addJNI.java 文件复制 app\src\main\java\com\example\jni 目录下(与导出时package 参数一致)
大功告成,下面让我们看一下它的工作流程吧。
四.加载过程
大家都知道,如果我们希望使用C/C++函数,将其加载到内存是基础,那到底什么时候加载,怎么加载的哪?
其实只要在调用前,可以在任何时候加载本地库,但是我们一般的做法是在java类static语句中加载,加载的方式是通过System.loadLibrary方法。需要注意的是System.loadLibrary的参数是动态库的名字,如上示例,在linux下为libadd.so,而在window下为add.dll。其实JNI对java程序员还是比较照顾的,他们只需要加载库文件,其他的都不需要做。native修饰的函数也已经被JNI自动生成,如果在*JNI.java中添加加载库的static函数,那么java程序员连加载库都免了,是不是感觉很方便。
五.注册过程
读到这里,相信大家应该还是有一个很大的疑问,C库我已经将其加载,但是它又是怎么和java函数联系起来的哪,为什么我调用java函数而它会自动调用C库的函数,他们之间是怎么联系起来的哪?
正常的java方法是在加载的过程中会被放到方法区,而native方法则没有被定义。这就必须存在一个注册的过程,即将C库中的函数(上例中add_wrap.c中的函数)与Java中Native方法(上例中addJIN.java中方法)建立对应。注册过程分为两种:
- 静态注册
当java层调用add方法时,如上例所示,他会从C库中查找Java_com_example_jni_addJNI_add方法,如果没有则报错,如果找到就将两个函数建立一个关联,以后调用add方法时就会直接调用Java_com_example_jni_addJNI_add函数。从这里可以看出,静态方法是通过函数名来建立java函数和C函数之间的对应的。所以它要求对应的C函数(add_wrap.c)必须遵循特定的格式。上例就是通过静态的方式进行注册。
静态注册是有缺点的:
需要编译所有生命了native方法的java类,每个class文件都需要对应一个**JNI.java的文件。
生成的JNI层函数名特别长,书写起来不方便
初次调用native函数时需要根据函数名搜索对应的JNI层函数来建立连接,影响运行效率
- 动态注册
有一个指向这种数据结构的二级指针来保存所有的本地方法。所以我们就可以考虑在类加载的时候直接将本地方法和JNI函数建立连接,这种方式就是动态注册。
而在java层我们只用了System.loadLibray方法,所以因该在这个方法中进行动态注册。顺藤摸瓜我们可以找到JNI_Onload函数,在执行System.loadLibrary方法时。
JNI_Onload函数主要操作有两部分:
- 通过JNIEnv的FindClass函数找到java层注册JNI的方法
- 调用该函数
作者:黄兴阁
【BitTribeLab】BitTribe Lab是一个全球创新实验室(节点)网络,以建设去中心化未来信息基础设施和去中心化金融(Defi)新世界为愿景,邀请思想家、科幻小说爱好者、技术极客、开源软件开发者等等加盟。
【闪电网络北京研发小组】:是由BitTribeLab发起的,由北京地区专注于区块链以及闪电网络技术研究的爱好者组成的开放性社群,欢迎加入我们,讨论产品技术。
**微信群秘小姐姐:flyari
网友评论