Android中的JNI调用

143 阅读5分钟

JNI全称Java Native Interface,是Java语言与本地C/C++代码交互的方法,允许Java代码调用本地代码。JNI机制使得Java程序能够充分利用底层系统功能,扩展了Java程序的功能并提高性能以及保护代码安全。
1,在android中使用JNI
1)在Java代码MainActivity中,加载库并定义本地方法

static {
 System.loadLibrary("ericnative");
}

public native String stringFromJNI();
public native void callJavaMethod();

2)在本地代码nativeJNI.cpp文件中,

#include <jni.h>    //包含JNI的头文件,这是编写JNI代码所必需的,它提供了JNI函数和类型定义的接口。
#include <string>  //包含C++标准库中的字符串头文件,使得可以使用std::string类型。
#include <android/log.h> //包含Android日志系统的头文件

#define LOG_TAG "ERIC_JNI" //定义日志标签
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG, __VA_ARGS__)
//__VA_ARGS__ 是一个在 C 和 C++ 的宏(macro)定义中使用的预处理器(preprocessor)特性,它允许宏接受可变数量的参数。当你定义一个宏时,如果你不确定这个宏将来会被调用时传递多少个参数,你可以使用 __VA_ARGS__ 来捕获这些参数,并在宏的展开中使用它们。
//__VA_ARGS__ 是预处理器本身提供的一个特殊标识符,用于在宏定义中引用可变参数列表
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)


extern "C" //采用C的编译方式,因为JNIEnv是指向JNINativeInterface结构体
//在C中,JNIEnv通常被定义为指向JNINativeInterface结构体的指针;而在C++中,JNIEnv则是一个结构体类型,其内部包含一个指向JNINativeInterface结构体的指针。
//JNIEnv是线程相关的, 即在每个线程中都有一个JNIEnv指针, 每个JNIEnv都是线程专有的,其它线程不能使用本线程中的 JNIEnv, 线程A不能调用线程B的JNIEnv
//在JNI中,JNIEXPORT 和 JNICALL 是两个宏,它们用于指定JNI函数的导出和调用约定。这些宏的使用是JNI规范的一部分,旨在确保JNI函数能够正确地被Java虚拟机(JVM)调用,并且能够在不同的平台和编译器之间保持兼容性。
//JNIEXPORT 宏用于指定函数是导出的,即该函数可以被JVM或其他模块调用。在JNI中,所有native方法都需要被导出,以便JVM能够找到并调用它们。
//JNICALL 宏用于指定函数的调用约定。调用约定定义了函数如何接收参数、如何返回值,以及由谁负责清理堆栈等。不同的平台和编译器可能有不同的调用约定。例如,在Windows上,__stdcall 是一种常用的调用约定,而在Linux和Mac OS X上,默认的调用约定通常是 __cdecl,JNICALL 宏被设计为在编译时展开为适当的调用约定。这样,JNI函数就可以使用正确的调用约定被JVM调用,而无需担心平台或编译器的差异。
JNIEXPORT jstring JNICALL Java_com_example_ericnative_MainActivity_stringFromJNI(
     JNIEnv* env,                        //JNIEnv*是一个指向JNI环境的指针,提供了JNI函数的接口。
     jobject /* this */) {     //jobject是调用此JNI方法的Java对象的引用,但在这个例子中并没有使用到它(因此使用了注释/* this */来表示)
 std::string hello = "Hello from C++";
 std::string world = "enter c++ world";
 LOGI("LOG from JNI:%s,%s",hello.c_str(), world.c_str()); //__android_log_print期望的是C风格的字符串作为参数
 return env->NewStringUTF(hello.c_str()); //使用env->NewStringUTF(hello.c_str())将C++字符串转换为JNI中的jstring类型。调用.c_str()方法将其转换为C风格的字符串(const char*)
}

extern "C"
JNIEXPORT void JNICALL Java_com_example_ericnative_MainActivity_callJavaMethod(JNIEnv * env, jobject instance) { //函数是在 JNI 环境下自动与 Java 中的本地方法callJavaMethod声明关联的, JNI 函数本身不会在 Java 代码中直接调用,而是在 Java 代码中声明的本地方法(native method)被调用时,由 JVM 自动寻找并调用对应的 JNI 函数。
//    jclass clz = env->FindClass("com/example/ericnative/MainActivity");
 LOGI("Call callJavaMethod...");
 jclass  clz = env->GetObjectClass(instance);
 jmethodID  jmethodId = env->GetMethodID(clz,"stringFromJava", "()V");
 env->CallVoidMethod(instance, jmethodId);
}

3)在Java代码的MainActivity中调用定义的native方法即可实现java代码与本地代码的互相调用。
4)在Android开发中,CMake逐渐成为管理本地(native)代码(如C和C++)的主要方式之一,尤其是在Android Studio项目中。CMakeLists.txt文件是用于定义CMake项目的配置文件,它告诉CMake如何编译和链接项目的源代码。CMake是一个跨平台的自动化建构系统,它使用配置文件(如CMakeLists.txt)来生成标准的构建文件,这些文件随后被用于编译和链接应用程序。在Android项目中,CMake通常用于编译和链接C或C++代码。
在项目的app模块的build.gradle文件中,确保启用了CMake,并正确设置了CMake的路径和版本。这通常在android{}块的externalNativeBuild部分进行配置。

externalNativeBuild {
    cmake {
        path file('src/main/cpp/CMakeLists.txt')
        version '3.22.1' //CMake版本
    }
}

编写CMakelists.txt示例

# 设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.4.1)
# 设置项目名称
project("pro_native")
# 创建一个库,这里以静态库为例,也可以创建共享库  
add_library( # 设置生成的库名  
             native-lib  
             # 设置库类型为STATIC或SHARED  
             SHARED  
             # 提供源文件  
             src/main/cpp/native-lib.cpp )  
# 查找并链接日志库  
find_library( # 设置变量的名称  
              log-lib  
              # 指定库的名称  
              log )  
# 将日志库链接到你的目标库  
target_link_libraries( # 目标库  
                       native-lib  
                       # 链接的库  
                       ${log-lib} )

5)CMakeLists.txt在Android项目中的构建过程
在构建Android项目时(比如,通过点击Android Studio中的“Build”按钮或运行你的应用),Android Gradle插件会检测到CMakeLists.txt文件的存在,并触发CMake的执行。Gradle插件会调用CMake命令行工具,并传递给它一些参数,比如源代码目录、构建目录(通常是app/.externalNativeBuild/cmake/<build_type>/<arch_abi>/)和CMake选项(这些选项在build.gradle文件中指定)。CMake根据CMakeLists.txt中的指令和Gradle插件提供的参数,生成适用于目标平台和架构的构建文件(如Makefile)。然后,CMake可能会调用实际的构建工具(如make或ninja)来编译和链接你的C/C++代码。构建过程完成后,生成的对象文件、库文件和其他必要的文件将被放置在构建目录中。这些文件随后会被Android的打包工具(如aapt)使用,以生成最终的APK文件。该过程是自动的,通常不需要直接调用CMake命令行工具。