网上看了一些JNI的入门教程,对新手来说很不友好,容易看的人一脸懵,决定自己写一个从0到1的入门教程。
关于JNI,谷歌官方也提供了入门教程,详情请查看:NDK入门教程
JNI(Java Native Interface)是Java编程语言提供的一种编程框架和技术,用于在Java应用程序中调用Native代码(通常是用C/C++编写的)以实现底层功能和与操作系统、硬件交互。JNI允许开发者编写用C/C++等本地语言编写的代码,然后通过JNI接口与Java代码进行交互。例如常见的音视频处理,图像处理,地图等等都会用到JNI。
原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。
您可以在 Android Studio 2.2 或更高版本中使用 NDK 将 C 和 C++ 代码编译到Native库中,然后使用 Android Studio 的集成构建系统 Gradle 将Native库打包到 APK 中。Java 代码随后可以通过JNI框架调用Native库中的函数。
Android Studio 编译Native库的默认构建工具是 CMake。由于很多现有项目都使用 ndk-build 构建工具包,因此 Android Studio 也支持 ndk-build。不过,如果您要创建新的原生库,则应使用 CMake。CMake是一款外部构建工具,可与 Gradle 搭配使用来构建原生库。
ndk-build与CMake都是构建Native代码的工具,安卓 Gradle Plugin 4.0版本开始谷歌更推荐使用CMake。
CMakeLists.txt
和Android.mk
都是用于构建和管理安卓应用中的本地代码的构建脚本文件。
CMakeLists.txt: 是CMake的配置文件,用于描述项目的构建过程和所需的构建设置。在安卓项目中,CMakeLists.txt文件通常位于app
模块的根目录下。
CMakeLists.txt文件可以包含以下内容:
使用CMake,可以根据项目需要配置C/C++编译器、库路径、编译标志等,从而生成适合目标平台的构建文件。 更多使用方法请参考配置CMake
Android.mk:是安卓应用中使用的一个Makefile格式的构建脚本文件。它是基于GNU Make的构建系统,用于构建和管理应用中的本地代码。默认情况下,它位于应用项目目录中的 jni/Android
.mk
下。
在安卓应用的早期,Android.mk
是主要用于构建本地代码的标准构建脚本文件。它提供了一种描述本地库和编译设置的方式。
Android.mk
文件通常位于安卓项目的jni
目录下,其中包含了以下内容:
Android.mk
还可以使用预定义的变量和函数,以及Makefile的语法来控制构建过程。更多使用方法请参考Android.mk
Application.mk:指定 ndk-build 的项目级设置。默认情况下,它位于应用项目目录中的 jni/Application.mk
下。我们通常在这里指定编译Native库的ABI版本,即 armeabi-v7a、arm64-v8a、x86等等。更多配置请参考Application.mk
既然用到了C/C++,那么数据类型的转换肯定是有区别的。关于C/C++的基础知识请自行学习,这是使用JNI的前提,这里讲一下JNI中的数据类型转换。
JNI(Java Native Interface)支持多种数据类型,用于在Java代码和Native代码之间进行数据传递和类型转换。以下是一些常见的JNI数据类型:
基本数据类型:
jboolean
: 布尔类型,对应Java中的boolean
。jbyte
: 字节类型,对应Java中的byte
。jchar
: 字符类型,对应Java中的char
。jshort
: 短整型,对应Java中的short
。jint
: 整型,对应Java中的int
。jlong
: 长整型,对应Java中的long
。jfloat
: 单精度浮点型,对应Java中的float
。jdouble
: 双精度浮点型,对应Java中的double
。引用类型:
jobject
: 通用对象引用类型,对应Java中的Object
。jclass
: 类引用类型,对应Java中的Class
。jstring
: 字符串类型,对应Java中的String
。jarray
: 数组类型,用于表示Java中的数组对象。jbooleanArray
: 布尔数组类型,对应Java中的boolean[]
。jbyteArray
: 字节数组类型,对应Java中的byte[]
。jcharArray
: 字符数组类型,对应Java中的char[]
。jshortArray
: 短整型数组类型,对应Java中的short[]
。jintArray
: 整型数组类型,对应Java中的int[]
。jlongArray
: 长整型数组类型,对应Java中的long[]
。jfloatArray
: 单精度浮点型数组类型,对应Java中的float[]
。jdoubleArray
: 双精度浮点型数组类型,对应Java中的double[]
。其他类型:
jthrowable
: 异常类型,用于抛出Java异常。在JNI中,这些数据类型用于声明Native方法的参数和返回值类型。
1.文件命名:
使用ndk-build或CMake工具构建生成的文件通常是.so,命名规范如下:
lib+库名.so,例如:libXXX.so XXX代表具体库的名称
2.函数命名:
在JNI中,函数命名遵循特定的规则,以确保Java代码和本地代码之间的正确映射和交互。JNI函数命名的约定基于以下形式:
Java_package_ClassName_MethodName
Java
:前缀指示这是一个JNI函数。package
:表示Java类所在的包名,使用下划线代替点号。ClassName
:表示Java类的名称。MethodName
:表示Java方法的名称。对于JNI函数命名规则,还有一些需要注意的细节:
_
用于分隔不同的元素,以表示层次结构和命名空间关系。_
。以下是一些示例,展示了符合JNI函数命名规则的命名方式:
//Java 层代码JNIDemo.java
public class JNIDemo {
static {
System.loadLibrary("libjni");
}
public native String showLog();
}
//Native层代码 jnidemo.cpp
extern "C"
JNIEXPORT jstring JNICALL Java_com_example_jni_JNIDemo_showLog(JNIEnv* env, jobject job) {
return env->NewStringUTF("hello world");
}
上述示例中,我们假设有一个名为 com.example.jni.JNIDemo
的Java类,其中包含一个名为 showLog的方法,Native层的函数Java_com_example_jni_JNIDemo_showLog是跟java层的包名+类名+方法名一一对应的,这样才能正确映射,命名不正确对应时会导致编译时出错。
3.extern “C” : 表示 C 语言 和 C++ 的兼容。
4.JNIEXPORT 与 JNICALL 是 JNI 中定义的用于标识函数用途的两个宏,在不同系统环境这两个宏定义不一样。在Android环境下定义如下
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
JNIEXPORT 用来表示该函数是否可导出(函数的可见性)。在普通的C语言里,如果想将函数或者变量使用范围限制在当前文件,需要对其添加static修饰。但如果希望将其暴露给共享库的指定文件,需要通过符号隐藏显示来控制。
GCC4.0以后提供了符号可见性选项-fvisibility=vis,vis可以是默认值default、或者hidden表示隐藏。
对应的代码可见性属性为__attribute__((visibility("default")))或者__attribute__((visibility("hidden")))
为了简化符号输出形式,可以通过EXPORT简化其写法。如下:
#define EXPORT __attribute__((visibility("default")))
EXPORT int Func();
所以JNIEXPORT可以认为是#define JNIEXPORT __attribute__((visibility("default"))) 这样来的,当然具体实现可能复杂些,要判断不同编译器等等
JNIEXPORT和JNICALL在Linux环境下是空定义,所以在 Linux 下 JNI 函数声明可以省略这两个宏。
5.jstring:函数的返回值类型。
6.JNIEnv*:是所有 native 函数的第一个参数,指向 JVM 函数表的指针,函数表中的每一个入口指向一个 JNI 函数,每个函数用于访问 JVM 中特定的数据结构。
7.jobject: 代表了定义native函数的Java类 或 Java类的实例:
配置环境:参考安装及配置 NDK 和 CMake
配置Android.mk、Application.mk或者CMakeLists.txt。
编写Native代码:用C或C++编写Native代码,这些代码将实现你想要在Java中调用的功能。
创建JNI接口文件:创建一个与Native代码对应的JNI接口文件,此文件是.h格式的头文件
,描述了Java代码与Native代码之间的交互。
实现JNI方法:在JNI接口文件中定义要调用的本地方法。你需要实现这些方法,并将其与本地代码连接起来。
生成Native库:使用本地开发工具(如GCC、Clang等)将Native代码编译成共享库(例如.so
文件)。
在Java中加载本地库:在Java代码中使用System.loadLibrary("your-library-name")
方法加载生成的本地库。
调用Native方法:在Java代码中声明Native方法,并通过JNI接口调用Native代码
具体实操示例我将在《安卓JNI从0到1入门教程(二)》中介绍