深入理解JNI与javah编译器的使用

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: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项目可以分为以下几个步骤:

  1. 创建CMakeLists.txt文件并配置构建参数。
  2. 在Android项目中指定CMakeLists.txt的位置。
  3. 构建项目,并确保CMake正确配置了环境。
  4. 编译生成的动态链接库(.so文件)。

以下是一个配置和构建Android项目使用CMake的案例演示:

  1. 在项目的根目录下创建一个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})
  1. 在app的 build.gradle 文件中指定CMakeLists.txt的位置:
android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
    ...
}
  1. 同步项目后,使用Android Studio的构建系统来编译和运行项目。可以通过 gradlew build 命令行工具进行构建,也可以直接在Android Studio的构建菜单中选择相应的操作。

通过以上的步骤,你就可以使用CMake配置和构建Android项目中的native库了。这为复杂项目的模块化和灵活构建提供了可能,同时也可以更好地利用现代开发工具和流程。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: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的工作原理依然对开发者具有重要意义。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值