简介:Java Native Interface (JNI) 允许Java代码与本地语言编写的代码进行交互,尤其在Android开发中,通过C++或C语言提高性能或访问硬件功能时发挥关键作用。尽管Android Studio 3.0及以上版本支持Gradle插件内置的JNI支持,使用javah工具编译JNI仍是许多开发者的首选。本教程将引导读者了解javah工具的使用方法,包括创建Java类、使用javah命令生成头文件、编写C/C++代码、配置CMakeLists.txt以及构建项目。最后,介绍如何在Java代码中调用本地方法,强调即使在新的开发环境中,理解javah的工作原理依然对开发者具有重要意义。
1. JNI简介与在Android开发中的作用
Java Native Interface(JNI)是Java开发工具包(JDK)的一部分,它为Java代码与本地应用程序或库之间提供了一种标准的编程接口。通过JNI,Java程序能够调用由C、C++或其它语言编写的函数,以及被这些语言编写的代码调用Java方法。JNI在Android开发中扮演着至关重要的角色,特别是在性能敏感或者需要使用特定平台功能的应用场景中。
1.1 JNI在Android开发中的角色
在Android平台上,性能和资源使用是开发中需要考虑的关键因素。虽然Java提供了丰富的标准库和良好的跨平台能力,但在某些场景下,为了获得最佳性能或访问特定的硬件或系统功能,开发者可能需要调用底层的本地代码。
使用JNI可以满足以下需求: - 性能优化:对于计算密集型任务,如图像处理、数学计算,使用C或C++可以利用更接近硬件的优化和运算能力。 - 系统级访问:访问Android系统的APIs,这些APIs可能不直接暴露给Java层,如蓝牙或NFC通信。 - 现有库集成:集成第三方库,这些库可能是用C或C++编写的,如图像处理库OpenCV。
通过JNI的引入,开发者能够根据应用的具体需求,灵活地在Java层和本地层之间进行代码交互,达到功能和性能的平衡。接下来的章节会详细介绍如何在Android开发中使用JNI,以及配合javah工具来生成头文件,并最终实现Java与本地代码的交互。
2. javah工具的角色和重要性
2.1 javah工具的原理与功能
2.1.1 javah工具的工作机制
javah是Java开发工具集(JDK)中的一个命令行工具,用于为声明了native方法的Java类自动生成C/C++头文件和源文件。这些头文件包含了可以被native方法实现所用的函数声明,它们遵循JNI(Java Native Interface)规范。JNI规范定义了Java与本地代码(即C/C++语言编写的代码)之间交互的标准方法。javah工具的核心作用是将Java的数据类型和方法签名转换为C/C++的数据类型和函数声明,从而实现Java代码与本地代码的链接。
当开发者在Java类中声明native方法并编译时,类文件并不包含这些方法的实现,只会留下方法签名和native关键字。此时,使用javah工具能够从Java类文件中解析出方法签名,并生成对应的C/C++头文件。在这些头文件中,每个native方法都对应一个特定的函数原型,这个原型可以被C/C++代码实现。
2.1.2 javah与JNI规范的关联
JNI规范是Java提供的一种编程接口,它允许Java代码和其他语言编写的应用程序进行互操作。JNI的主要功能包括访问Java对象和数组、调用Java方法、抛出和处理异常、以及加载本地库等。javah工具正是在这个接口的指导下工作的,它生成的头文件和C/C++源文件为JNI功能的实现提供了桥梁。
为了满足JNI规范中对数据类型和方法签名的严格要求,javah在生成的头文件中准确地映射了Java类型到C/C++类型。例如,Java中的int类型在JNI中对应于jint类型,而Java的double类型对应于jdouble类型。这样的一一映射确保了在Java和本地代码之间传递的数据能够正确地解释。
2.2 javah工具的使用场景
2.2.1 对应于不同编程语言的头文件生成
虽然javah主要用于生成C/C++头文件,但它生成的头文件也可以用于其他语言的本地代码实现,前提是这些语言能够与JNI兼容,并能正确处理JNI数据类型和方法签名。在实际开发中,开发者可以根据项目需求选择合适的语言来实现native方法,比如C、C++甚至是汇编语言。
2.2.2 如何解决常见问题及最佳实践
使用javah时,开发者经常会遇到一些问题,如生成的头文件中函数声明与期望的不符、包含错误的数据类型映射等。为了解决这些问题,首先需要深入理解JNI规范和javah工具的内部工作原理。另外,最佳实践包括将javah的使用集成到构建系统中,确保Java代码的任何更改都能触发对应的头文件更新。同时,开发者应该在编码阶段就遵循良好的JNI编程习惯,例如正确使用数据类型和异常处理,以及避免直接操作Java对象和数组,从而减少运行时错误和潜在的内存泄漏。
在本章节中,我们将详细了解javah工具的角色和重要性,并深入探讨其工作机制、应用场景以及如何解决常见问题的最佳实践。通过下一节的内容,读者将能够掌握在使用javah工具时需要注意的关键点以及如何通过该工具更有效地实现Java和本地代码的交互。
3. 创建Java类包含native方法
3.1 native方法的声明和使用
3.1.1 native方法的语法结构
在Java中声明一个native方法非常简单,只需要在方法声明前加上关键字 native
。这表示该方法将由非Java代码实现,通常是C或C++。native方法没有方法体,它们在Java层面充当一个抽象方法的角色。下面是一个简单的示例:
public class MyNativeClass {
/**
* A native method that is implemented by the native library
* named 'libnative-lib.so'
*/
public native String myNativeMethod();
}
在上述代码中, myNativeMethod
方法被声明为native。这告诉Java虚拟机(JVM),该方法的具体实现将位于一个外部库中。
3.1.2 Java中native方法的意义和用途
native方法允许Java代码和底层本地代码(通常是用C或C++编写的)进行交互。这在很多场景下非常有用:
- 性能优化:对于计算密集型操作,将代码用更接近硬件的语言实现通常会获得更好的性能。
- 资源访问:直接访问操作系统或硬件资源,如相机、GPS等。
- 代码重用:可以利用现有的本地库而不必将它们完全重写为Java。
native方法为开发者提供了一种灵活的方式来扩展Java的功能,尤其是在没有合适Java实现或性能至关重要的场景下。
3.2 Java类与native代码的交互方式
3.2.1 Java和C/C++的交互模型
Java和C/C++代码之间的交互是通过JNI(Java Native Interface)实现的。JNI是Java提供的一套标准编程接口,允许Java代码和其他语言写的代码进行交互。交互模型主要基于以下步骤:
- 加载库:使用
System.loadLibrary
加载包含native方法实现的本地库。 - 方法查找:JNI通过方法签名找到并调用相应native方法的实现。
- 参数传递:JNI提供了数据类型映射,将Java对象和基本类型转换为本地代码可以理解的格式。
- 返回值处理:将native方法的返回值转换为Java可识别的数据类型。
3.2.2 数据类型在Java和native层的对应关系
JNI定义了一套完整的类型映射,以便在Java和本地代码之间传递数据。主要的类型映射如下:
- Java的基本数据类型对应到本地代码中相应的C/C++类型。
- Java对象类型需要通过JNI函数
FindClass
和GetMethodID
等函数来处理。 - 字符串类型在JNI中特别需要使用
NewStringUTF
和GetStringUTFChars
等函数来转换。
为了使用这些映射,开发者必须确保在native代码中正确地声明和转换数据类型,以避免数据丢失或错误。例如,下面是一个在Java和C++之间传递字符串的例子:
Java端声明:
public native String stringFromNative();
C++端实现:
JNIEXPORT jstring JNICALL
Java_MyNativeClass_stringFromNative(JNIEnv *env, jobject obj) {
const char *nativeString = "Hello from native";
return env->NewStringUTF(nativeString);
}
在本节中,我们学习了native方法在Java中的声明和使用,以及Java与本地代码交互的基本模型和数据类型对应关系。在接下来的章节中,我们会深入了解如何使用javah工具来生成C/C++头文件,以及如何编写本地代码来实现这些native方法。
4. 通过javah生成C/C++头文件
在JNI编程中, javah
工具是将Java类中的native声明转换成C或C++头文件的关键一环。这个过程是将Java层的native方法映射到本地代码的桥梁,对于开发者来说,理解如何生成和使用这些头文件是进行JNI开发的基础。
4.1 javah命令的使用和参数解析
4.1.1 如何使用javah命令生成头文件
在命令行中, javah
命令的使用非常简单。假设有一个Java类 com.example.MyClass
,其中包含native方法声明,我们可以通过以下命令来生成头文件:
javah com.example.MyClass
该命令会在当前目录下生成一个名为 com_example_MyClass.h
的头文件。这个文件包含了所有native方法的声明,但并不包含方法的实现。需要注意的是, javah
已经不再推荐使用,在Java 8及之后的版本中,开发者应该使用 javac
命令配合 -h
参数来生成头文件:
javac -h . com.example.MyClass
这将指示 javac
编译器在当前目录生成相应的头文件。
4.1.2 头文件结构和内容的解读
生成的头文件结构非常直观,它包含了Java类的完整包名转换成的路径,以及native方法的签名。例如:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_MyClass */
#ifndef _Included_com_example_MyClass
#define _Included_com_example_MyClass
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_com_example_MyClass_nativeMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
这个文件定义了 Java_com_example_MyClass_nativeMethod
函数,其名称由Java类和方法名拼接而成,且所有Java标识符都转换成了C的标识符形式。
4.2 头文件在native开发中的作用
4.2.1 头文件作为native代码开发的蓝图
一旦头文件生成,它就成为本地代码开发者的主要参考文件。头文件明确了需要实现的方法名,参数类型和返回类型,帮助开发者了解Java层的调用接口。开发者需要根据这些声明来编写具体的实现代码。
4.2.2 如何根据头文件实现具体的native方法
在头文件的指导下,开发者需要在C或C++源文件中实现对应的方法。在实现过程中,开发者需要遵循JNI规范来操作Java对象和调用Java方法。例如:
#include "com_example_MyClass.h"
#include <jni.h>
JNIEXPORT void JNICALL Java_com_example_MyClass_nativeMethod
(JNIEnv *env, jobject obj) {
// 这里编写native方法的具体实现代码
}
native方法的实现需要正确处理Java类型和本地类型的转换,确保数据传递无误。
通过以上的步骤,开发者可以将Java代码与本地代码高效地结合起来,充分发挥各自语言的优势,实现更加强大和高效的系统级功能。
5. 编写C/C++代码实现native方法
编写C/C++代码以实现Java中的native方法是JNI开发中至关重要的一步。本章节将深入探讨JNI环境下C/C++代码编写的基础,包括环境设置、函数调用规则、数据类型和结构体的使用,以及一些高级技巧,如错误处理、异常抛出、对象引用和全局引用的管理。
5.1 native方法的C/C++实现基础
在开始编写native代码之前,开发者需要理解JNI环境设置的基本要求和native方法调用的规则。接下来将逐步讲解如何在C/C++中设置JNI环境,并提供一些常用的JNI数据类型和结构体的使用示例。
5.1.1 JNI环境设置和函数调用规则
在C/C++代码中实现native方法时,首先需要初始化JNI环境。JNI环境是一个接口指针,通常通过 AttachCurrentThread
方法与当前线程关联,或者通过 FindClass
方法获取Java类的引用。所有对Java对象的操作都需要在JNI环境中进行。
//JNI环境初始化示例代码
jclass cls;
jmethodID mid;
jobject obj;
// 获取JNI环境接口指针
JNIEnv* env = NULL;
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6;
args.name = NULL; // 线程名
args.group = NULL; // 线程组
// 将JNI环境与当前线程关联
JNI.attachCurrentThread((void**)&env, &args);
// 获取Java类引用
cls = env->FindClass("com/example/MyClass");
// 获取Java方法ID
mid = env->GetMethodID(cls, "myJavaMethod", "()V");
// 创建Java对象实例
obj = env->NewObject(cls, mid);
上述代码展示了如何在C++代码中初始化JNI环境,并获取Java类、方法ID和对象实例的引用。当native方法结束时,需要调用 DetachCurrentThread
方法断开当前线程与JNI环境的关联。
在函数调用规则方面,当native方法是静态时,可以直接通过类名和方法名获取方法ID;若为非静态方法,则需要先获取Java对象实例的引用,再使用该引用来获取方法ID。
5.1.2 常用的JNI数据类型和结构体使用
JNI提供了丰富的数据类型和结构体,用于在C/C++代码中处理Java数据类型。例如, jint
用于表示Java中的 int
, jstring
表示Java中的 String
。此外, jobject
、 jclass
和 jmethodID
等类型用于表示不同类型的引用。
JNI提供了数据类型转换的功能,如 GetIntField
、 SetIntField
等函数,用于获取和设置Java对象的属性值。结构体如 jfieldID
用于存储对Java类字段的引用。
// 示例:获取和设置Java对象中的int字段
jfieldID fid = env->GetFieldID(cls, "myIntField", "I");
// 获取Java对象的int字段值
jint val = env->GetIntField(obj, fid);
// 设置Java对象的int字段值
env->SetIntField(obj, fid, new_val);
5.2 高级native方法实现技巧
在深入实现native方法的过程中,会遇到各种复杂情况,如错误处理和异常抛出、对象引用的管理等。掌握这些高级技巧对编写高效、稳定的JNI代码至关重要。
5.2.1 错误处理和异常抛出机制
在C/C++代码中处理错误时,JNI提供了 Throw
和 ThrowNew
方法用于抛出异常。为了确保资源释放,应使用Java的try-catch结构,或在C++中使用异常处理结构。
try {
// JNI方法调用
} catch(jni::JNIException& e) {
// 异常处理代码
}
5.2.2 对象引用和全局引用的管理
在JNI编程中,本地代码对Java对象的引用通常分为局部引用和全局引用。局部引用只在native方法执行期间有效,而全局引用则需要手动释放。局部引用过多可能造成内存泄露,因此在不需要时应当使用 DeleteLocalRef
方法进行删除。
全局引用则通过 NewGlobalRef
创建,它们在native方法之外的任何地方都有效,直到被 DeleteGlobalRef
方法删除。
// 创建全局引用示例
jobject globalRef = env->NewGlobalRef(localRef);
// 删除全局引用示例
env->DeleteGlobalRef(globalRef);
这些高级技巧的掌握,将使开发者能够编写更加健壮和可维护的JNI代码。接下来的章节将进一步介绍使用CMakeLists.txt来配置和构建项目,这是在Android NDK开发中常用的一种构建系统配置方式。
6. 使用CMakeLists.txt配置和构建项目
6.1 CMakeLists.txt基础和结构
CMake是一个跨平台的自动化构建系统,它使用CMakeLists.txt文件来控制编译过程,生成原生的构建环境和编译脚本。在Android开发中,CMake已经成为了官方推荐的构建系统之一,特别是在处理JNI项目时,CMake可以提供更灵活的编译配置和更好的性能。
6.1.1 CMakeLists.txt的语法和关键字
CMakeLists.txt文件由一系列命令组成,每个命令由一个关键字开头。基本的命令包括 cmake_minimum_required
、 project
、 add_library
、 target_link_libraries
等。例如:
cmake_minimum_required(VERSION 3.4.1)
project(NativeLib)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
6.1.2 如何组织CMakeLists.txt以适应项目结构
在大型项目中,CMakeLists.txt文件通常会被组织为模块化的结构,以支持不同的功能或模块。可以创建顶层CMakeLists.txt文件,并在不同的目录下创建子目录的CMakeLists.txt文件。顶层的CMakeLists.txt文件包含子目录,利用 add_subdirectory()
命令。
例如,如果有一个名为 lib
的子目录包含了需要构建的库,顶层的CMakeLists.txt可以包含如下内容:
cmake_minimum_required(VERSION 3.4.1)
project(MyProject)
add_subdirectory(lib)
6.2 集成CMakeLists.txt到Android项目中
6.2.1 CMakeLists.txt与Android.mk的对比
Android.mk是传统Android项目中使用Makefile进行构建配置的文件,而CMakeLists.txt提供了更现代化的构建选项。相比Makefile,CMake提供了更好的跨平台支持、更灵活的构建规则和更清晰的语法结构。随着Android Studio对CMake的原生支持,开发者越来越多地转向使用CMake进行项目构建。
6.2.2 配置和构建流程详解及案例演示
配置和构建使用CMake的Android项目可以分为以下几个步骤:
- 创建CMakeLists.txt文件并配置构建参数。
- 在Android项目中指定CMakeLists.txt的位置。
- 构建项目,并确保CMake正确配置了环境。
- 编译生成的动态链接库(.so文件)。
以下是一个配置和构建Android项目使用CMake的案例演示:
- 在项目的根目录下创建一个CMakeLists.txt文件,配置如下:
cmake_minimum_required(VERSION 3.4.1)
# 设置项目名
project(MyNativeProject)
# 包含子目录中的CMakeLists.txt
add_subdirectory(./src/main/cpp)
# 设置C++标准
set(CMAKE_CXX_STANDARD 11)
# 寻找日志库
find_library(log-lib log)
# 将native-lib库链接到日志库
target_link_libraries(native-lib ${log-lib})
- 在app的
build.gradle
文件中指定CMakeLists.txt的位置:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
}
- 同步项目后,使用Android Studio的构建系统来编译和运行项目。可以通过
gradlew build
命令行工具进行构建,也可以直接在Android Studio的构建菜单中选择相应的操作。
通过以上的步骤,你就可以使用CMake配置和构建Android项目中的native库了。这为复杂项目的模块化和灵活构建提供了可能,同时也可以更好地利用现代开发工具和流程。
简介:Java Native Interface (JNI) 允许Java代码与本地语言编写的代码进行交互,尤其在Android开发中,通过C++或C语言提高性能或访问硬件功能时发挥关键作用。尽管Android Studio 3.0及以上版本支持Gradle插件内置的JNI支持,使用javah工具编译JNI仍是许多开发者的首选。本教程将引导读者了解javah工具的使用方法,包括创建Java类、使用javah命令生成头文件、编写C/C++代码、配置CMakeLists.txt以及构建项目。最后,介绍如何在Java代码中调用本地方法,强调即使在新的开发环境中,理解javah的工作原理依然对开发者具有重要意义。