
引言
在Android开发中,Java是一种主要的编程语言,然而,对于一些性能要求较高的场景(如音视频处理、图像处理、计算密集型任务等),我们可能需要使用到C或C++等语言来编写底层的高效代码。为了实现Java代码与C/C++代码之间的交互,Android提供了一个强大的工具——JNI(Java Native Interface)。通过JNI,Java可以调用C/C++代码,C/C++也可以调用Java代码,从而实现高效的原生交互。
开始之前
如果你对C/C++语言比较陌生,可以先看一下我的这两篇文章:
 (大致过一下就行,挑重点去记,毕竟不是做C++开发,没必要完全理解,更多的是我们在开发中去学习)
- C语言基础
- C++基础
1. 什么是 JNI(Java Native Interface)?
JNI 是 Java 与其他编程语言(通常是 C 或 C++)之间的接口,允许 Java 代码与底层的本地代码进行交互。通过 JNI,我们可以在 Java 代码中调用本地(native)方法,或者让本地代码调用 Java 方法。
1.1 为什么要使用 JNI?
JNI 的主要作用是实现 Java 程序与本地程序之间的交互,特别是在以下几种情况下非常有用:
- 性能优化:有些运算或操作,Java 实现的效率可能较低,使用 C/C++ 可以提高性能,特别是在图像处理、音视频编解码等领域。
- 访问底层硬件或特性:Java 不能直接访问底层硬件或操作系统的某些特性,而 JNI 使得 Java 程序可以调用 C/C++ 中的底层代码,进而访问这些特性。
- 重用现有的本地代码库:有时为了节省开发时间,我们希望直接重用一些已有的 C/C++ 代码或第三方库,这时 JNI 就是连接 Java 和本地代码的桥梁。
1.2 JNI 如何工作?
JNI 的工作机制可以分为几个步骤:
- Java 调用 C/C++ 方法:通过在 Java 中声明本地方法(native),并使用System.loadLibrary()加载本地库。Java 代码通过 JNI 机制调用底层的 C/C++ 函数。
- C/C++ 调用 Java 方法:JNI 允许在 C/C++ 中调用 Java 中的方法,甚至可以操作 Java 对象。
- 数据传递:通过 JNI,Java 和 C/C++ 之间可以传递基本数据类型(如整数、浮点数)和复杂的数据结构(如数组、对象等)。
1.3 JNI 的基本结构
- Java 层:Java 中声明 native方法,并通过System.loadLibrary()加载本地库。
- 本地层:通过 C/C++ 实现 JNI 接口,并将它编译成共享库(.so 文件)。
- JNI 头文件:使用 javah工具(或者在 Android 中通过ndk-build)生成的头文件,定义了 Java 类与本地方法之间的映射关系。
2. NDK 与 JNI 的关系
在 Android 开发中,NDK(Native Development Kit)是一个工具集,它允许开发者在 Android 应用中编写和使用 C/C++ 代码。JNI 是 NDK 的一部分,它提供了 Android 中 Java 代码和 C/C++ 本地代码之间的交互接口。
2.1 NDK 的功能
NDK 是一组工具和库,允许开发者用 C 和 C++ 编写 Android 应用中的一些性能关键的代码。NDK 提供的功能包括:
- 访问硬件资源:通过 NDK,你可以直接访问一些低级的硬件特性,比如摄像头、传感器、GPS 等。
- 性能优化:一些计算密集型的任务(例如图像处理、音视频编解码等)可以通过 C/C++ 实现,性能上更有优势。
- 使用已有的本地库:有时候开发者会利用一些已有的 C/C++ 库或第三方库,而这些库通常需要通过 NDK 来编译和链接。
2.2 NDK 与 JNI 的结合
- JNI 是 NDK 与 Java 层之间的桥梁,利用 JNI,Java 层可以调用本地层的 C/C++ 函数,反之,C/C++ 代码也可以调用 Java 层的代码。
- 使用 NDK 时,JNI 使得 Java 和 C/C++ 之间的数据和方法调用变得可能。
- 通过 JNI,我们可以在 Java 代码中调用 NDK 中编写的本地方法,或者直接操作 Java 对象。
3. 数据类型
Java、JNI、C/C++ 三者之间的数据类型转换是跨语言编程中的一个核心问题,尤其在涉及到 Java 调用 C/C++ 编写的本地方法时。JNI(Java Native Interface)作为 Java 与 C/C++ 交互的桥梁,提供了一套标准机制来实现 Java 与本地代码之间的数据交换。
3.1 基础类型
Java 通过 JNI 与 C/C++ 交互时,JNI 提供了一些专门的类型和方法来桥接 Java 类型与 C/C++ 类型的差异。
| Java 类型 | JNI 类型 | C/C++ 类型 | 备注 | 
|---|---|---|---|
| byte | jbyte | char(8-bit) | JNI 使用 jbyte来表示 Java 的byte类型。 | 
| short | jshort | short(16-bit) | JNI 使用 jshort来表示 Java 的short类型。 | 
| int | jint | int(32-bit) | JNI 使用 jint来表示 Java 的int类型。 | 
| long | jlong | long long(64-bit) | JNI 使用 jlong来表示 Java 的long类型。 | 
| float | jfloat | float(32-bit) | JNI 使用 jfloat来表示 Java 的float类型。 | 
| double | jdouble | double(64-bit) | JNI 使用 jdouble来表示 Java 的double类型。 | 
| char | jchar | wchar_t(16-bit) | JNI 使用 jchar来表示 Java 的char类型,它是 16 位 Unicode 字符,C/C++ 中通常用wchar_t来表示宽字符。 | 
| boolean | jboolean | bool(1-bit) | JNI 使用 jboolean来表示 Java 的boolean类型,jboolean是 8 位的布尔值,通常与 C/C++ 中的bool类型兼容。 | 
3.2 引用类型
Java 对象类型通常通过 JNI 提供的 API 转换为 C/C++ 中的指针类型,这些指针类型并不代表实际的数据内容,而是用于访问 Java 对象或方法的接口。
| Java 类型 | JNI 类型 | C/C++ 类型 | 转换方式 | JNI API 示例 | 
|---|---|---|---|---|
| String | jstring | jstring | Java String到 C/C++ 的转换(通过GetStringUTFChars或GetStringChars) | env->GetStringUTFChars(jstring, nullptr) | 
| Object | jobject | jobject | Java 对象到 C/C++ 的转换,可以用来操作任意 Java 对象 | env->GetObjectClass(jobject) | 
| Class | jclass | jclass | Java Class对象到 C/C++ 的转换,通过FindClass或GetObjectClass获取类引用 | env->FindClass("java/lang/String") | 
| Array (Object) | jobjectArray | jobjectArray | 对象数组到 C/C++ 的转换,通过 JNI API 访问数组元素 | env->GetObjectArrayElement(jobjectArray, index) | 
| Array (Primitive) | jintArray | jintArray | 基本类型数组转换(如 int[]到jintArray) | env->GetIntArrayElements(jintArray, nullptr) | 
| Field | jfieldID | jfieldID | 通过 JNI 获取字段 ID,通常用于访问 Java 类中的字段 | env->GetFieldID(jclass, "fieldName", "I") | 
| Method | jmethodID | jmethodID | 通过 JNI 获取方法 ID,通常用于调用 Java 方法 | env->GetMethodID(jclass, "methodName", "()V") | 
4. JNI 中的 Java 签名信息
在学习签名之前,先来看一段Java反射代码:
import java.lang.reflect.Method;
public class ReflectionExample {
    public void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
    public static void main(String[] args) throws Exception {
        // 获取 ReflectionExample 类的 Class 对象
        Class<?> clazz = Class.forName("ReflectionExample");
        // 获取方法 sayHello(String)
        Method method = clazz.getMethod("sayHello", String.class);
        // 创建实例并调用方法
        Object instance = clazz.getDeclaredConstructor().newInstance();
        method.invoke(instance, "World");
    }
}
在clazz.getMethod中,我们通过方法名称 和 参数类型拿到了sayHello方法,在JNI中C/C++ 调用Java的方法也类似,不同点是参数类型 和 返回值 要用签名方式代替(因为C/C++不能直接拿到Java方法嘛),那么JNI中签名长什么样呢?
4.1 基本数据类型的签名
Java 中的基本数据类型对应 JNI 中的签名符号。JNI 使用单一字符来表示 Java 中的基本数据类型。
| Java 类型 | JNI 签名 | 
|---|---|
| boolean | Z | 
| byte | B | 
| char | C | 
| short | S | 
| int | I | 
| long | J | 
| float | F | 
| double | D | 
| void | V | 
4.2 对象类型的签名
Java 对象类型(类类型、接口类型等)的签名格式如下:
- 以 L开始,后接类的全名(包括包名),最后以;结尾。例如,String类型的签名为Ljava/lang/String;。
- 注意:数组类型的签名也以 [开头,并且每增加一个维度就多一个[。
| Java 类型 | JNI 签名 | 
|---|---|
| String | Ljava/lang/String; | 
| Object | Ljava/lang/Object; | 
| int[] | [I | 
| String[] | [Ljava/lang/String; | 
| Object[] | [Ljava/lang/Object; | 
4.3 方法签名
Java 方法的签名由两部分组成:方法的参数类型和返回类型,方法签名的格式为:(参数类型1, 参数类型2, ...)返回类型。例如,一个有两个 int 参数并返回 String 类型的方法签名为 (II)Ljava/lang/String;。
| Java 方法 | JNI 签名 | 
|---|---|
| int add(int a, int b) | (II)I | 
| String getName(String name) | (Ljava/lang/String;)Ljava/lang/String; | 
| void setValues(int x, int y) | (II)V | 
4.4 构造函数签名
Java 构造函数的签名与普通方法类似,不同之处在于构造函数没有返回类型(V),且通常没有方法名。在 JNI 中,构造函数的签名格式是 (参数类型1, 参数类型2, ...)V。
| Java 构造函数 | JNI 签名 | 
|---|---|
| MyClass(int, String) | (ILjava/lang/String;)V | 
4.5 静态方法签名(重点)
静态方法的签名与实例方法类似,唯一的区别是静态方法是类级别的,因此它通过类的对象引用来调用。静态方法的签名与实例方法的签名相同,但 JNI 调用时不需要实例对象。
4.6 示例
(1) 获取 Java 方法签名
GetMethodID 或 GetStaticMethodID,拿到相应的方法。
jmethodID methodId = env->GetMethodID(clazz, "methodName", "(I)Ljava/lang/String;");
这个方法的签名为 (I)Ljava/lang/String;,表示该方法有一个 int 类型的参数,返回一个 String 类型。
(2) 获取字段签名
 GetFieldID 或 GetStaticFieldID,拿到类的属性字段。
jfieldID fieldId = env->GetFieldID(clazz, "fieldName", "Ljava/lang/String;");
这个字段的签名为 Ljava/lang/String;,表示它是一个 String 类型的字段。
(3) 构造函数签名
 通过签名和构造函数名称查找类的构造函数 ID。构造函数的签名与普通方法相同,但没有返回类型。
jmethodID constructorId = env->GetMethodID(clazz, "<init>", "(I)V");
构造函数的签名为 (I)V,表示它接受一个 int 类型的参数并没有返回值。
5. 在Android中使用JNI
5.1 配置项目
在build.gradle包含对NDK的支持:
android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}
5.2 编写Java代码
在Java代码中声明本地方法:
public class NativeLib {
    static {
        System.loadLibrary("native-lib");
    }
    public native String stringFromJNI();
}
5.3 编写C/C++代码
在cpp目录下创建对应的C/C++文件,实现上述声明的本地方法:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_NativeLib_stringFromJNI(JNIEnv* env, jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
5.4 配置CMakeLists.txt
在项目的根目录下,配置CMakeLists.txt 如:
cmake_minimum_required(VERSION 3.4.1)
add_library(
    native-lib
    SHARED
    src/main/cpp/native-lib.cpp)
find_library(
    log-lib
    log)
target_link_libraries(
    native-lib
    ${log-lib})
如果你项目中想写多个.cpp文件,CMakeLists.txt xiugai配置如下:
cmake_minimum_required(VERSION 3.4.1)
add_library(
    native-lib
    SHARED
    src/main/cpp/native-lib.cpp)
add_library(
    native-lib2
    SHARED
    src/main/cpp/native-lib2.cpp)
//更多...
find_library(
    log-lib
    log)
target_link_libraries(
    native-lib
    ${log-lib})
    
target_link_libraries(
    native-lib2
    ${log-lib})
//更多...
即在 find_library 和 target_link_libraries 增加相对应的.cpp文件即可。
6. 实战
因为在写这篇文章之前,我已经完善了一些实战的功能,在此就不一一讲解了,包括:
- 传递int数据
- 传递String数据
- 传递Array数据
- 在C++中调用Java的返回值Void方法
- 在C++中调用Java的返回值int方法
- 在C++中调用Java的返回值String方法
- 在C++中显示Toast
- 文本加解密演示
- 锅炉压力进度条
- C++ 创建子线程
- C++ 线程锁之生产者消费者
- 串口通信(SerialPort) - 可拿来直接使用,已验证功能。
代码已经上传Github:JNIStudy,感兴趣的可以下载看看,里面我加了世上最全注释,由基础到复杂,看不懂来打我!😆
打包为.so文件可以看我的这篇文章:在Android中,将 .cpp 文件编译成共享库(.so 文件)
7. 最后
之前一直对JNI望而却步,真正学过后回头看看,也不是那么的难,难的是你不主动去学。所有伟大,都源于一个勇敢的开始!共勉!
另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai



