安卓JNI从0到1入门教程(一)

网上看了一些JNI的入门教程,对新手来说很不友好,容易看的人一脸懵,决定自己写一个从0到1的入门教程。

关于JNI,谷歌官方也提供了入门教程,详情请查看:NDK入门教程

一、简介

JNI(Java Native Interface)是Java编程语言提供的一种编程框架和技术,用于在Java应用程序中调用Native代码(通常是用C/C++编写的)以实现底层功能和与操作系统、硬件交互。JNI允许开发者编写用C/C++等本地语言编写的代码,然后通过JNI接口与Java代码进行交互。例如常见的音视频处理,图像处理,地图等等都会用到JNI。

二、NDK与CMake

原生开发套件 (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。

三、Android.mk、Application.mk与CMakeLists.txt

CMakeLists.txtAndroid.mk都是用于构建和管理安卓应用中的本地代码的构建脚本文件。

CMakeLists.txt: 是CMake的配置文件,用于描述项目的构建过程和所需的构建设置。在安卓项目中,CMakeLists.txt文件通常位于app模块的根目录下。

CMakeLists.txt文件可以包含以下内容:

  • 定义构建的最低CMake版本。
  • 声明要构建的本地库。
  • 指定源代码文件。
  • 配置编译选项和链接库。
  • 定义生成的共享库的名称和属性。
  • 配置构建输出路径等。

使用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数据类型:

  1. 基本数据类型:

    • jboolean: 布尔类型,对应Java中的boolean
    • jbyte: 字节类型,对应Java中的byte
    • jchar: 字符类型,对应Java中的char
    • jshort: 短整型,对应Java中的short
    • jint: 整型,对应Java中的int
    • jlong: 长整型,对应Java中的long
    • jfloat: 单精度浮点型,对应Java中的float
    • jdouble: 双精度浮点型,对应Java中的double
  2. 引用类型:

    • 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[]
  3. 其他类型:

    • 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函数命名中的下划线 _ 用于分隔不同的元素,以表示层次结构和命名空间关系。
  • 如果JNI函数是静态方法,则在方法名之前添加下划线 _
  • 对于JNI函数,返回类型和参数类型的签名应与Java方法的签名匹配。签名用特定字符表示不同的类型。

以下是一些示例,展示了符合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.JNIEXPORTJNICALL 是 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类的实例:

  • 如果native函数是static,则代表类Class对象
  • 如果native函数非static,则代表类的实例对象

六、开发步骤

  1. 配置环境:参考安装及配置 NDK 和 CMake

  2. 配置Android.mk、Application.mk或者CMakeLists.txt。

  3. 编写Native代码:用C或C++编写Native代码,这些代码将实现你想要在Java中调用的功能。

  4. 创建JNI接口文件:创建一个与Native代码对应的JNI接口文件,此文件是.h格式的头文件,描述了Java代码与Native代码之间的交互。

  5. 实现JNI方法:在JNI接口文件中定义要调用的本地方法。你需要实现这些方法,并将其与本地代码连接起来。

  6. 生成Native库:使用本地开发工具(如GCC、Clang等)将Native代码编译成共享库(例如.so文件)。

  7. 在Java中加载本地库:在Java代码中使用System.loadLibrary("your-library-name")方法加载生成的本地库。

  8. 调用Native方法:在Java代码中声明Native方法,并通过JNI接口调用Native代码

具体实操示例我将在《安卓JNI从0到1入门教程(二)》中介绍

你可能感兴趣的:(JNI,安卓进阶,android,JNI,NDK)