Android JNI 技术入门指南

引言

在Android开发中,Java是一种主要的编程语言,然而,对于一些性能要求较高的场景(如音视频处理、图像处理、计算密集型任务等),我们可能需要使用到C或C++等语言来编写底层的高效代码。为了实现Java代码与C/C++代码之间的交互,Android提供了一个强大的工具------JNI(Java Native Interface)。通过JNI,Java可以调用C/C++代码,C/C++也可以调用Java代码,从而实现高效的原生交互。

开始之前先了解一些基础概念

开始之前

如果你对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 的工作机制可以分为几个步骤:

  1. Java 调用 C/C++ 方法 :通过在 Java 中声明本地方法(native),并使用 System.loadLibrary() 加载本地库。Java 代码通过 JNI 机制调用底层的 C/C++ 函数。
  2. C/C++ 调用 Java 方法:JNI 允许在 C/C++ 中调用 Java 中的方法,甚至可以操作 Java 对象。
  3. 数据传递:通过 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++ 的转换(通过 GetStringUTFCharsGetStringChars env->GetStringUTFChars(jstring, nullptr)
Object jobject jobject Java 对象到 C/C++ 的转换,可以用来操作任意 Java 对象 env->GetObjectClass(jobject)
Class jclass jclass Java Class 对象到 C/C++ 的转换,通过 FindClassGetObjectClass 获取类引用 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反射代码:

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 方法签名

GetMethodIDGetStaticMethodID,拿到相应的方法。

c++ 复制代码
jmethodID methodId = env->GetMethodID(clazz, "methodName", "(I)Ljava/lang/String;");

这个方法的签名为 (I)Ljava/lang/String;,表示该方法有一个 int 类型的参数,返回一个 String 类型。

(2) 获取字段签名 GetFieldIDGetStaticFieldID,拿到类的属性字段。

c++ 复制代码
jfieldID fieldId = env->GetFieldID(clazz, "fieldName", "Ljava/lang/String;");

这个字段的签名为 Ljava/lang/String;,表示它是一个 String 类型的字段。

(3) 构造函数签名 通过签名和构造函数名称查找类的构造函数 ID。构造函数的签名与普通方法相同,但没有返回类型。

c++ 复制代码
jmethodID constructorId = env->GetMethodID(clazz, "<init>", "(I)V");

构造函数的签名为 (I)V,表示它接受一个 int 类型的参数并没有返回值。

5. 在Android中使用JNI

5.1 配置项目

build.gradle包含对NDK的支持:

c 复制代码
android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

5.2 编写Java代码

在Java代码中声明本地方法:

java 复制代码
public class NativeLib {
    static {
        System.loadLibrary("native-lib");
    }

    public native String stringFromJNI();
}

5.3 编写C/C++代码

在cpp目录下创建对应的C/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 如:

c 复制代码
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配置如下:

c 复制代码
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_librarytarget_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

相关推荐
烬奇小云4 小时前
认识一下Unicorn
android·python·安全·系统安全
顾北川_野16 小时前
Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
android
CYRUS STUDIO16 小时前
Android 下内联汇编,Android Studio 汇编开发
android·汇编·arm开发·android studio·arm
右手吉他16 小时前
Android ANR分析总结
android
PenguinLetsGo18 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
杨武博20 小时前
音频格式转换
android·音视频
音视频牛哥1 天前
Android音视频直播低延迟探究之:WLAN低延迟模式
android·音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·android rtmp
ChangYan.1 天前
CondaError: Run ‘conda init‘ before ‘conda activate‘解决办法
android·conda
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
夏非夏1 天前
Android 生成并加载PDF文件
android