定义
JNI(Java Native Interface)是Java平台提供的一种机制,用于实现Java代码与其他语言(如C/C++)的交互。它允许Java程序调用本地代码(Native Code),同时也支持本地代码调用Java方法。
作用
- 跨语言交互:Java与C/C++之间的双向调用。
- 异常处理:JNI提供了机制来处理Java和本地代码之间的异常。
- 内存管理:JNI允许本地代码与Java虚拟机(JVM)共享数据,并管理这些数据的生命周期。
- 性能优化:将性能敏感的代码用C/C++实现,提升执行效率。
- 硬件访问:通过本地代码直接操作硬件设备,如摄像头、传感器等37。
- 在Android系统中,JNI是连接Java层与Native层的桥梁,广泛应用于系统底层开发、性能优化和安全加固等领域
JNI数据类型
基本数据类型
Java 类型 | JNI类型 | C/C++类型 | 说明 |
---|---|---|---|
boolean | jboolean | unsigned char | 无符号 8位 |
byte | jbyte | signed char | 有符号8 位 |
char | jchar | unsigned short | 无符号 16 位 |
short | jshort | signed short | 有符号16 位 |
int | jint | signed int | 有符号32 位 |
long | jlong | signed long | 有符号64 位 |
float | jfloat | float | 32 位浮点型 |
double | jdouble | double | 64 浮点型 |
引用数据类型
Java 类型 | JNI 类型 | 描述 |
---|---|---|
java.lang.Object | jobject | 可以表示任何Java的对象,或则没有JNI对应的类型的Java对象(实例方法的强制参数) |
java.lang.String | jstring | Java的String字符串类型的对象 |
java.lang.Class | jclass | Java的Class类型对象(静态方法的强制参数) |
Object[] | jobjectArray | Java任何对象的数组 |
boolean[] | jbyteArray | Java byte类型的数组 |
char[] | jcharArray | Java char类型的数组 |
short[] | jshortArray | Java short类型的数组 |
int[] | jintArray | Java int类型的数组 |
long[] | jlongArray | Java long类型的数组 |
float[] | jfloatArray | Java float类型的数组 |
double[] | jdoubleArray | Java double类型的数组 |
java.lang.Throwable | jthrowable | Java的Throwable类型,表示异常的所有类型和子类 |
Java 类型 | JNI 类型 |
---|---|
void | void |
JNI开发步骤
编写Java代码
- 用native关键字修饰,表示本地。
java">public class HelloWorld {
public native String get();
public native void set(String str);
}
生成头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnitest_HelloWorld */
//防止头文件被重复包含
#ifndef _Included_com_example_jnitest_HelloWorld
#define _Included_com_example_jnitest_HelloWorld
//如果代码被C++编译器编译,extern "C" 会告诉编译器以C语言的方式处理函数名(即不进行名称修饰),以确保C++编译器生成的函数符号与Java虚拟机期望的符号一致
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnitest_HelloWorld
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jnitest_HelloWorld_get
(JNIEnv *, jobject);
/*
* Class: com_example_jnitest_HelloWorld
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_jnitest_HelloWorld_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
头文件几种参数说明
Java方法在JNI中体现
- Java的方法,如get()就会变成Java_包名_类名_方法名()。
- set()变成Java_包名_类名_方法名(参数)
JNIEnv
- JNIEnv是一个指向JNI环境的指针,提供了访问JVM功能的方法。
- 通过JNIEnv,本地代码可以调用Java方法、访问Java字段、抛出异常等。
jobject
- jobject是Java对象的本地表示。在本地代码中,jobject用于引用Java对象。
JNICALL
- JNICALL 是一个宏,用于指定本地方法的调用约定(Calling Convention)。调用约定定义了函数参数的传递方式、栈的清理方式等。
- JNICALL 的定义通常位于 jni.h 头文件中,具体定义取决于操作系统和编译器。
JNIEXPORT
- JNIEXPORT 是一个宏,用于指定本地方法的导出方式。它的作用是告诉编译器将该函数导出为动态链接库(如 .so 或 .dll 文件)中的符号,以便Java虚拟机能够找到并调用它。
- JNIEXPORT 的定义也位于 jni.h 头文件中,具体定义取决于操作系统和编译器。
JNICALL、JNIEXPORT 作用
-
跨平台兼容性:不同的操作系统和编译器对函数导出和调用约定的处理方式不同。JNIEXPORT 和 JNICALL 通过宏定义屏蔽了这些差异,确保代码在不同平台上都能正常工作。
-
动态链接库的符号导出:在Windows上,必须显式导出函数符号,否则Java虚拟机无法找到本地方法。JNIEXPORT 解决了这个问题。
-
调用约定的一致性:调用约定影响函数参数的传递方式和栈的清理方式。JNICALL 确保本地方法的调用约定与JVM的要求一致。
实现本地方法
创建cpp目录
在主工程目录下创建cpp目录,将.h头文件放入该目录。
编写C/C++代码实现本地方法
创建c++或者c文件,如HelloWorld.cpp,实现本地方法。
#include "com_example_jnitest_HelloWorld.h"
#include <jni.h>
#include <string>
#include <stdio.h>
#include <iostream>
JNIEXPORT jstring JNICALL Java_com_example_jnitest_HelloWorld_get(JNIEnv *env , jobject thisz){
printf("get HelloWorld");
return env->NewStringUTF("HelloWorld");
};
JNIEXPORT jint JNICALL Java_com_example_jnitest_HelloWorld_add(JNIEnv *env , jobject thisz, jint a,jint b){
printf("add ");
return a + b;
};
编译so库
以下基于Android Studio工具编译so库。
NDK和CMake下载
在SDK Tools 选项卡中选择SDK Manager,勾选NDK和CMake选项。
CMake方式编译
创建CMakeLists.txt
CMakeLists.txt 是 CMake 的配置文件,用于定义项目的构建规则。在 Android NDK 开发中,CMakeLists.txt 用于编译 C/C++ 代码并生成共享库(.so 文件)。以下是 CMakeLists.txt 的常用语法和配置详解。
- 基本结构
一个典型的 CMakeLists.txt 文件包含以下部分:- 最低 CMake 版本:指定所需的 CMake 最低版本。
- 项目名称:定义项目名称。
- 添加库:定义要编译的库(静态库或动态库)。
- 查找依赖库:查找系统或第三方库。
- 链接库:将目标库与依赖库链接。
cmake_minimum_required(VERSION 3.4.1) # 最低 CMake 版本
project("myproject") # 项目名称
# 添加库
add_library(
native-lib # 库名称
SHARED # 库类型:SHARED 表示动态库,STATIC 表示静态库
native-lib.cpp # 源文件
)
# 查找系统库
find_library(
log-lib # 变量名称
log # 库名称
)
# 链接库
target_link_libraries(
native-lib # 目标库
${log-lib} # 链接的系统库
)
- 常用指令详解
命令 | 说明 |
---|---|
cmake_minimum_required | 指定项目所需的最低 CMake 版本 |
cmake_minimum_required(VERSION 3.4.1)
命令 | 说明 |
---|---|
project | 定义项目名称 |
project("myproject")
命令 | 说明 |
---|---|
add_library | 定义要编译的库 |
add_library(
native-lib # 库名称
SHARED # 库类型:SHARED(动态库)或 STATIC(静态库)
native-lib.cpp # 源文件
)
命令 | 说明 |
---|---|
find_library | 查找系统或第三方库 |
find_library(
log-lib # 变量名称
log # 库名称
)
命令 | 说明 |
---|---|
target_link_libraries | 将目标库与依赖库链接 |
target_link_libraries(
native-lib # 目标库
${log-lib} # 依赖库
)
命令 | 说明 |
---|---|
include_directories | 添加头文件搜索路径 |
include_directories(
include # 头文件目录
)
命令 | 说明 |
---|---|
add_definitions | 添加编译定义(宏定义) |
add_definitions(-DMY_MACRO=1)
命令 | 说明 |
---|---|
set | 设置变量 |
add_definitions(-DMY_MACRO=1)
- 高级配置
- 添加多个源文件
如果需要编译多个源文件,可以列出所有文件:
add_library(
native-lib
SHARED
native-lib.cpp
utils.cpp
math.cpp
)
- 添加预编译库
如果需要链接预编译的第三方库:
# 添加预编译库
add_library(
prebuilt-lib
SHARED
IMPORTED
)
# 设置预编译库的路径
set_target_properties(
prebuilt-lib
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libprebuilt.so
)
# 链接预编译库
target_link_libraries(
native-lib
prebuilt-lib
)
- 分模块编译
可以将代码分为多个模块,每个模块单独编译:
# 模块 1
add_library(
module1
SHARED
module1.cpp
)
# 模块 2
add_library(
module2
SHARED
module2.cpp
)
# 主模块
add_library(
native-lib
SHARED
main.cpp
)
# 链接模块
target_link_libraries(
native-lib
module1
module2
)
- 条件编译
根据平台或配置进行条件编译:
if(ANDROID)
add_definitions(-DANDROID)
endif()
编译CMakeLists.txt
- CMake源文件
cmake_minimum_required(VERSION 3.22.1)
#项目名称
project("helloworld")
# 定义库名称和源文件
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
HelloWorld.cpp)
# 查找系统库
find_library(
log-lib # 变量名称
log # 库名称
)
# 链接库
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
- 在 app/build.gradle 中,配置 CMake 路径
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "" # C++ 编译选项
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
- 编译和运行
重新编译项目会在app\build\intermediates\cxx\Debug\423t1085\obj生成so库
ndk-build方式编译
ndk-build 是基于 Android.mk 和 Application.mk 文件的构建系统
配置 NDK
确保已经安装 NDK:
- 打开 Android Studio。
- 进入 File > Settings > Appearance & Behavior > System Settings > Android SDK > SDK Tools。
- 勾选 NDK (Native Development Kit),然后点击 Apply 安装。
创建 JNI 目录
在项目的 app/src/main 目录下创建一个名为 jni 的文件夹,用于存放 C/C++ 代码和构建脚本。
app/
└── src/
└── main/
└── jni/
├── Android.mk
├── Application.mk
└── native-lib.cpp
编写 C/C++ 代码
在 jni/native-lib.cpp 中编写 C/C++ 代码。例如:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from NDK!";
return env->NewStringUTF(hello.c_str());
}
编写 Android.mk
Android.mk 是一个 Makefile 文件,用于定义如何编译 C/C++ 代码。
在 jni/Android.mk 中编写以下内容:
LOCAL_PATH := $(call my-dir)
# 清除变量
include $(CLEAR_VARS)
# 库名称
LOCAL_MODULE := native-lib
# 源文件
LOCAL_SRC_FILES := native-lib.cpp
# 链接系统库
LOCAL_LDLIBS := -llog
# 构建共享库
include $(BUILD_SHARED_LIBRARY)
编写 Application.mk
Application.mk 用于配置构建的目标 ABI 和其他全局设置。
在 jni/Application.mk 中编写以下内容:
# 目标 ABI
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
# 使用 STL
APP_STL := c++_shared
# 平台版本
APP_PLATFORM := android-21
配置 build.gradle
在 app/build.gradle 中配置 ndk-build:
android {
...
defaultConfig {
...
externalNativeBuild {
ndkBuild {
// 可选:指定 ABI
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
}
...
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk" // 指定 Android.mk 路径
}
}
}
编译项目
重新编译项目会生成so。
加载和使用 .so 文件
java">public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib"); // 加载库
}
// 声明本地方法
public native String stringFromJNI();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 调用本地方法
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
}
本地代码调用JVM
描述符
描述符(Descriptor)**是用来描述Java类、方法、字段的类型信息的字符串。描述符在JNI中非常重要,因为它们用于标识方法签名、字段类型和类名,以便本地代码能够正确地与Java代码交互。
字段描述符(Field Descriptors)
字段描述符用于描述Java字段的类型。它是一个字符串,表示字段的类型。
- 基本类型的字段描述符
Java 类型 | 字段描述符 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
- 引用类型的字段描述符
对于引用类型(如类、数组等),字段描述符的格式如下:- 类类型:L<全限定类名>;
例如:Ljava/lang/String; 表示 String 类型。
- 类类型:L<全限定类名>;
- 数组类型:[<元素类型描述符>
例如:[I 表示 int[] 类型。
[Ljava/lang/String; 表示 String[] 类型。
Java 类型 | 字段描述符 |
---|---|
int age | I |
String name | Ljava/lang/String; |
boolean[] flags | [Z |
Object obj | Ljava/lang/Object; |
int[][] matrix | [[I |
方法描述符(Method Descriptors)
方法描述符用于描述Java方法的签名,包括参数类型和返回值类型。它的格式为:
(参数类型描述符)返回值类型描述符
规则
- 参数类型描述符:按顺序列出所有参数的类型描述符。
- 返回值类型描述符:方法的返回类型描述符。如果方法返回 void,则使用 V。
Java 方法签名 | 方法描述符 |
---|---|
void print() | ()V |
int add(int a, int b) | (II)I |
String getName() | ()Ljava/lang/String; |
void setValues(int x, double y) | (ID)V |
boolean isEmpty(String str) | (Ljava/lang/String;)Z |
Object[] createArray(int size) | (I)[Ljava/lang/Object; |
类描述符(Class Descriptors)
类描述符用于描述Java类的全限定名。它的格式为:
L<全限定类名>;
规则
- 全限定类名:使用 / 代替 . 作为包分隔符。
- 必须以 L 开头,以 ; 结尾。
Java 类名 | 类描述符 |
---|---|
java.lang.String | Ljava/lang/String; |
java.util.List | Ljava/util/List; |
com.example.MyClass | Lcom/example/MyClass; |
开发步骤
编写Java代码
在Java代码中声明一个本地方法,并加载包含本地方法的库。
java">public class NativeExample {
// 加载本地库
static {
System.loadLibrary("NativeLibrary");
}
// 声明本地方法
public native void callJavaMethod();
// 定义一个Java方法,供本地代码调用
public void javaMethod() {
System.out.println("Java method called from native code!");
}
public static void main(String[] args) {
NativeExample example = new NativeExample();
example.callJavaMethod();
}
}
生成头文件
使用javac编译Java代码,并使用javah生成头文件。
java">javac NativeExample.java
javah -jni NativeExample
这将生成一个名为NativeExample.h的头文件,其中包含本地方法的声明。
实现本地方法
在C/C++中实现本地方法。首先,包含生成的头文件,并实现callJavaMethod方法。
#include <jni.h>
#include "NativeExample.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_NativeExample_callJavaMethod(JNIEnv *env, jobject obj) {
// 获取Java类的引用
jclass clazz = (*env)->GetObjectClass(env, obj);
// 获取Java方法的ID
jmethodID methodID = (*env)->GetMethodID(env, clazz, "javaMethod", "()V");
if (methodID == NULL) {
return; // 方法未找到
}
// 调用Java方法
(*env)->CallVoidMethod(env, obj, methodID);
}
编译本地代码
编译代码生成so库