NDK介绍
1.app为什么会把代码放到so中
c语言历史悠久,有很多现成的代码可用
c语言代码执行效率比java高
java代码很容易被反编译,而且反编译以后的逻辑很清晰
2.为什么要学习NDK开发
在安卓的so开发中,其他基本与c/c++开发一致,而与java交互需要用到ini
在本部分的DNK开发讲解中,主要就是介绍jni相关内容
so中会接触到的:系统库函数,jni调用,加密算法,魔改算法,系统调用,自定义算法
3.什么是JNI
jni是java Native intereface的缩写,从java1.1开始,jni标准成为java平台的一部分,允许Java代码和其他语言写的代码进行交互
JNI(Java Native Interface)是一个编程框架,允许 Java 代码与其他编程语言(通常是 C 或 C++)编写的本地代码进行交互。JNI 是 Java 平台的一部分,提供了一种机制,使得 Java 应用程序能够调用和被本地应用程序或库调用。
### JNI 的主要功能
1. **跨语言调用**:
- 允许 Java 程序调用用 C/C++ 编写的函数和库,反之亦然。这使得开发者可以利用已有的 C/C++ 代码库,或在性能要求高的场景中使用本地代码。
2. **性能优化**:
- 在需要高性能的计算密集型任务(如图形处理、音频处理和算法实现)时,使用本地代码可以显著提高性能。
3. **访问底层系统资源**:
- JNI 允许 Java 代码访问底层操作系统功能和硬件资源,这在某些情况下是 Java 本身无法直接实现的。
### JNI 的工作原理
1. **Java 代码调用本地方法**:
- 在 Java 中定义一个本地方法(native method),并使用 `native` 关键字进行声明。
- Java 虚拟机(JVM)负责加载本地库,并通过 JNI 调用相应的本地实现。
2. **本地代码实现**:
- 在 C/C++ 中实现与 Java 中定义的本地方法相对应的函数。JNI 提供了丰富的 API,允许本地代码访问 Java 对象、调用 Java 方法等。
3. **数据类型转换**:
- JNI 处理 Java 数据类型与 C/C++ 数据类型之间的转换,例如将 Java 字符串转换为 C 字符串,或将 Java 数组转换为 C 数组。
### 示例
以下是一个简单的 JNI 使用示例:
1. **Java 代码**:
```java
public class HelloJNI {
// 声明本地方法
public native String sayHello();
static {
// 加载本地库
System.loadLibrary("hello");
}
public static void main(String[] args) {
HelloJNI helloJNI = new HelloJNI();
System.out.println(helloJNI.sayHello());
}
}
- C/C++ 实现:
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h" // 生成的头文件
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "Hello from C!");
}
- 编译和运行:
- 使用
javac
编译 Java 代码,使用javah
生成头文件,使用 C 编译器编译 C 代码并生成共享库,最后运行 Java 程序。
- 使用
注意事项
- 性能:虽然 JNI 可以提高性能,但频繁的 Java 和本地代码之间的调用可能会导致性能下降,因此应谨慎使用。
- 平台依赖性:使用 JNI 可能会使应用程序变得平台依赖,因为本地代码需要针对特定操作系统和硬件架构进行编译。
- 错误处理:处理 JNI 代码时,需要特别注意内存管理和错误处理,以避免内存泄漏和程序崩溃。
总结
JNI 是 Java 平台中一个强大的功能,允许 Java 与本地代码进行交互,提供了跨语言调用的能力。它在性能优化和访问底层资源方面非常有用,但也带来了额外的复杂性和平台依赖性。开发者在使用 JNI 时需要权衡其优缺点,以确保应用程序的稳定性和性能。
# 4.什么是NDK
交叉编译工具链
DNK的配置
DNK,Cmake,LLDB的作用
```jsx
“NDK”(Native Development Kit)。
NDK 是 Android 开发中的一个重要工具包,允许开发者使用 C 和 C++ 等本地编程语言来编写 Android 应用程序的部分代码。
### NDK(Native Development Kit)
#### 定义
NDK 是 Android 提供的一个工具包,允许开发者使用 C 和 C++ 语言编写 Android 应用的本地代码。这对于需要高性能计算或需要访问底层硬件的应用程序(如游戏、图形处理、音频处理等)尤其有用。
#### 主要功能
- **性能优化**:通过使用 C/C++,开发者可以更好地控制内存和性能,适合计算密集型任务。
- **代码重用**:可以重用现有的 C/C++ 代码库,减少开发时间。
- **跨平台支持**:NDK 支持多种硬件架构(如 ARM、x86),使得应用可以在不同设备上运行。
#### 主要组件
- **工具链**:包括编译器、链接器等工具,用于构建本地代码。
- **JNI(Java Native Interface)**:允许 Java 代码与本地代码进行交互,使得两者可以相互调用。
- **示例和文档**:提供了丰富的示例代码和文档,帮助开发者快速上手。
#### 使用场景
- **游戏开发**:许多游戏引擎(如 Unity 和 Unreal Engine)使用 NDK 来提高性能。
- **图形和音频处理**:需要高效处理图形和音频数据的应用。
- **已有 C/C++ 代码库的集成**:将现有的 C/C++ 库集成到 Android 应用中。
### 总结
NDK 是一个强大的工具,适用于对性能有高要求的 Android 应用开发。它允许开发者利用 C/C++ 的优势,同时与 Java 代码进行无缝集成。如果你指的不是 NDK,请提供更多上下文,以便我更准确地回答你的问题。
5.ABI与指令集
ABI(应用二进制接口)和指令集是计算机体系结构和软件开发中的两个重要概念,它们在系统的运行和兼容性方面起着关键作用。以下是它们的定义、区别以及相互关系。
### 1. ABI(应用二进制接口)
**定义**:
ABI 是应用程序与操作系统之间的接口规范,定义了程序如何与操作系统进行交互。它包括了函数调用约定、数据类型的大小和对齐、系统调用的接口、库的使用规则等。
**主要组成部分**:
- **调用约定**:定义了函数参数如何传递(通过寄存器还是栈)、返回值如何处理等。
- **数据类型**:定义了基本数据类型(如整型、浮点型)的大小、对齐方式等。
- **系统调用接口**:定义了应用程序如何请求操作系统服务(如文件操作、网络通信等)。
- **二进制格式**:定义了可执行文件和库的格式(如 ELF、PE 等)。
**重要性**:
- 确保不同编译器生成的代码能够互操作。
- 使得程序可以在不同版本的操作系统上运行,只要 ABI 保持不变。
### 2. 指令集
**定义**:
指令集是处理器的基础,定义了处理器可以执行的所有指令的集合。每条指令指定了处理器如何执行特定的操作,如算术运算、数据传输、控制流等。
**主要组成部分**:
- **指令格式**:定义了每条指令的二进制编码格式。
- **操作码**:指示指令的类型(如加法、减法、跳转等)。
- **操作数**:指令所需的输入数据,可以是寄存器、内存地址或立即数。
**重要性**:
- 决定了处理器的功能和性能。
- 不同的处理器架构(如 x86、ARM、RISC-V 等)有不同的指令集。
### 3. ABI 与指令集的关系
- **依赖关系**:ABI 通常是建立在特定指令集之上的。ABI 定义了如何使用指令集来实现高层次的功能,如函数调用和数据管理。
- **兼容性**:如果一个新的处理器架构使用相同的指令集并保持 ABI 不变,那么用该 ABI 编写的程序可以在新的处理器上运行,而无需重新编译。
- **跨平台开发**:在跨平台开发中,开发者需要关注 ABI,以确保代码在不同平台上能够正确运行。指令集则影响代码的性能和效率。
### 4. 示例
- **ABI 示例**:
- 在 x86 架构上,常见的 ABI 如 System V ABI(用于 UNIX/Linux 系统)和 Microsoft x64 ABI(用于 Windows 系统)。它们定义了函数参数的传递方式、返回值的处理等。
- **指令集示例**:
- x86 指令集包括指令如 `ADD`(加法)、`SUB`(减法)、`MOV`(数据传输)等。
- ARM 指令集则包括 `ADD`、`SUB`、`LDR`(加载寄存器)等指令。
### 总结
ABI 和指令集在计算机体系结构和软件开发中扮演着重要角色。ABI 定义了应用程序与操作系统之间的接口,而指令集则定义了处理器可以执行的指令。理解它们之间的关系有助于开发高效、兼容的应用程序。
6.NDK与java的区别
NDK(Native Development Kit)和 Java 是 Android 开发中的两个不同层面,主要区别如下:
1. 编程语言
- NDK: 使用 C/C++ 语言进行开发,允许开发者编写本地代码。
- Java: 使用 Java 语言进行开发,Android 应用的主要编程语言。
2. 性能
- NDK: 本地代码通常比 Java 代码执行更快,适合性能要求高的应用,如游戏、图形处理和计算密集型任务。
- Java: 虽然 Java 代码的性能较低,但其运行时环境(JVM)提供了自动内存管理和垃圾回收,简化了开发过程。
3. 开发复杂性
- NDK: 开发过程相对复杂,需要处理内存管理、指针等底层细节,调试和错误处理也更为困难。
- Java: 开发相对简单,提供了丰富的库和框架,易于学习和使用。
4. 访问 Android API
- NDK: 通过 JNI(Java Native Interface)与 Java 代码交互,访问 Android API 需要额外的步骤。
- Java: 直接访问 Android API,使用更为方便。
5. 应用场景
- NDK: 适用于需要高性能计算、图形处理、音频处理等场景,如游戏引擎、图像处理库等。
- Java: 适用于大多数 Android 应用开发,尤其是业务逻辑、用户界面等。
6. 代码可移植性
- NDK: 由于使用 C/C++,需要针对不同平台编译不同的二进制文件,移植性较差。
- Java: Java 代码可以在任何支持 JVM 的平台上运行,具有较好的可移植性。
7. 生态系统
- NDK: 生态系统相对较小,主要依赖于 C/C++ 的库和工具。
- Java: 拥有丰富的库、框架和社区支持,开发资源丰富。
总结
NDK 和 Java 各有优缺点,选择使用哪种方式取决于应用的需求和开发者的熟悉程度。对于性能要求高的部分,可以使用 NDK,而大部分业务逻辑和用户界面则可以使用 Java。
7.第一个NDK工程
1.CMakelist介绍
CMakeLists.txt
是 CMake 构建系统的配置文件,用于定义项目的构建过程。CMake 是一个跨平台的构建工具,能够生成标准的构建文件(如 Makefile 或 Visual Studio 项目文件)。以下是对 CMakeLists.txt
的详细介绍,包括基本结构、常用命令和示例。
1. 基本结构
CMakeLists.txt
文件通常位于项目的根目录中。其基本结构如下:
cmake_minimum_required(VERSION <version>)
project(<project_name>)
# 设置编译选项
set(CMAKE_CXX_STANDARD 11)
# 添加源文件
add_executable(<target_name> <source_files>)
2. 常用命令
cmake_minimum_required(VERSION <version>)
: 指定所需的最低 CMake 版本。project(<project_name>)
: 定义项目的名称。set(<variable> <value>)
: 设置变量的值。add_executable(<target_name> <source_files>)
: 创建一个可执行文件目标。add_library(<target_name> <source_files>)
: 创建一个库目标(静态库或动态库)。target_link_libraries(<target> <libraries>)
: 指定目标链接的库。include_directories(<directories>)
: 添加头文件搜索路径。find_package(<package_name>)
: 查找并配置外部库。
3. 示例
以下是一个简单的 CMakeLists.txt
示例,展示如何构建一个包含多个源文件的 C++ 项目:
cmake_minimum_required(VERSION 3.10) # 指定 CMake 最低版本
project(MyProject) # 定义项目名称
set(CMAKE_CXX_STANDARD 11) # 设置 C++ 标准为 C++11
# 添加头文件搜索路径
include_directories(include)
# 添加源文件
set(SOURCES
src/main.cpp
src/other.cpp
)
# 创建可执行文件
add_executable(MyExecutable ${SOURCES})
# 链接外部库(例如 pthread)
find_package(Threads REQUIRED)
target_link_libraries(MyExecutable Threads::Threads)
4. 代码解析
cmake_minimum_required
: 确保使用的 CMake 版本不低于 3.10。project
: 定义项目名称为MyProject
。set(CMAKE_CXX_STANDARD 11)
: 设置 C++ 标准为 C++11。include_directories
: 添加include
目录作为头文件搜索路径。set(SOURCES ...)
: 定义源文件列表。add_executable
: 创建名为MyExecutable
的可执行文件,包含指定的源文件。find_package
: 查找并链接pthread
库。
5. 生成构建文件
要使用 CMake 生成构建文件,通常在项目根目录下执行以下命令:
mkdir build
cd build
cmake ..
make
这将创建一个 build
目录,生成构建文件并编译项目。
6. 总结
CMakeLists.txt
是 CMake 的核心配置文件,定义了项目的构建过程。通过使用 CMake,开发者可以轻松管理跨平台的构建过程,简化项目的构建和依赖管理。
2.so的加载
在 Android 和其他基于 Linux 的系统中,.so
文件(共享对象文件)是动态链接库的格式。它们可以在运行时被加载和使用,允许多个程序共享相同的库代码。
1. .so
文件的加载
在 Android 应用中,.so
文件通常通过 Java Native Interface (JNI) 加载。使用 System.loadLibrary()
方法可以在 Java 代码中加载 .so
文件。
1.1 使用 System.loadLibrary()
在 Java 中加载 .so
文件的基本方法如下:
public class NativeLib {
static {
System.loadLibrary("yourlib"); // 加载名为 libyourlib.so 的共享库
}
// 声明本地方法
public native void yourNativeMethod();
}
System.loadLibrary("yourlib")
: 这个调用会加载名为libyourlib.so
的共享库。注意,这里不需要加前缀lib
和后缀.so
。
2. 动态链接库的基本概念
2.1 导出表和导入表
- 导出表: 导出表包含了动态链接库中可供其他程序使用的函数和变量的列表。每个函数在导出表中都有一个名称和地址,使得其他程序可以通过这些信息调用该函数。
- 导入表: 导入表是一个程序在运行时需要调用的外部函数的列表。它包含了程序需要使用的动态链接库的名称和其中函数的名称。程序在加载时会根据导入表的信息查找相应的共享库。
3. CMakeLists.txt
在 Android 项目中,通常使用 CMake 构建 .so
文件。CMakeLists.txt
是 CMake 的配置文件,指定了如何构建项目。
3.1 示例 CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 设置库的名称
add_library(yourlib SHARED
yourlib.cpp)
# 查找 JNI 库
find_library(log-lib log)
# 链接库
target_link_libraries(yourlib
${log-lib})
add_library(yourlib SHARED yourlib.cpp)
: 创建一个名为yourlib
的共享库,源文件为yourlib.cpp
。find_library(log-lib log)
: 查找 Android 的日志库。target_link_libraries(yourlib ${log-lib})
: 将日志库链接到yourlib
。
4. 动态注册与静态注册
在 JNI 中,注册本地方法有两种方式:静态注册和动态注册。
4.1 静态注册
静态注册是在 Java 代码中直接声明本地方法,并在加载共享库时通过 JNIEXPORT 和 JNI_METHOD 定义方法的实现。通常在 .cpp
文件中实现。
JNIEXPORT void JNICALL Java_NativeLib_yourNativeMethod(JNIEnv *env, jobject obj) {
// 实现
}
4.2 动态注册
动态注册是在运行时使用 JNI 提供的函数注册本地方法。这种方式通常在 JNI_OnLoad
函数中进行。
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
// 注册本地方法
JNINativeMethod methods[] = {
{"yourNativeMethod", "()V", (void *)yourNativeMethod}
};
jclass clazz = env->FindClass("NativeLib");
if (clazz == nullptr) {
return -1;
}
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return -1;
}
return JNI_VERSION_1_6;
}
5. 总结
在 Android 和 Linux 中,.so
文件是动态链接库的格式,通过 JNI 可以在 Java 中加载和使用这些库。理解动态链接库的导出表和导入表、CMake 的使用,以及 JNI 的注册方式(静态和动态)对于开发高效的 Android 应用至关重要。
3.native函数的声明
在 Java 中,声明本地(native)函数的语法非常简单。
1. Native 函数的声明
在 Java 类中,使用 native
关键字来声明本地方法。以下是基本的语法:
public class YourClass {
// 声明一个本地方法
public native void yourNativeMethod();
}
2. 关键点
native
关键字: 表示该方法的实现是在本地代码(如 C/C++)中,而不是在 Java 中实现的。- 方法签名: 本地方法的签名可以包含参数和返回类型。例如:
public native int add(int a, int b);
3. 示例
以下是一个完整的示例,展示如何声明和实现一个 native 方法。
3.1 Java 代码
public class NativeLib {
// 声明本地方法
public native int add(int a, int b);
// 加载本地库
static {
System.loadLibrary("native-lib"); // 加载名为 libnative-lib.so 的共享库
}
public static void main(String[] args) {
NativeLib lib = new NativeLib();
int result = lib.add(5, 10);
System.out.println("Result: " + result);
}
}
3.2 C/C++ 代码
接下来,在 C/C++ 中实现这个本地方法:
#include <jni.h>
#include "NativeLib.h" // 生成的头文件
JNIEXPORT jint JNICALL Java_NativeLib_add(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b; // 返回两个整数的和
}
4. 生成头文件
在实现本地方法之前,需要生成 JNI 头文件。可以使用 javah
工具(在 JDK 8 及之前的版本)或使用 javac
的 -h
选项(在 JDK 8 及之后的版本):
javac -h . NativeLib.java
这将生成一个名为 NativeLib.h
的头文件,其中包含本地方法的 C/C++ 函数原型。
5. 总结
- 声明: 使用
native
关键字在 Java 中声明本地方法。 - 实现: 在 C/C++ 中实现这些方法,并确保方法签名与 Java 中的声明一致。
- 加载: 使用
System.loadLibrary()
加载包含本地方法实现的共享库。
通过这种方式,Java 应用程序可以利用本地代码的性能和功能。
4.JNI函数的静态注册规则
在 JNI 中,静态注册是指在 Java 类中直接声明本地方法,并在加载共享库时通过 JNIEXPORT 和 JNI_METHOD 定义方法的实现。
1. 静态注册的基本规则
- 方法声明: 在 Java 类中声明本地方法,使用
native
关键字。 - JNIEXPORT 和 JNICALL: 在 C/C++ 中实现这些方法时,必须使用
JNIEXPORT
和JNICALL
宏来定义方法的导出。 - 方法签名: 方法的名称和参数类型必须与 Java 中的声明严格匹配,包括返回类型和参数的顺序。
- 命名约定: 本地方法的 C/C++ 实现名称必须按照特定的命名约定进行命名,格式为
Java_包名_类名_方法名
。
2. 静态注册的步骤
2.1 Java 代码
首先,在 Java 中声明本地方法。例如,创建一个名为 NativeLib
的类:
public class NativeLib {
// 声明本地方法
public native void nativeMethod();
// 加载本地库
static {
System.loadLibrary("native-lib");
}
}
2.2 C/C++ 代码
接下来,在 C/C++ 中实现这个本地方法。假设类的包名为 com.example
,类名为 NativeLib
,方法名为 nativeMethod
,那么 C/C++ 中的实现如下:
#include <jni.h>
#include "com_example_NativeLib.h" // 生成的头文件
JNIEXPORT void JNICALL Java_com_example_NativeLib_nativeMethod(JNIEnv *env, jobject obj) {
// 实现本地方法
printf("Hello from native method!\\n");
}
3. 生成头文件
在实现本地方法之前,需要生成 JNI 头文件。使用以下命令:
javac -h . NativeLib.java
这将生成一个名为 com_example_NativeLib.h
的头文件,其中包含本地方法的 C/C++ 函数原型。
4. 编译和链接
编译 C/C++ 代码并生成共享库。例如,使用 GCC 编译:
gcc -shared -o libnative-lib.so -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" com_example_NativeLib.c
5. 注意事项
- JNIEXPORT: 确保使用
JNIEXPORT
,这使得方法可以被 JNI 识别。 - JNICALL: 确保使用
JNICALL
,这是调用约定,确保方法的参数传递方式正确。 - 方法签名: 方法的名称和参数必须完全匹配,包括大小写。
6. 总结
静态注册是 JNI 中一种简单直接的方法,适用于本地方法的实现。通过遵循上述规则和步骤,可以确保本地方法能够正确地与 Java 代码进行交互。
5.JNIEnv,jobject/jclass
在 Java Native Interface (JNI) 中,JNIEnv
、jobject
和 jclass
是与 Java 虚拟机(JVM)交互时使用的重要数据类型。下面是对它们的详细解释:
JNIEnv
-
定义:
JNIEnv
是一个指向结构体的指针,该结构体包含了一组函数指针,这些函数用于与 JVM 进行交互。 -
作用:它提供了访问 JNI 函数的上下文,包括调用 Java 方法、访问 Java 字段、创建 Java 对象等。
-
使用:在 JNI 函数中,
JNIEnv
是第一个参数,通常命名为env
。通过它,可以调用各种 JNI 函数。例如:jstring result = env->NewStringUTF("Hello from C++");
jobject
-
定义:
jobject
是一个代表 Java 对象的引用类型。它可以指向任何 Java 对象。 -
作用:在 JNI 方法中,
jobject
通常用来表示调用本地方法的 Java 对象实例。通过这个引用,可以访问该对象的字段和方法。 -
使用:在 JNI 函数中,
jobject
通常是第二个参数,通常命名为obj
。例如:jclass clazz = env->GetObjectClass(obj);
jclass
-
定义:
jclass
是一个代表 Java 类的引用类型,专门用于表示 Java 类的类型。 -
作用:在 JNI 中,
jclass
用于获取类的信息、访问类的静态方法和字段等。 -
使用:在 JNI 函数中,
jclass
通常是通过GetObjectClass
或FindClass
函数获得的。例如:jclass clazz = env->GetObjectClass(obj); jmethodID methodId = env->GetMethodID(clazz, "methodName", "()V");
示例
下面是一个简单的 JNI 示例,演示了如何使用 JNIEnv
、jobject
和 jclass
:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv *env, jobject obj) {
// 获取调用该方法的对象的类
jclass clazz = env->GetObjectClass(obj);
// 创建一个新的字符串
const char *message = "Hello from C++";
jstring result = env->NewStringUTF(message);
// 返回字符串
return result;
}
总结
JNIEnv
:提供与 JVM 交互的接口,允许调用 JNI 函数。jobject
:代表调用本地方法的 Java 对象实例,允许访问该对象的字段和方法。jclass
:代表 Java 类,允许访问类的静态方法和字段。
通过这些类型,JNI 能够实现 Java 和 C/C++ 之间的高效交互。
6.NewstringUTF
系统函数(使用频率很高)
NewStringUTF
是 Java Native Interface (JNI) 中的一个函数,用于创建一个新的 Java 字符串对象。它接受一个 C 风格的字符串(即以 null 结尾的字符数组)并将其转换为 Java 中的 String
对象。以下是关于 NewStringUTF
的详细说明:
函数原型
jstring NewStringUTF(JNIEnv *env, const char *utf);
参数
JNIEnv *env
:指向 JNI 环境的指针,提供了与 JVM 交互的接口。const char *utf
:一个指向以 null 结尾的 C 字符串的指针,通常是 UTF-8 编码的字符串。
返回值
jstring
:返回一个新的 Java 字符串对象。如果发生错误,可能返回NULL
。
使用场景
NewStringUTF
通常用于以下情况:
- 从 C/C++ 代码返回字符串到 Java:当需要将 C/C++ 中的字符串传递回 Java 层时,可以使用此函数。
- 创建字符串对象:在需要构造 Java 字符串的情况下,例如在 JNI 方法中返回一些文本信息。
示例
下面是一个简单的示例,演示如何在 JNI 中使用 NewStringUTF
:
#include <jni.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv *env, jobject obj) {
// C 风格的字符串
const char *message = "Hello from C++";
// 创建一个新的 Java 字符串对象
jstring result = env->NewStringUTF(message);
// 返回 Java 字符串
return result;
}
注意事项
- 内存管理:
NewStringUTF
创建的 Java 字符串对象是由 JVM 管理的,因此不需要手动释放。只需确保在不再需要该字符串时,JVM 会自动处理内存。 - 编码:确保传递给
NewStringUTF
的字符串是有效的 UTF-8 编码。如果字符串包含无效的 UTF-8 字符,可能会导致异常或未定义的行为。 - 性能:频繁调用
NewStringUTF
可能会影响性能,尤其是在高频率的 JNI 调用中,建议在可能的情况下减少字符串的创建。
总结
NewStringUTF
是一个重要的 JNI 函数,用于在 C/C++ 代码中创建 Java 字符串对象。通过使用这个函数,可以轻松地将 C/C++ 中的文本信息传递给 Java 层。
7.在DNK开发中,一定要注意那些是java的类型,那些是c/c++的类型,在适当的时候需要转换
在 JNI(Java Native Interface)开发中,确实需要注意 Java 和 C/C++ 之间的数据类型差异。不同的语言有不同的类型系统,因此在两者之间进行交互时,必须进行适当的转换。以下是一些常见的 Java 类型和对应的 C/C++ 类型,以及如何进行转换的示例。
常见的 Java 和 C/C++ 类型对照
Java 类型 | C/C++ 类型 | 说明 |
---|---|---|
int | jint | 32 位整数 |
long | jlong | 64 位整数 |
float | jfloat | 32 位浮点数 |
double | jdouble | 64 位浮点数 |
boolean | jboolean | 1 字节布尔值(0 或 1) |
char | jchar | 16 位 Unicode 字符 |
byte | jbyte | 8 位字节 |
short | jshort | 16 位整数 |
String | jstring | Java 字符串对象 |
Object | jobject | Java 对象引用 |
Class | jclass | Java 类引用 |
数组 | jobjectArray | Java 数组对象 |
示例:类型转换
- 整数类型转换
#include <jni.h>
extern "C" JNIEXPORT void JNICALL
Java_com_example_myapp_MainActivity_nativeMethod(JNIEnv *env, jobject obj, jint intValue) {
// 将 jint 转换为 C/C++ 的 int
int cIntValue = static_cast<int>(intValue);
// 进行一些操作
cIntValue += 10;
// 将结果转换回 jint
jint result = static_cast<jint>(cIntValue);
}
- 字符串类型转换
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv *env, jobject obj) {
// 创建一个 C++ 字符串
std::string cppString = "Hello from C++";
// 将 C++ 字符串转换为 Java 字符串
jstring javaString = env->NewStringUTF(cppString.c_str());
return javaString; // 返回 Java 字符串
}
- 数组类型转换
#include <jni.h>
extern "C" JNIEXPORT jintArray JNICALL
Java_com_example_myapp_MainActivity_getArray(JNIEnv *env, jobject obj) {
// 创建一个 C++ 数组
int cArray[] = {1, 2, 3, 4, 5};
jintArray javaArray = env->NewIntArray(5); // 创建 Java 整数数组
// 将 C++ 数组复制到 Java 数组中
env->SetIntArrayRegion(javaArray, 0, 5, cArray);
return javaArray; // 返回 Java 数组
}
注意事项
- 内存管理:所有通过 JNI 创建的 Java 对象(如字符串、数组等)都由 JVM 管理,因此不需要手动释放它们的内存。确保在不再需要时,JVM 会自动处理这些对象的内存。
- 编码:在处理字符串时,确保使用有效的 UTF-8 编码。
NewStringUTF
函数会将 C 字符串转换为 Java 字符串,但要确保输入是有效的 UTF-8 字符串。 - 错误处理:在 JNI 调用过程中,可能会发生错误(例如,找不到类、方法或字段)。可以使用
env->ExceptionCheck()
和env->ExceptionClear()
来检查和清除异常。 - 性能:频繁的类型转换和 JNI 调用可能会影响性能。尽量在本地代码中处理更多逻辑,减少 JNI 调用的频率。
总结
在 JNI 开发中,理解 Java 和 C/C++ 之间的数据类型差异非常重要。通过适当的转换,可以确保在两者之间顺利地传递数据。务必注意内存管理、编码和错误处理,以确保代码的健壮性和性能。
8.extern “C” JNIEXPORT jstring JNICALL
extern "C" JNIEXPORT jstring JNICALL
是在 Java Native Interface (JNI) 中定义本地方法的一种方式。下面是对这个声明的逐部分解释:
extern "C"
- 作用:这个关键字告诉编译器使用 C 语言的链接方式,而不是 C++ 的链接方式。C++ 会对函数名进行“名称修饰”(name mangling),使得函数名在编译后变得复杂,而 C 则不会。
- 目的:确保在 C++ 中定义的函数能够被 C 代码(如 Java 的 JNI)正确地调用。使用
extern "C"
可以避免名称修饰,从而使得 JNI 能够找到这些函数。
JNIEXPORT
- 作用:这是一个宏,通常在
jni.h
头文件中定义。它用于指定函数的导出属性,使得该函数能够被 JNI 调用。 - 目的:确保该函数在动态链接库(.so 文件或 DLL 文件)中是可见的,以便 Java 虚拟机(JVM)能够找到并调用它。
jstring
- 作用:这是 JNI 中定义的一种数据类型,表示一个 Java 字符串对象。在 JNI 中,
jstring
是一个表示 Java 字符串的指针类型。 - 目的:作为返回值类型,表示这个函数将返回一个 Java 字符串对象。
JNICALL
- 作用:这是一个宏,定义了调用约定。它确保函数按照 JNI 规定的方式进行调用。
- 目的:提供跨平台的兼容性,确保在不同的操作系统和编译器中,函数调用的行为是一致的。
总结
将这些部分结合起来,extern "C" JNIEXPORT jstring JNICALL
定义了一个 JNI 函数,该函数可以被 Java 调用,返回一个 Java 字符串,并且使用 C 的链接方式和 JNI 的调用约定。以下是一个示例函数的完整声明:
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv *env, jobject obj) {
return env->NewStringUTF("Hello from C++");
}
在这个示例中,Java_com_example_myapp_MainActivity_stringFromJNI
是 JNI 函数的名称,它遵循 JNI 的命名规则,表示这个函数是 MainActivity
类中的 stringFromJNI
方法。这个函数使用 JNIEnv
指针来与 Java 虚拟机交互,并返回一个新的 Java 字符串。
9.指定只编译arm64的so
要指定只编译 ARM64 架构的 .so
文件(共享库),你需要在 Android NDK 项目中配置 CMake 或 ndk-build 文件。以下是如何在这两种构建系统中进行配置的示例。
1. 使用 CMake
如果你使用 CMake 构建系统,可以在 CMakeLists.txt
文件中指定目标架构。
1.1 CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.4.1)
# 设置库的名称
add_library(yourlib SHARED yourlib.cpp)
# 设置目标架构为 ARM64
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DANDROID_ABI=arm64-v8a")
# 查找 JNI 库
find_library(log-lib log)
# 链接库
target_link_libraries(yourlib ${log-lib})
1.2 通过 gradle 配置
在 build.gradle
文件中,你可以指定 ABI 过滤器:
android {
...
defaultConfig {
...
ndk {
abiFilters "arm64-v8a" // 只编译 ARM64
}
}
}
2. 使用 ndk-build
如果你使用 ndk-build
,可以在 Android.mk
文件中指定目标架构。
2.1 Android.mk 示例
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := yourlib
LOCAL_SRC_FILES := yourlib.cpp
include $(BUILD_SHARED_LIBRARY)
# 指定只编译 ARM64
APP_ABI := arm64-v8a
2.2 Application.mk 示例
在 Application.mk
文件中,你可以指定 ABI:
APP_ABI := arm64-v8a
3. 编译
无论你使用哪种方法,确保在 Android Studio 中同步项目并进行构建。构建完成后,生成的 .so
文件将仅针对 ARM64 架构。
4. 总结
通过在 CMake 或 ndk-build 配置文件中指定 arm64-v8a
,你可以确保只编译 ARM64 架构的共享库。这对于优化应用程序的性能和减小 APK 大小非常有用。
10.指定编译后的so名字
要在Frida中主动调用特定的check
函数,并指定编译后的共享库(.so
文件)的名称,你可以使用Frida的Module
API来加载该库并调用其中的函数。以下是一个示例,说明如何实现这一目标。
Frida 脚本示例
假设你要调用的check
函数在名为libtarget.so
的共享库中。以下是Frida脚本的实现:
// 指定要加载的共享库名称
var targetLibrary = 'libtarget.so';
function main() {
// 等待目标库加载
Module.ensureInitialized(targetLibrary);
// 获取目标库中的check函数
var checkFunction = Module.findExportByName(targetLibrary, 'check');
if (checkFunction) {
console.log('check function found at: ' + checkFunction);
// 定义主动调用check函数的逻辑
setInterval(function() {
// 假设check函数接受三个参数,类型为int, float, string
var arg1 = 42; // 示例int参数
var arg2 = 3.14; // 示例float参数
var arg3 = "test_flag"; // 示例string参数
// 调用check函数
var result = Memory.callFunction(checkFunction, [arg1, arg2, arg3]);
console.log('check called with arguments: ' + arg1 + ', ' + arg2 + ', ' + arg3 + '. Result: ' + result);
}, 2000); // 每2秒调用一次
} else {
console.error('check function not found in ' + targetLibrary);
}
}
// 在Java环境中执行
Java.perform(main);
代码说明
- 指定共享库名称:
var targetLibrary = 'libtarget.so';
:指定要加载的共享库的名称。
- 确保库已加载:
Module.ensureInitialized(targetLibrary);
:确保目标库已加载到内存中。
- 查找函数地址:
var checkFunction = Module.findExportByName(targetLibrary, 'check');
:查找check
函数的地址。
- 主动调用函数:
- 使用
setInterval
设置定时器,每2秒调用一次check
函数。 Memory.callFunction(checkFunction, [arg1, arg2, arg3]);
:调用check
函数,传递示例参数。
- 使用
- 输出结果:
- 打印调用的参数和返回结果。
运行Frida脚本
使用以下命令运行Frida脚本:
frida -U -l hook_check.js -f com.ctf.mobile --no-pause
注意事项
- 确保
check
函数的参数类型和数量与实际实现匹配。在示例中,假设check
接受一个int
、一个float
和一个string
类型的参数。 - 根据需要调整定时器的时间间隔(例如,2000毫秒)。
- 监控控制台输出,以查看
check
函数的调用情况和返回结果。
8.so中常用的log输出
在使用 JNI(Java Native Interface)时,通常会在 C/C++ 代码中输出日志,以便于调试和监控。以下是一些常用的日志输出方法,特别是在 Android 开发中,常用的日志库是 Android 的 `Log` 类,但在 C/C++ 中,我们可以使用标准输出或其他日志库。
### 1. 使用 `printf`
最简单的方式是使用 `printf` 函数输出日志信息:
```c
#include <stdio.h>
JNIEXPORT void JNICALL Java_HelloJNI_logMessage(JNIEnv *env, jobject obj) {
printf("Hello from C! This is a log message.\n");
}
2. 使用 LOGI
, LOGE
等宏(在 Android NDK 中)
在 Android NDK 中,可以使用 __android_log_print
函数来输出日志。这个函数提供了不同的日志级别,如 LOGI
(信息)、LOGE
(错误)、LOGD
(调试)等。
首先,包含头文件:
#include <android/log.h>
然后,可以使用以下方式输出日志:
#define LOG_TAG "MyNativeCode"
JNIEXPORT void JNICALL Java_HelloJNI_logMessage(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hello from C! This is an info log.");
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "This is an error log.");
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "This is a debug log.");
}
3. 日志级别说明
ANDROID_LOG_VERBOSE
: 详细信息ANDROID_LOG_DEBUG
: 调试信息ANDROID_LOG_INFO
: 一般信息ANDROID_LOG_WARN
: 警告信息ANDROID_LOG_ERROR
: 错误信息ANDROID_LOG_FATAL
: 致命错误
4. 使用 C++ 的 std::cout
如果你在使用 C++,可以使用标准输出流:
#include <iostream>
extern "C" {
JNIEXPORT void JNICALL Java_HelloJNI_logMessage(JNIEnv *env, jobject obj) {
std::cout << "Hello from C++!" << std::endl;
}
}
5. 组合使用 JNI 和日志输出
在 JNI 方法中,可以结合 Java 的日志系统和本地日志输出。例如,可以在本地代码中捕获错误并将其通过 JNI 返回到 Java 层进行处理。
示例代码
以下是一个完整的示例,展示了如何在 JNI 中使用日志输出:
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "MyNativeCode"
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "sayHello called");
return (*env)->NewStringUTF(env, "Hello from C!");
}
JNIEXPORT void JNICALL Java_HelloJNI_logMessage(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Logging from native code.");
}
总结
在 JNI 开发中,使用适当的日志输出机制可以帮助你快速定位问题和调试代码。在 Android NDK 开发中,推荐使用 __android_log_print
函数,以便更好地与 Android 的日志系统集成。使用标准输出或其他日志库也是可行的,但在 Android 环境中,使用 Android 提供的日志功能通常是最有效的方式。
在 Android 开发中,通常会在 JNI(Java Native Interface)中使用 C/C++ 代码,并希望能够在 Java 层通过日志输出进行调试。以下是一个示例,展示了如何在 C/C++ 代码中使用日志输出,并在 Java 代码中调用这些方法。
### 1. Java 代码示例
首先,在 Java 代码中,我们定义一个类来加载本地库,并调用本地方法。我们将使用 `Log.d()` 来输出调试信息。
```java
import android.util.Log;
public class HelloJNI {
private static final String TAG = "HelloJNI";
static {
System.loadLibrary("hello"); // 加载本地库
}
// 声明本地方法
public native String sayHello();
public native void logMessage();
public void testLogging() {
Log.d(TAG, "Testing JNI logging.");
String message = sayHello();
Log.d(TAG, "Message from native code: " + message);
logMessage();
}
}
2. C/C++ 代码示例
在 C/C++ 代码中,我们使用 __android_log_print()
来输出日志信息。确保包含必要的头文件。
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "HelloJNI"
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "sayHello called from native code.");
return (*env)->NewStringUTF(env, "Hello from C!");
}
JNIEXPORT void JNICALL Java_HelloJNI_logMessage(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "logMessage called from native code.");
}
3. 使用示例
在你的 Android Activity 或其他地方调用 testLogging()
方法:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HelloJNI helloJNI = new HelloJNI();
helloJNI.testLogging(); // 调用测试日志的方法
}
}
4. 日志输出示例
当你运行应用程序并调用 testLogging()
方法时,你应该能够在 Logcat 中看到如下输出:
D/HelloJNI: Testing JNI logging.
D/HelloJNI: sayHello called from native code.
D/HelloJNI: logMessage called from native code.
D/HelloJNI: Message from native code: Hello from C!
总结
通过在 Java 代码中使用 Log.d()
和在 C/C++ 代码中使用 __android_log_print()
,你可以方便地输出调试信息。这对于调试 JNI 交互和确保本地代码正常工作非常有帮助。
9.NDK多线程
在 Android NDK 中实现多线程可以通过多种方式进行,最常见的方法是使用 POSIX 线程(pthread)。以下是一个关于如何在 NDK 中创建和管理多线程的基本示例。
1. 使用 POSIX 线程(pthread)
1.1. C/C++ 代码示例
首先,你需要在 C/C++ 代码中创建一个线程并进行简单的操作。以下是一个使用 pthread 库的示例:
#include <jni.h>
#include <pthread.h>
#include <android/log.h>
#include <unistd.h>
#define LOG_TAG "NDKExample"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
void* thread_function(void* arg) {
LOGI("Thread started.");
// 模拟一些工作
for (int i = 0; i < 5; i++) {
LOGI("Thread is running: %d", i);
sleep(1); // 暂停1秒
}
LOGI("Thread finished.");
return NULL;
}
JNIEXPORT void JNICALL Java_com_example_ndkexample_MainActivity_startThread(JNIEnv *env, jobject obj) {
pthread_t thread;
LOGI("Creating thread...");
// 创建线程
if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
LOGI("Error creating thread.");
}
// 等待线程结束
pthread_join(thread, NULL);
LOGI("Thread has been joined.");
}
1.2. Java 代码示例
在 Java 代码中,你需要声明本地方法并调用它。以下是 Java 类的示例:
package com.example.ndkexample;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
static {
System.loadLibrary("native-lib"); // 加载本地库
}
// 声明本地方法
public native void startThread();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "Starting thread...");
startThread(); // 调用本地方法以启动线程
}
}
2. 线程安全
在多线程环境中,确保数据的安全性是非常重要的。你可以使用互斥锁(mutex)来保护共享资源。以下是如何使用 pthread 的互斥锁的示例:
#include <pthread.h>
#include <android/log.h>
#define LOG_TAG "NDKExample"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
pthread_mutex_t lock;
void* thread_function(void* arg) {
LOGI("Thread started.");
// 访问共享资源
pthread_mutex_lock(&lock);
// 这里可以安全地访问共享资源
LOGI("Thread is modifying shared resource.");
pthread_mutex_unlock(&lock);
LOGI("Thread finished.");
return NULL;
}
JNIEXPORT void JNICALL Java_com_example_ndkexample_MainActivity_startThread(JNIEnv *env, jobject obj) {
pthread_t thread;
LOGI("Creating thread...");
// 初始化互斥锁
pthread_mutex_init(&lock, NULL);
// 创建线程
if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
LOGI("Error creating thread.");
}
// 等待线程结束
pthread_join(thread, NULL);
LOGI("Thread has been joined.");
// 销毁互斥锁
pthread_mutex_destroy(&lock);
}
3. 线程池(可选)
如果你需要更复杂的线程管理,可以考虑实现一个线程池。线程池可以帮助你重用线程,减少线程创建和销毁的开销。
4. 总结
在 Android NDK 中使用多线程可以通过 POSIX 线程(pthread)实现。确保在多线程环境中使用适当的同步机制(如互斥锁)来保护共享资源,从而避免数据竞争和不一致性问题。通过合理设计线程的生命周期和管理,可以提高应用程序的性能和响应能力。
10.pthread_create
是 POSIX 线程库中用于创建新线程的函数。它允许程序在运行时生成新的执行流,使得多线程编程成为可能。以下是对 pthread_create
的详细介绍,包括其用法、参数、返回值以及示例。
1. 函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
2. 参数说明
pthread_t *thread
: 指向pthread_t
类型的指针,用于存储新创建线程的 ID。这个 ID 可以用于后续的线程管理(如pthread_join
或pthread_cancel
)。const pthread_attr_t *attr
: 指向线程属性对象的指针。这个参数可以为NULL
,表示使用默认的线程属性。线程属性可以定义线程的栈大小、调度策略等。void *(*start_routine)(void *)
: 指向线程执行的函数的指针。这个函数必须接受一个void*
类型的参数,并返回一个void*
类型的结果。线程开始执行时会调用这个函数。void *arg
: 传递给start_routine
函数的参数。可以传递任何类型的数据,但通常需要将其转换为void*
进行传递。
3. 返回值
- 成功时: 返回 0。
- 失败时: 返回一个非零的错误代码,表示错误的类型。例如,常见的错误包括:
EAGAIN
: 系统资源不足,无法创建更多线程。EINVAL
: 传入的属性参数无效。EPERM
: 没有权限创建线程。
4. 示例代码
以下是一个使用 pthread_create
的简单示例,创建一个线程并执行一个函数:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define NUM_THREADS 5
void* thread_function(void* arg) {
int thread_id = *((int*)arg);
printf("Thread %d is running.\\n", thread_id);
sleep(1); // 模拟一些工作
printf("Thread %d is finished.\\n", thread_id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
// 创建多个线程
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
perror("Failed to create thread");
exit(EXIT_FAILURE);
}
}
// 等待所有线程结束
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("All threads have finished.\\n");
return 0;
}
5. 代码解析
- 线程函数:
thread_function
接受一个参数,打印线程 ID 并模拟工作。线程结束时返回NULL
。 - 创建线程: 在
main
函数中,使用pthread_create
创建多个线程,每个线程执行thread_function
。 - 等待线程: 使用
pthread_join
等待所有线程完成。这样可以确保主线程在所有子线程完成之前不会退出。
6. 线程属性(可选)
如果需要自定义线程的行为,可以使用 pthread_attr_t
设置线程属性。例如,设置线程栈大小:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 1024 * 1024); // 设置栈大小为1MB
pthread_create(&thread, &attr, thread_function, NULL);
pthread_attr_destroy(&attr); // 清理属性对象
7. 总结
pthread_create
是创建和管理线程的核心函数。通过合理使用这个函数,可以实现多线程应用程序,提高程序的并发性和性能。注意在多线程环境中要谨慎处理共享资源,以避免数据竞争和不一致性问题。
11.JNI_Load
在 Android NDK 中,JNI_OnLoad
是一个特殊的函数,用于在动态链接库(.so 文件)被加载时执行初始化操作。这个函数通常用于设置 JNI 环境、注册本地方法等。以下是一个详细的 JNI_OnLoad
示例,包括注释,帮助你理解每一部分的作用。
示例代码
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "JNIExample"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 声明本地方法
JNIEXPORT void JNICALL Java_com_example_ndkexample_MainActivity_nativeMethod(JNIEnv *env, jobject obj) {
LOGI("Native method called!");
}
//!!!!!!!!!!!!!!!!!!!!!!!
***// JNI_OnLoad 函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
// 获取 JNI 环境
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
LOGI("Failed to get JNI environment");
return -1; // 返回错误代码
}***
//!!!!!!!!!!!!!!!!!!!!!!!!!!
// 注册本地方法
jclass clazz = (*env)->FindClass(env, "com/example/ndkexample/MainActivity");
if (clazz == NULL) {
LOGI("Failed to find class");
return -1; // 返回错误代码
}
// 定义本地方法的结构
JNINativeMethod methods[] = {
{"nativeMethod", "()V", (void *)Java_com_example_ndkexample_MainActivity_nativeMethod}
};
// 注册本地方法
if ((*env)->RegisterNatives(env, clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
LOGI("Failed to register native methods");
return -1; // 返回错误代码
}
LOGI("JNI_OnLoad completed successfully");
return JNI_VERSION_1_6; // 返回 JNI 版本
}
代码解析
-
包含头文件:
#include <jni.h> #include <android/log.h>
jni.h
: 提供 JNI 的函数和数据类型定义。android/log.h
: 用于 Android 日志打印。
-
日志宏定义:
#define LOG_TAG "JNIExample" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
- 定义日志标签和日志打印宏,方便在代码中输出日志信息。
-
本地方法实现:
JNIEXPORT void JNICALL Java_com_example_ndkexample_MainActivity_nativeMethod(JNIEnv *env, jobject obj) { LOGI("Native method called!"); }
- 这是一个简单的本地方法实现,调用时会打印日志。
-
JNI_OnLoad 函数:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNI_OnLoad
是 JNI 的入口点,当库被加载时会自动调用。
-
获取 JNI 环境:
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { LOGI("Failed to get JNI environment"); return -1; // 返回错误代码 }
- 通过
GetEnv
获取 JNI 环境指针env
,并检查是否成功。
- 通过
-
查找 Java 类:
jclass clazz = (*env)->FindClass(env, "com/example/ndkexample/MainActivity"); if (clazz == NULL) { LOGI("Failed to find class"); return -1; // 返回错误代码 }
- 使用
FindClass
查找 Java 类MainActivity
,如果找不到则返回错误。
- 使用
-
定义本地方法数组:
JNINativeMethod methods[] = { {"nativeMethod", "()V", (void *)Java_com_example_ndkexample_MainActivity_nativeMethod} };
- 定义一个
JNINativeMethod
结构体数组,包含本地方法的名称、签名和实现。
- 定义一个
-
注册本地方法:
if ((*env)->RegisterNatives(env, clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) { LOGI("Failed to register native methods"); return -1; // 返回错误代码 }
- 使用
RegisterNatives
注册本地方法。如果注册失败,返回错误。
- 使用
-
完成初始化:
LOGI("JNI_OnLoad completed successfully"); return JNI_VERSION_1_6; // 返回 JNI 版本
- 打印成功信息并返回 JNI 版本。
结论
JNI_OnLoad
是 JNI 中一个重要的初始化函数,用于在动态库加载时执行必要的设置,如获取 JNI 环境、注册本地方法等。理解和正确实现这个函数是使用 JNI 进行 Android NDK 开发的基础。
12.JavaVM
JavaVM
是 Java Native Interface (JNI) 中的一个重要概念,它表示 Java 虚拟机的一个实例。通过 JavaVM
,本地代码可以与 Java 代码进行交互。下面是关于 JavaVM
的详细介绍和使用示例。
1. JavaVM 的定义
在 JNI 中,JavaVM
是一个指向 Java 虚拟机的指针,它允许本地代码访问 Java 的功能。通过 JavaVM
,你可以:
- 创建 Java 对象。
- 调用 Java 方法。
- 访问 Java 字段。
2. JavaVM 的结构
在 JNI 中,通常会使用一个 JavaVM
指针和一个 JNIEnv
指针。JNIEnv
是一个指向 JNI 环境的指针,提供了调用 JNI 函数的接口。每个线程可以有自己的 JNIEnv
指针,但通常只有一个 JavaVM
实例。
3. 使用 JavaVM 的示例
以下是一个简单的示例,展示如何在 C/C++ 中使用 JavaVM
来调用 Java 方法。
3.1 Java 代码
首先,我们定义一个简单的 Java 类,包含一个要调用的方法:
// HelloWorld.java
public class HelloWorld {
public void sayHello() {
System.out.println("Hello from Java!");
}
}
编译这个 Java 文件:
javac HelloWorld.java
3.2 C/C++ 代码
接下来,我们编写 C/C++ 代码,使用 JNI 加载 Java 类并调用 sayHello
方法:
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
JavaVM *jvm; // Java 虚拟机
JNIEnv *env; // JNI 环境
JavaVMInitArgs vm_args; // JVM 启动参数
JavaVMOption options[1]; // JVM 选项
// 设置 JVM 选项,指定类路径
options[0].optionString = "-Djava.class.path=."; // 当前目录
vm_args.version = JNI_VERSION_1_6; // JNI 版本
vm_args.nOptions = 1; // 选项数量
vm_args.options = options; // 选项数组
vm_args.ignoreUnrecognized = 0; // 忽略未识别的选项
// 创建 Java 虚拟机
jint ret = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (ret != JNI_OK) {
fprintf(stderr, "Failed to create JVM\\n");
exit(EXIT_FAILURE);
}
// 查找 HelloWorld 类
jclass cls = (*env)->FindClass(env, "HelloWorld");
if (cls == NULL) {
fprintf(stderr, "Failed to find HelloWorld class\\n");
(*jvm)->DestroyJavaVM(jvm); // 清理 JVM
exit(EXIT_FAILURE);
}
// 创建 HelloWorld 类的实例
jobject obj = (*env)->AllocObject(env, cls);
if (obj == NULL) {
fprintf(stderr, "Failed to create HelloWorld object\\n");
(*jvm)->DestroyJavaVM(jvm); // 清理 JVM
exit(EXIT_FAILURE);
}
// 查找 sayHello 方法
jmethodID mid = (*env)->GetMethodID(env, cls, "sayHello", "()V");
if (mid == NULL) {
fprintf(stderr, "Failed to find sayHello method\\n");
(*jvm)->DestroyJavaVM(jvm); // 清理 JVM
exit(EXIT_FAILURE);
}
// 调用 sayHello 方法
(*env)->CallVoidMethod(env, obj, mid);
// 销毁 Java 虚拟机
(*jvm)->DestroyJavaVM(jvm);
return 0;
}
4. 代码解释
- 创建 JVM: 使用
JNI_CreateJavaVM
创建 Java 虚拟机实例,并初始化JNIEnv
指针。传入的参数包括 JVM 选项和 JNI 版本。 - 查找类: 使用
FindClass
查找 Java 类HelloWorld
。如果找不到类,将打印错误信息并清理 JVM。 - 创建对象: 使用
AllocObject
创建HelloWorld
类的实例。如果创建失败,同样会打印错误信息并清理 JVM。 - 查找方法: 使用
GetMethodID
查找sayHello
方法的 ID。需要提供方法名和方法签名(()V
表示无参数且无返回值的方法)。 - 调用方法: 使用
CallVoidMethod
调用sayHello
方法。 - 清理: 最后,使用
DestroyJavaVM
销毁 Java 虚拟机,释放资源。
5. 编译和运行
要编译 C/C++ 代码,你需要链接 JNI 库。假设你将代码保存为 HelloWorld.c
,可以使用以下命令编译:
gcc -o HelloWorld HelloWorld.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -L"$JAVA_HOME/lib" -ljvm
然后运行生成的可执行文件:
./HelloWorld
你应该能看到输出:
Hello from Java!
6. 总结
通过 JavaVM
和 JNIEnv
,你可以在 C/C++ 代码中与 Java 进行交互。这个示例展示了如何创建 JVM、查找类和方法以及调用 Java 方法。使用 JNI 时,请确保正确管理资源,避免内存泄漏和其他问题。
GetEnv
是 Java Native Interface (JNI) 中的一个重要函数,用于获取当前线程的 JNIEnv
指针。这个指针提供了调用 JNI 函数的接口,使得本地代码可以与 Java 代码进行交互。以下是对 GetEnv
的详细介绍,包括其用法、参数、返回值以及示例。(主线程获取)
1. 函数原型
jint GetEnv(JavaVM *vm, JNIEnv **env, jint version);
2. 参数说明
JavaVM *vm
: 指向 Java 虚拟机的指针,通常是在创建 JVM 时获得的。JNIEnv **env
: 指向JNIEnv
指针的指针,用于存储当前线程的JNIEnv
指针。jint version
: 请求的 JNI 版本,通常使用JNI_VERSION_1_6
或其他版本常量。
3. 返回值
- 成功时: 返回
JNI_OK
,表示成功获取JNIEnv
指针。 - 失败时: 返回一个非零的错误代码,例如
JNI_EDETACHED
(表示当前线程未附加到 JVM)或JNI_EVERSION
(表示请求的版本不支持)。
4. 使用场景
GetEnv
通常在以下情况下使用:
- 当线程首次启动时,它可能没有
JNIEnv
指针。在这种情况下,你可以使用GetEnv
来获取它。 - 在多线程环境中,确保每个线程都有自己的
JNIEnv
指针。
5. 示例代码
以下是一个示例,展示如何使用 GetEnv
获取 JNIEnv
指针,并调用 Java 方法。
5.1 Java 代码
首先,定义一个简单的 Java 类:
// HelloWorld.java
public class HelloWorld {
public void sayHello() {
System.out.println("Hello from Java!");
}
}
编译这个 Java 文件:
javac HelloWorld.java
5.2 C/C++ 代码
接下来,编写 C/C++ 代码,使用 GetEnv
来获取 JNIEnv
指针并调用 sayHello
方法:
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
JavaVM *jvm; // 声明全局 Java 虚拟机指针
void* thread_function(void* arg) {
JNIEnv *env; // 声明 JNI 环境指针
jint ret;
// 获取当前线程的 JNIEnv 指针
ret = (*jvm)->GetEnv(jvm, (void**)&env, JNI_VERSION_1_6);
if (ret == JNI_EDETACHED) {
// 如果当前线程未附加到 JVM,则附加它
JavaVMAttachArgs attachArgs;
attachArgs.version = JNI_VERSION_1_6;
attachArgs.name = "Thread-1"; // 线程名称
attachArgs.group = NULL; // 线程组
ret = (*jvm)->AttachCurrentThread(jvm, (void**)&env, &attachArgs);
if (ret != JNI_OK) {
fprintf(stderr, "Failed to attach thread\\n");
return NULL;
}
} else if (ret != JNI_OK) {
fprintf(stderr, "Failed to get JNIEnv\\n");
return NULL;
}
// 查找 HelloWorld 类
jclass cls = (*env)->FindClass(env, "HelloWorld");
if (cls == NULL) {
fprintf(stderr, "Failed to find HelloWorld class\\n");
return NULL;
}
// 创建 HelloWorld 类的实例
jobject obj = (*env)->AllocObject(env, cls);
if (obj == NULL) {
fprintf(stderr, "Failed to create HelloWorld object\\n");
return NULL;
}
// 查找 sayHello 方法
jmethodID mid = (*env)->GetMethodID(env, cls, "sayHello", "()V");
if (mid == NULL) {
fprintf(stderr, "Failed to find sayHello method\\n");
return NULL;
}
// 调用 sayHello 方法
(*env)->CallVoidMethod(env, obj, mid);
// 如果是附加的线程,分离它
if (ret == JNI_EDETACHED) {
(*jvm)->DetachCurrentThread(jvm);
}
return NULL;
}
int main() {
JavaVMInitArgs vm_args; // JVM 启动参数
JavaVMOption options[1]; // JVM 选项
// 设置 JVM 选项,指定类路径
options[0].optionString = "-Djava.class.path=."; // 当前目录
vm_args.version = JNI_VERSION_1_6; // JNI 版本
vm_args.nOptions = 1; // 选项数量
vm_args.options = options; // 选项数组
vm_args.ignoreUnrecognized = 0; // 忽略未识别的选项
// 创建 Java 虚拟机
jint ret = JNI_CreateJavaVM(&jvm, (void**)&vm_args, &vm_args);
if (ret != JNI_OK) {
fprintf(stderr, "Failed to create JVM\\n");
exit(EXIT_FAILURE);
}
// 创建线程
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
// 等待线程结束
pthread_join(thread, NULL);
// 销毁 Java 虚拟机
(*jvm)->DestroyJavaVM(jvm);
return 0;
}
6. 代码解释
- 全局 JVM 指针: 在全局范围内声明
JavaVM *jvm
,以便在不同的线程中访问。 - 线程函数:
thread_function
中使用GetEnv
获取JNIEnv
指针。若当前线程未附加到 JVM,则使用AttachCurrentThread
附加线程。 - 查找类和方法: 使用
FindClass
和GetMethodID
查找 Java 类和方法。 - 调用方法: 使用
CallVoidMethod
调用sayHello
方法。 - 分离线程: 如果线程是附加的,使用
DetachCurrentThread
分离它。
7. 编译和运行
编译 C/C++ 代码时,确保链接 JNI 库。假设代码保存为 HelloWorldThread.c
,可以使用以下命令编译:
gcc -o HelloWorldThread HelloWorldThread.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -L"$JAVA_HOME/lib" -ljvm -lpthread
运行生成的可执行文件:
./HelloWorldThread
你应该能看到输出:
Hello from Java!
8. 总结
GetEnv
是 JNI 中获取当前线程的 JNIEnv
指针的关键函数。通过合理使用 GetEnv
,可以在多线程环境中安全地调用 Java 方法。确保在使用 JNI 时正确管理线程的附加和分离,以避免潜在的资源泄漏和错误。
AttachCurrentThread
在 Java Native Interface (JNI) 中,GetEnv
和 AttachCurrentThread
是两个用于获取和管理 JNI 环境的重要函数。以下是对这两个函数的详细介绍。
1. GetEnv
GetEnv
函数用于获取当前线程的 JNIEnv
指针。通过这个指针,本地代码可以访问 JNI 的功能,例如调用 Java 方法、访问字段等。
函数原型
jint GetEnv(JavaVM *vm, JNIEnv **env, jint version);
参数说明
JavaVM *vm
: 指向 Java 虚拟机的指针,通常在创建 JVM 时获得。JNIEnv **env
: 指向JNIEnv
指针的指针,用于接收当前线程的 JNI 环境指针。jint version
: 期望的 JNI 版本,通常使用JNI_VERSION_1_6
或其他相应的版本。
返回值
- 成功时: 返回
JNI_OK
。 - 失败时: 返回一个非零的错误代码。
使用示例
JNIEnv *env;
jint result = (*jvm)->GetEnv(jvm, (void**)&env, JNI_VERSION_1_6);
if (result != JNI_OK) {
// 处理错误
}
2. AttachCurrentThread
AttachCurrentThread
函数用于将当前线程附加到 Java 虚拟机,并获取其 JNIEnv
指针。通常在本地线程(不是由 JVM 创建的线程)需要调用 Java 方法时使用。(子线程获取)
函数原型
jint AttachCurrentThread(JavaVM *vm, JNIEnv **env, void *args);
参数说明
JavaVM *vm
: 指向 Java 虚拟机的指针。JNIEnv **env
: 指向JNIEnv
指针的指针,用于接收当前线程的 JNI 环境指针。void *args
: 可以传递给 Java 线程的参数,通常为NULL
。
返回值
- 成功时: 返回
JNI_OK
。 - 失败时: 返回一个非零的错误代码。
使用示例
JNIEnv *env;
jint result = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
if (result != JNI_OK) {
// 处理错误
}
3. 结合使用
在一个 JNI 应用程序中,通常会在创建 Java 虚拟机时获得 JavaVM
指针,然后在本地线程中使用 AttachCurrentThread
获取 JNIEnv
指针,最后可以使用 GetEnv
检查当前线程的环境。
示例代码
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
JavaVM *jvm; // 全局 JavaVM 指针
void* thread_function(void* arg) {
JNIEnv *env;
// 将当前线程附加到 JVM
jint result = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
if (result != JNI_OK) {
fprintf(stderr, "Failed to attach thread\\n");
return NULL;
}
// 现在可以使用 env 调用 Java 方法
// ...
// 完成后分离线程
(*jvm)->DetachCurrentThread(jvm);
return NULL;
}
int main() {
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString = "-Djava.class.path=."; // 设置类路径
vm_args.version = JNI_VERSION_1_6; // JNI 版本
vm_args.nOptions = 1; // 选项数量
vm_args.options = options; // 选项数组
vm_args.ignoreUnrecognized = 0; // 忽略未识别的选项
// 创建 JVM
jint ret = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (ret != JNI_OK) {
fprintf(stderr, "Failed to create JVM\\n");
exit(EXIT_FAILURE);
}
// 创建线程
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
pthread_join(thread, NULL); // 等待线程结束
// 销毁 JVM
(*jvm)->DestroyJavaVM(jvm);
return 0;
}
4. 总结
GetEnv
用于获取当前线程的JNIEnv
指针,适用于已经附加到 JVM 的线程。AttachCurrentThread
用于将本地线程附加到 JVM,并获取JNIEnv
指针,适用于未由 JVM 创建的线程。
这两个函数在 JNI 编程中非常重要,确保本地线程能够正确地与 Java 代码进行交互。
13.JavaVM的获取方式
(1).在 JNI 中,获取 JavaVM
的方式通常有两种:
通过 JNI 环境 (JNIEnv
) 获取和通过 JNI 初始化时的参数获取。以下是详细介绍。
1. 通过 JNI 环境获取 JavaVM
在 JNI 中,每个线程都有一个 JNIEnv
指针,但只有一个 JavaVM
实例。可以通过 JNIEnv
的 GetJavaVM
方法获取 JavaVM
指针。以下是示例代码:
#include <jni.h>
#include <stdio.h>
void someFunction(JNIEnv *env) {
JavaVM *jvm; // 声明 JavaVM 指针
// 获取 JavaVM 指针
if ((*env)->GetJavaVM(env, &jvm) != JNI_OK) {
fprintf(stderr, "Failed to get JavaVM\\n");
return;
}
// 现在可以使用 jvm 指针进行其他操作
// ...
}
2. 通过 JNI 初始化时的参数获取
在 JNI 初始化时,通常会通过 JNI_CreateJavaVM
函数创建 JVM,并在该函数的参数中获取 JavaVM
指针。示例代码如下:
#include <jni.h>
#include <stdio.h>
int main() {
JavaVM *jvm; // 声明 JavaVM 指针
JNIEnv *env; // 声明 JNI 环境指针
JavaVMInitArgs vm_args; // JVM 启动参数
JavaVMOption options[1]; // JVM 选项
// 设置 JVM 选项
options[0].optionString = "-Djava.class.path=."; // 设置类路径
vm_args.version = JNI_VERSION_1_6; // JNI 版本
vm_args.nOptions = 1; // 选项数量
vm_args.options = options; // 选项数组
vm_args.ignoreUnrecognized = 0; // 忽略未识别的选项
// 创建 Java 虚拟机
jint ret = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (ret != JNI_OK) {
fprintf(stderr, "Failed to create JVM\\n");
return -1;
}
// 现在可以使用 jvm 指针进行其他操作
// ...
// 销毁 Java 虚拟机
(*jvm)->DestroyJavaVM(jvm);
return 0;
}
3. 总结
- 通过
GetJavaVM
: 在已有的JNIEnv
中获取JavaVM
指针,适用于已经在 JNI 环境中的线程。 - 通过
JNI_CreateJavaVM
: 在创建 JVM 时获取JavaVM
指针,适用于程序启动时。
这两种方式都可以有效地获取 JavaVM
指针,以便在本地代码中与 Java 进行交互。
(2)JNI_OnLoad的第一个参数
JNI_OnLoad
是 JNI 中的一个特殊函数,它在共享库被加载时自动调用。这个函数的第一个参数是 JavaVM *vm
,用于获取 Java 虚拟机的指针。以下是对 JNI_OnLoad
和其第一个参数的详细介绍。
1. JNI_OnLoad 函数原型
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
2. 参数说明
JavaVM *vm
:- 这是指向 Java 虚拟机的指针。通过这个指针,您可以获取
JNIEnv
指针,并与 Java 进行交互。 - 该指针在 JNI 环境中是非常重要的,因为它允许本地代码访问 Java 的功能,如创建对象、调用方法等。
- 这是指向 Java 虚拟机的指针。通过这个指针,您可以获取
void *reserved
:- 这个参数保留供将来使用,通常可以忽略。
3. 使用示例
以下是一个简单的示例,展示如何在 JNI_OnLoad
中使用 JavaVM
指针:
#include <jni.h>
#include <stdio.h>
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
// 获取 JNIEnv 指针
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // 返回错误
}
// 在这里可以进行其他初始化操作
printf("JNI_OnLoad called, JavaVM pointer obtained.\\n");
return JNI_VERSION_1_6; // 返回支持的 JNI 版本
}
4. 代码解析
- 获取 JNIEnv: 使用
GetEnv
函数从JavaVM
获取JNIEnv
指针。这个指针可以用于后续的 JNI 调用。 - 返回值:
JNI_OnLoad
返回一个jint
类型的值,表示支持的 JNI 版本。如果返回JNI_ERR
,表示加载失败。
5. 总结
JNI_OnLoad
的第一个参数 JavaVM *vm
是获取 Java 虚拟机指针的关键。通过这个指针,您可以在本地代码中与 Java 进行交互,执行各种 JNI 操作。确保在 JNI_OnLoad
中正确处理 JNIEnv
的获取和其他初始化工作,以便在后续的 JNI 调用中顺利进行。
(3)JNI_OnUnload的第一个参数
在 JNI 中,JNI_OnLoad
和 JNI_OnUnload
是两个特殊的回调函数,用于在动态链接库(DLL)加载和卸载时执行初始化和清理操作。它们的第一个参数分别是 JavaVM *
和 JNIEnv *
。以下是这两个函数的详细介绍。
1. JNI_OnLoad
函数原型
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
参数说明
JavaVM *vm
: 这是指向 Java 虚拟机的指针。通过这个指针,你可以获取JNIEnv
指针,并可以在这个虚拟机实例中执行 JNI 操作。void *reserved
: 这个参数保留给未来的使用,通常在当前版本中不需要使用。
返回值
- 返回一个
jint
类型的值,表示 JNI 版本。通常返回JNI_VERSION_1_6
或其他支持的 JNI 版本。
作用
JNI_OnLoad
在库加载时被调用,通常用于执行一些初始化操作,如注册本地方法、创建全局引用等。
2. JNI_OnUnload
函数原型
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);
参数说明
JavaVM *vm
: 这是指向 Java 虚拟机的指针,允许你执行一些清理操作。void *reserved
: 同样,这个参数保留给未来的使用,通常在当前版本中不需要使用。
作用
JNI_OnUnload
在库卸载时被调用,通常用于释放资源,例如删除全局引用、关闭打开的文件等。
3. 示例代码
以下是一个简单的示例,展示如何实现 JNI_OnLoad
和 JNI_OnUnload
:
#include <jni.h>
#include <stdio.h>
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
printf("JNI_OnLoad called\\n");
// 可以在这里进行初始化操作
return JNI_VERSION_1_6; // 返回支持的 JNI 版本
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) {
printf("JNI_OnUnload called\\n");
// 在这里进行清理操作
}
4. 使用注意事项
- 线程安全: 如果在
JNI_OnLoad
中创建了全局引用,确保在JNI_OnUnload
中释放它们,以避免内存泄漏。 - 版本兼容性: 在
JNI_OnLoad
中返回的版本号应该是你所支持的 JNI 版本,以确保与 Java 虚拟机的兼容性。 - 全局状态管理: 如果需要在
JNI_OnLoad
中创建全局变量或状态,确保在JNI_OnUnload
中进行相应的清理。
5. 总结
JNI_OnLoad
和 JNI_OnUnload
提供了在 JNI 库加载和卸载时执行初始化和清理操作的机制。通过这两个函数,可以管理 JNI 资源,确保应用的稳定性和性能。
(4)指针获取的是否一致
在 JNI 中,获取 JavaVM
指针的方式有几种,主要包括通过 JNI 函数、通过 JNI_OnLoad
和 JNI_OnUnload
函数的参数等。下面我们详细介绍这些方式,并对比它们获取的 JavaVM
指针是否一致。
1. 获取 JavaVM 指针的方式
1.1 JNI_CreateJavaVM
使用 JNI_CreateJavaVM
函数创建 Java 虚拟机时,可以直接获取 JavaVM
指针。
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
// 初始化 vm_args 和选项
JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
1.2 JNI_GetCreatedJavaVMs
如果已经存在一个 Java 虚拟机,可以使用 JNI_GetCreatedJavaVMs
函数获取已经创建的 JavaVM
指针。
JavaVM *jvm;
jint numVMs;
JNI_GetCreatedJavaVMs(&jvm, 1, &numVMs);
1.3 在 JNI_OnLoad
中
当动态链接库被加载时,JNI_OnLoad
函数会被调用,传入的第一个参数是 JavaVM
指针。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
// 可以使用 jvm
return JNI_VERSION_1_6;
}
1.4 在 JNI_OnUnload
中
同样,JNI_OnUnload
函数的第一个参数也是 JavaVM
指针,表示卸载时的 Java 虚拟机实例。
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *jvm, void *reserved) {
// 可以使用 jvm
}
2. 获取的 JavaVM 指针是否一致
在不同的上下文中获取的 JavaVM
指针通常是一致的。以下是一些关键点:
JNI_CreateJavaVM
和JNI_GetCreatedJavaVMs
: 这两种方式获取的JavaVM
指针应该是相同的,前提是你是在同一个进程中。如果你在进程中创建了 JVM,后续调用JNI_GetCreatedJavaVMs
将返回相同的JavaVM
指针。JNI_OnLoad
和JNI_OnUnload
: 在这两个函数中接收到的JavaVM
指针也是相同的,且与通过JNI_CreateJavaVM
或JNI_GetCreatedJavaVMs
获取的指针一致。
3. 示例验证
以下是一个示例,验证不同方式获取的 JavaVM
指针是否一致:
#include <jni.h>
#include <stdio.h>
JavaVM *g_jvm; // 全局 JavaVM 指针
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
g_jvm = jvm; // 保存 JavaVM 指针
printf("JNI_OnLoad: %p\\n", (void*)jvm);
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *jvm, void *reserved) {
printf("JNI_OnUnload: %p\\n", (void*)jvm);
}
void check_jvm() {
JavaVM *jvm;
jint numVMs;
// 使用 JNI_GetCreatedJavaVMs 获取 JavaVM 指针
JNI_GetCreatedJavaVMs(&jvm, 1, &numVMs);
printf("GetCreatedJavaVMs: %p\\n", (void*)jvm);
printf("Global JVM: %p\\n", (void*)g_jvm);
}
int main() {
// 假设在这里创建 JVM
// 例如使用 JNI_CreateJavaVM
// 检查 JVM 指针
check_jvm();
return 0;
}
4. 总结
在 JNI 中,通过不同方式获取的 JavaVM
指针通常是一致的。无论是通过 JNI_CreateJavaVM
、JNI_GetCreatedJavaVMs
,还是在 JNI_OnLoad
和 JNI_OnUnload
中接收的参数,指向的是同一个 Java 虚拟机实例。这种一致性在多线程和多模块的应用中非常重要,确保了 JNI 的稳定性和可靠性。
14.代码C与C++的区别
在使用 JNI(Java Native Interface)时,无论是用 C 还是 C++ 编写代码,基本的 JNI 操作和 API 调用都是相同的,但在语言特性和代码结构上会有一些显著的区别。以下是 C 和 C++ 在 JNI 编程中的主要区别。
1. 代码结构
C 代码示例
在 C 中,代码通常是以过程为导向的,使用函数进行操作:
#include <jni.h>
#include <stdio.h>
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// 使用 vm 参数
return JNI_VERSION_1_6;
}
C++ 代码示例
在 C++ 中,代码可以使用类和对象的概念,结构更为灵活:
#include <jni.h>
#include <iostream>
class MyClass {
public:
static jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// 使用 vm 参数
return JNI_VERSION_1_6;
}
};
2. 名称修饰
在 C++ 中,函数的名称会被编译器进行名称修饰(name mangling),这意味着 C++ 的函数名与 C 的函数名不同。因此,JNI 函数的声明和实现需要使用 extern "C"
来避免名称修饰:
extern "C" {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
return JNI_VERSION_1_6;
}
}
3. 错误处理
C 和 C++ 在错误处理方面的风格有所不同。C 通常使用返回值来指示错误,而 C++ 可以使用异常处理机制:
C 错误处理示例
if (some_error_condition) {
return JNI_ERR; // 返回错误码
}
C++ 错误处理示例
try {
// 可能引发异常的代码
} catch (const std::exception &e) {
std::cerr << "Error: " << e.what() << std::endl;
return JNI_ERR;
}
4. 对象和内存管理
C++ 支持对象和类的概念,可以使用构造函数和析构函数来管理资源,而 C 则需要手动管理内存:
C++ 对象管理示例
class MyClass {
public:
MyClass() {
// 构造函数
}
~MyClass() {
// 析构函数
}
};
C 内存管理示例
typedef struct {
// 数据成员
} MyClass;
MyClass* createMyClass() {
MyClass* obj = malloc(sizeof(MyClass));
// 初始化
return obj;
}
void destroyMyClass(MyClass* obj) {
free(obj);
}
5. STL 和标准库
C++ 可以使用 STL(标准模板库)来简化数据结构和算法的使用,例如 std::vector
、std::string
等,而 C 则需要使用原生数组和字符指针:
C++ STL 示例
#include <vector>
#include <string>
std::vector<int> numbers = {1, 2, 3};
std::string message = "Hello, World!";
C 原生数组示例
int numbers[] = {1, 2, 3};
char message[] = "Hello, World!";
6. 总结
尽管 C 和 C++ 在 JNI 编程中可以执行相同的操作,但由于语言特性和编程范式的不同,C++ 提供了更多的灵活性和功能。在选择使用哪种语言时,应考虑项目的需求、团队的熟悉程度以及可维护性等因素。C++ 的面向对象特性使得处理复杂数据结构和逻辑时更加方便,而 C 则更为直接和简单。