JNI及使用流程

news/2025/2/24 6:01:55

定义

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++类型说明
booleanjbooleanunsigned char无符号 8位
bytejbytesigned char有符号8 位
charjcharunsigned short无符号 16 位
shortjshortsigned short有符号16 位
intjintsigned int有符号32 位
longjlongsigned long有符号64 位
floatjfloatfloat32 位浮点型
doublejdoubledouble64 浮点型

引用数据类型

Java 类型JNI 类型描述
java.lang.Objectjobject可以表示任何Java的对象,或则没有JNI对应的类型的Java对象(实例方法的强制参数)
java.lang.StringjstringJava的String字符串类型的对象
java.lang.ClassjclassJava的Class类型对象(静态方法的强制参数)
Object[]jobjectArrayJava任何对象的数组
boolean[]jbyteArrayJava byte类型的数组
char[]jcharArrayJava char类型的数组
short[]jshortArrayJava short类型的数组
int[]jintArrayJava int类型的数组
long[]jlongArrayJava long类型的数组
float[]jfloatArrayJava float类型的数组
double[]jdoubleArrayJava double类型的数组
java.lang.ThrowablejthrowableJava的Throwable类型,表示异常的所有类型和子类
Java 类型JNI 类型
voidvoid

JNI开发步骤

编写Java代码
  • 用native关键字修饰,表示本地。
java">public class HelloWorld {
    
    public native String get();
    public native void set(String str);
}
生成头文件
  • javac XXX.java编译生成.class文件。
  • javah或者javac -h . 源文件编译生成.h头文件。

在这里插入图片描述

/* 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 的常用语法和配置详解。

  1. 基本结构
    一个典型的 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}             # 链接的系统库
)
  1. 常用指令详解
命令说明
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)
  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:

  1. 打开 Android Studio。
  2. 进入 File > Settings > Appearance & Behavior > System Settings > Android SDK > SDK Tools。
  3. 勾选 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 类型字段描述符
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
  • 引用类型的字段描述符
    对于引用类型(如类、数组等),字段描述符的格式如下:
    • 类类型:L<全限定类名>;
      例如:Ljava/lang/String; 表示 String 类型。
  • 数组类型:[<元素类型描述符>
    例如:[I 表示 int[] 类型。
    [Ljava/lang/String; 表示 String[] 类型。
Java 类型字段描述符
int ageI
String nameLjava/lang/String;
boolean[] flags[Z
Object objLjava/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.StringLjava/lang/String;
java.util.ListLjava/util/List;
com.example.MyClassLcom/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库


http://www.niftyadmin.cn/n/5864011.html

相关文章

关于在mac中配置Java系统环境变量

引言 在 macOS 上开发 Java 或 Flutter 应用时&#xff0c;正确配置环境变量是至关重要的。环境变量不仅能让系统找到开发工具的位置&#xff0c;还能简化命令行操作。本文将手把手教你从零开始安装 Java SDK&#xff0c;并详细配置环境变量&#xff0c;涵盖常见问题解决和优化…

uniapp实现移动端剪切板小功能

在制作移动端支付的项目时用到了剪切板这个功能&#xff0c;uniapp中想要实现这个功能会有一个问题。 uni.setClipboardData(OBJECT) 这个方法是用来设置系统剪贴板的内容&#xff0c;这个方法里的object的参数data&#xff0c;是放我们需要设置的内容。当然一般可以在succes…

加密数据的模糊查询

密文检索的功能实现是根据4位英文字符&#xff08;半角&#xff09;&#xff0c;2个中文字符&#xff08;全角&#xff09;为一个检索条件。 将一个字段拆分为多个&#xff0c; 比如&#xff1a;taobao123 使用4个字符为一组的加密方式。 第一组 taob &#xff0c;第二组aoba &…

【C语言】第五期——函数

目录 0 前言 1 定义函数 2 调用函数 3 函数的实参和形参 4 函数声明 5 作用域 5.1 局部变量和全局变量 5.2 static关键字 5.2.1 修饰局部变量 5.2.2 修饰全局变量 5.2.3 修饰函数 6 函数的返回值 6.1 return语句 6.2 函数返回值的类型 7 函数的其他形式 7.1 函…

rust学习笔记5-所有权机制

rust核心就是所有权机制&#xff0c;是其内存管理的核心特性&#xff0c;旨在消除内存安全问题&#xff08;如空指针、悬垂指针、内存泄漏等&#xff09;而无需依赖垃圾回收&#xff08;GC&#xff09; 1.首先看一下语义模型 当声明一个变量 let a "32";它的语义模…

vue3中解决组件间 css 层级问题最佳实践(Teleport的使用)

定义&#xff1a; <Teleport> 是 Vue 3 中引入的一个内置组件&#xff0c;用于将组件的内容渲染到 DOM 中的指定位置&#xff0c;而不受组件层级结构的限制。这在处理模态框、通知、下拉菜单等需要脱离当前组件层级的情况下非常有用。 通俗来说&#xff0c;Teleport的功…

C/C++中的字符串

字符串表示方式&#xff1a; 在C语言中&#xff0c;字符串实际上是一个字符数组&#xff08;字节数组&#xff09;&#xff0c;并且以空字符 \0 结尾。例如&#xff0c;字符串 "RUNOOB" 实际上在内存中表示为 {R, U, N, O, O, B, \0}。这个 \0 终止符告诉标准库函数&…

[AI]【Comfyui】 生成基本流程图的步骤保姆记录

在进行深度学习模型或图像生成的过程中,创建流程图能够帮助清晰地表达模型的工作流程和数据流动。本文将为您介绍生成基本流程图的一般步骤,适用于常见的深度学习图像生成模型。以下是该流程图的基本步骤: 1. 创建 Load Checkpoint 节点 流程图的第一步通常是加载已经训练好…