【JNI】JNI环境搭建

1 前言

        JNI (Java Native Interface) 是 JDK 提供的一种机制,用于实现 Java 代码与其他语言(主要是 C 和 C++)编写的本地代码之间的交互。

        JNI 接口详见 JDK 安装目录中的 include/jni.h 文件,Android NDK 对 JDK 的 JNI 进行了扩展,对应的 jni.h 文件路径如下。

SDK\ndk\27.2.12479018\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\include\jni.h

         JNI 官方文档见 → https://docs.oracle.com/en/java/javase/17/docs/specs/jni/index.html

2 在命令行中编译代码

2.1 环境准备

        1)安装 JDK

        下载 JDK(建议下载 x64 Compressed Archive 文件),解压后将根目录写入 JAVA_HOME 环境变量中,如下。

        然后将 %JAVA_HOME%、%JAVA_HOME%\bin 加入 Path 环境变量中,如下。

        在命令行分别输入 java --version、 javac --version,如果有打印版本号,说明 JDK 环境搭建成功。

        2)安装 MinGW

        MinGW(Minimalist GNU for Windows)是一个用于 Windows 平台的轻量级 GNU 开发环境,它允许开发者在 Windows 上使用 GCC(GNU Compiler Collection)工具链编译本地 Windows 应用程序。

        下载 MinGW,解压后将根目录下的 bin 目录写入 Path 环境变量中。

        在命令行输入 gcc --version、g++ --version, 如果有打印版本号,说明 MinGW 环境搭建成功。

2.2 一个简单的案例

        1)编写 java 代码

        用记事本编辑以下代码,保存为 HelloJNI.java。

        HelloJNI.java

/**
 * 1. 生成字节码和头文件
 *    javac -encoding UTF-8 -h ./ HelloJNI.java (或 javah -encoding UTF-8 ./ HelloJNI.java, jdk10 之前)
 * 
 * 2. 生成 dll 或 so
 *    g++ -fPIC -I"%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o hello.dll HelloJNI.c
 * 
 * 3. 运行
 *    java HelloJNI
 */
public class HelloJNI {
   static {
      // 运行时加载 native 库, hello.dll (Windows) 或 hello.so (Linux/Unix/Android)
      System.loadLibrary("hello");
   }

   private native void sayHello();

   public static void main(String[] args) {
      new HelloJNI().sayHello();
   }
}

        2)生成字节码和头文件

        在命令行执行以下命令,生成字节码(.class 文件)和头文件(.h 文件)。

// jdk10 及之后
javac -encoding UTF-8 -h ./ HelloJNI.java
// jdk10 之前
javah -encoding UTF-8 ./ HelloJNI.java

        执行完以上命令后,会生成 HelloJNI.class 和 HelloJNI.h 文件。HelloJNI.h 文件内容如下。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

        3)编写 C 代码 

#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}

        4)生成 dll 或 so

        在命令行执行以下命令,生成 ddl(或 so)文件。

// 生成 dll 文件 (Windows)
g++ -fPIC -I"%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o hello.dll HelloJNI.c

// 生成 so 文件 (Linux/Unix/Android)
g++ -fPIC -I"%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o hello.so HelloJNI.c

        5)运行代码

        在命令行执行以下命令,运行代码。

java HelloJNI

        运行结果如下。

Hello World!

        6)一键编译运行脚本

@echo off
set class_name=HelloJNI
set native_file_name=HelloJNI
set lib_name=hello

:: 删除缓存文件
del %class_name%.class
del %class_name%.h
del %lib_name%.dll

:: 生成字节码和头文件
javac -encoding UTF-8 -h ./ %class_name%.java

:: 生成 dll 或 so
g++ -fPIC -I"%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o %lib_name%.dll %native_file_name%.c

:: 运行
java %class_name%
pause

3 在 Android Studio 中编译代码

        在 Android Studio 中,依此选择 New -> New Project -> Native C++,创建 Native C++ 项目。

        也可以通过 New -> New Module -> Android Native Library,创建 Native Library。

        对于普通的 Android 项目,可以通过以下配置,使其支持 JNI。

        1)build.gradle

defaultConfig {
    ndk {
        abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
    }
}

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

        abiFilters 的作用是指定应用支持的 ABI(Application Binary Interface) 架构类型,即应用只会为指定的 CPU 架构生成原生库(.so 文件),而忽略其他架构。如果未配置 abiFilters,Gradle 默认会构建所有支持的 ABI(取决于 NDK 版本和项目依赖)。

armeabi-v7a: 32 位 ARM 架构 (较旧的 Android 设备)
arm64-v8a: 64 位 ARM 架构 (现代主流设备,性能更好)
x86: 32 位 Intel 架构 (模拟器或少数平板设备)
x86_64: 64 位 Intel 架构 (模拟器或高端设备)

        2)创建 cpp 目录

        3)C/C++ 代码

        如果用 C 语言实现,代码如下。

        demo.c

#include <jni.h>
#include <string.h>

JNIEXPORT jstring JNICALL
Java_com_zhyan8_test_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    return (*env)->NewStringUTF(env, "Hello from C");
}

        如果用 C++ 语言实现,代码如下。 

        demo.cpp

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_zhyan8_test_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    return env->NewStringUTF("Hello from C++");
}

        可以看到,C 与 C++ 的主要区别如下。

  • 头文件:C 中 string 的引用是 #include <string.h>,C++ 中 string 的引用是 #include <string>。
  • extern "C":C++ 中多了 extern "C" 修饰,表示使用 C 风格的函数链接,禁止 C++ 的名称修饰。如果不禁用名称修饰,C++ 编译器会对函数名进行修饰,以支持函数重载等特性,会导致 Java 无法查找到本地函数。因此,使用 extern "C" 的函数不能重载(因为名称不再修饰)。
  • env:C 中的 env 是二级指针,所以访问函数通过 (*env)-> 访问;C++ 中的 env 是一级指针,所以访问函数通过 env-> 访问。

        4)CMakeLists.txt

# 设置最小CMake版本
cmake_minimum_required(VERSION 3.22.1)

# 声明项目名, 可以通过${PROJECT_NAME}访问, 在顶层CMakeLists.txt中, 也可以通过${CMAKE_PROJECT_NAME}访问
project("hello")

# 设置源码包路径 (相对于当前CMakeLists.txt的路径)
set(PACKAGES_DIR "com/zhyan8/demo")

# 头文件目录列表 (便于以该路径为相对路径访问头文件)
include_directories(${PACKAGES_DIR})

# 源文件列表 (相对于当前CMakeLists.txt的路径)
file(GLOB_RECURSE SOURCES ./${PACKAGES_DIR} *.cpp)

# 添加预构建库, SHARED用于将该库声明为一个shared library
# 在Java/Kotlin中, 需要通过System.loadLibrary()加载该库
add_library(${CMAKE_PROJECT_NAME} SHARED ${SOURCES})

# 添加链接的三方库文件
target_link_libraries(${CMAKE_PROJECT_NAME}
        # 链接到目标库的库文件
        android
        log)

        5)MainActivity.java

package com.zhyan8.test;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("hello");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i("MainActivity", stringFromJNI());
    }

    public native String stringFromJNI();
}

        6)打印日志

        如果想在 JNI 中使用 Android 中的日志类打印日志,可以使用以下代码。

#include <jni.h>
#include <android/log.h>

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "JNI_TAG", __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "JNI_TAG", __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "JNI_TAG", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI_TAG", __VA_ARGS__)

extern "C"
JNIEXPORT void JNICALL
Java_com_zhyan8_test_MainActivity_testLog(JNIEnv* env, jobject /* this */) {
    LOGD("value1=%d", true);
    LOGI("value2=%d", 100);
    LOGW("value3=%f", 3.5f);
    LOGE("value4=%s", "abcd");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little_fat_sheep

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值