写在前面
最近在看一些底层源码,发现 JNI 这块还是有必要系统的看一下,索性就写一写博客,加深加深印象🍻
本文重点聊一聊一些干货,避免长篇大论
JNI 概述
JNI 是什么?
定义:Java Native Interface ,即 Java 本地接口 作用:使得 Java 与本地其他类型语言(如 C、C++ )进行交互
注意:
-
JNI 是 Java 调用 Native 语言的一种特性
-
JNI 是属于 Java 的,与 Android 平台无直接关系
以 Java 8 为例,JNI 最新在线 API: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
为什么会有 JNI ?
实际的使用中,Java 需要与本地代码进行交互,因为 Java 项目具备跨平台的特点,所以 Java 与本地代码交互的能力非常弱,采用 JNI 特性,增强 Java 与本地代码交互的能力
JNI 和 NDK 的关系
JNI 是 Java 平台提供的一套非常强大的框架 Java Native Interface,用于与本地代码进行相互调用
NDK 是 Android 平台提供的 Native 开发工具集,Native Development Kit的缩写。NDK 其中包含了 JNI 并对其进行了封装
关于 JNI 的入口头文件会有两份,分别在 JDK 和 NDK 中,进一步说明 Android 的 NDK 对 JDK 的 JNI 进行了二次封装
常见所在目录:
-
JDK: JAVA_HOME/include/jni.h
-
NDK: ~/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h
环境准备
-
Android Studio 版本:Android Studio Hedgehog | 2023.1.1 Patch 1
-
Gradle 版本:gradle-8.0
-
targetSdk:33
Android Studio 安装相关工具:
创建示例
File -> New,选择 Native++ 模板:
检查 NDK 相关配置是否正常:
native-lib.cpp C++ 示例代码:
MainActivity.java 示例代码:
CMake 构建
在 Android 开发中,CMake 用于编译 C/C++ 代码。从 Android Studio 2.2 版本开始,Google 引入了对 CMake 的支持,使得开发者可以通过 CMake 和 NDK 将 C/C++ 代码编译成底层的库,然后再配合 Gradle 的编译将库打包到 APK 中
CMake 具体的配置信息如下:
# cmake最低版本要求
cmake_minimum_required(VERSION 3.22.1)
# 配置库生成路径
# CMAKE_CURRENT_SOURCE_DIR是指 cmake库的源路径,通常是build/.../cmake/
# /../jniLibs/是指与CMakeList.txt所在目录的同级目录:jniLibs (如果没有会新建)
# ANDROID_ABI 生成库文件时,采用gradle配置的ABI策略(即:生成哪些平台对应的库文件)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
# 添加库
add_library( # 库名
native-lib
# 类型:
# SHARED 是指动态库,对应的是.so文件
# STATIC 是指静态库,对应的是.a文件
# 其他类型:略
SHARED
# native类路径
native-lib.cpp)
# 查找依赖库
find_library(
# 依赖库别名
log-lib
# 希望加到本地的NDK库名称,log指NDK的日志库
log)
# 链接库,建立关系( 此处就是指把log-lib 链接给native-lib使用 )
target_link_libraries(
# 目标库名称(native-lib就是咱们要生成的so库)
native-lib
# 要链接的库(上面查找的log库)
${log-lib})
摘自: https://www.cnblogs.com/qixingchao/p/11911787.html
默认 so 的目录(注意 AS 版本,不同版本不一致):
安装包反编译 so 路径位置:
示例解读
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring
JNICALL
Java_org_lulu_jnilearning_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "这是 C++ 中的代码";
char * str = "这是 C++ 中的代码";
return env->NewStringUTF(str);
}
-
extern "C"
:避免按照 C++ 的方式去编译 C 函数为什么需要使用它呢? 这是因为 C++ 支持函数重载,所以编译器在编译函数时会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名。而 C 语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名
-
JNIEXPORT
:用来表示该函数是否可导出(宏定义) -
JNICALL
:(可以缺少)jni call 约束函数入栈顺序和堆栈内存清理规则,在 Linux 中置空 -
jstring
:代表 Java 中的 String -
JNIEnv
:C 和 Java 相互调用的桥梁,代表 Java 环境,内部包含了众多函数(后续详细介绍) -
jobject
:( Java 侧声明的 native 方法为非静态,传递时为 jobject)Java 传递过来的示例对象,即当前 Java 类的对象,示例中 MainActivity.this 就是它 -
jclass
:( Java 侧声明的 native 方法为静态,传递时为 jclass)Java 传递过来的类对象,即当前 Java 类的 Class 对象,示例中 MainActivity.class 就是它
JNIEnv
C 和 Java 相互调用的桥梁,代表 Java 环境
通过 JNIEnv
就可以对 Java 端的代码进行操作:
-
创建 Java 对象
-
调用 Java 对象方法
-
获取 Java 对象的属性
-
...
C++ 中 JNIEnv
指向_JNIEnv
,而_JNIEnv
是定义的一个结构体,包裹了 JNINativeInterface
,而在 C 中JNIEnv
就是 JNINativeInterface
常用的方法:
函数名称 | 作用 |
---|---|
NewObject | 创建Java类中的对象 |
NewString | 创建Java类中的String对象 |
NewArray | 创建类型为Type的数组对象 |
GetField | 获得类型为Type的字段 |
SetField | 设置类型为Type的字段 |
GetStaticField | 获得类型为Type的static的字段 |
SetStaticField | 设置类型为Type的static的字段 |
CallMethod | 调用返回值类型为Type的static方法 |
CallStaticMethod | 调用返回值类型为Type的static方法 |
FindClass | 通过类路径获取 Java 类的类对象 |
GetObjectClass | 通过类对象获取 Java 类的类对象 |
Java、JNI、C/C++ 基本类型映射
Java 、C/C++都有一些常用的数据类型,分别是如何与JNI类型对应的呢?做以下整理:
JNI中定义的别名 | Java类型 | C/C++类型 |
---|---|---|
jint / jsize | int | int |
jshort | short | short |
jlong | long | long / long long (__int64) |
jbyte | byte | signed char |
jboolean | boolean | unsigned char |
jchar | char | unsigned short |
jfloat | float | float |
jdouble | double | double |
jobject | Object | _jobject* |
JNI描述符(签名)
JNI 开发时,我们除了写本地C/C++实现,还可以通过JNIEnv *env
调用 Java 层代码,如获得某个字段、获取某个函数、执行某个函数等:
//获得某类中定义的字段id
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
//获得某类中定义的函数id
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
以上函数和 Java 的反射比较类似,参数说明:
-
clazz:类的 class 对象
-
name:字段名称、函数名称
-
sig:字段描述符(字段签名),函数描述符(函数签名)
特别的,对 sig 进行解释:
-
如果是字段,表示字段类型的描述符
-
如果是函数,表示函数结构的描述符,即:每个参数类型描述符 + 返回值类型描述符
后面会结合实际案例进一步说明
整理字段类型签名:
Java类型 | 字段描述符(签名) | 备注 |
---|---|---|
int | I | int 的首字母、大写 |
float | F | float 的首字母、大写 |
double | D | double 的首字母、大写 |
short | S | short 的首字母、大写 |
long | L | long 的首字母、大写 |
char | C | char 的首字母、大写 |
byte | B | byte 的首字母、大写 |
boolean | Z | 因 B 已被 byte 使用,所以 JNI 规定使用 Z |
object | L + /分隔完整类名 | String 如: Ljava/lang/String |
array | [ + 类型描述符 | int[] 如:[I |
整理函数类型签名:
Java函数 | 函数描述符(签名) | 备注 |
---|---|---|
void | V | 无返回值类型 |
Method | (参数字段描述符...)返回值字段描述符 | int add(int a,int b) 如:(II)I |
如何通过指令获取当前 class 的签名:
找到此目录: 执行以下命令:
javap -s -p MainActivity.class
相关描述符信息如下:
Compiled from "MainActivity.kt"
public final class org.lulu.jnilearning.MainActivity extends androidx.appcompat.app.AppCompatActivity {
//...
private java.lang.String nonStaticField;
descriptor: Ljava/lang/String;
private static java.lang.String staticField;
descriptor: Ljava/lang/String;
//...
}
Java和C++交互示例
我们以从 Native 侧修改 Java 侧非静态字段和静态字段为例,编写一段简单 Java 和 C++ 交互的示例:
修改非静态字段:
MainActivity.kt
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
/**
* 待修改的非静态字段
*/
private var nonStaticField = "Java 非静态字段"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
Log.d(TAG, "changeNonStaticField before: $nonStaticField")
changeNonStaticField()
Log.d(TAG, "changeNonStaticField after: $nonStaticField")
}
/**
* 修改非静态字段的 Native 方法
*/
external fun changeNonStaticField()
companion object {
// Used to load the 'jnilearning' library on application startup.
init {
System.loadLibrary("jnilearning")
}
}
//...
}
native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_org_lulu_jnilearning_MainActivity_changeNonStaticField(JNIEnv *env, jobject thiz) {
//1. 获取当前实例的类对象,即 MainActivity.class
// 有两种方式
// 1.1 jclass FindClass(const char* name)
//jclass clazz = env->FindClass("org/lulu/jnilearning/MainActivity");
// 1.2 【推荐】jclass GetObjectClass(jobject obj)
jclass clazz = env->GetObjectClass(thiz);
//2. 获取要修改的非静态字段 Id
//jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jfieldID nonStaticFieldId = env->GetFieldID(clazz, "nonStaticField", "Ljava/lang/String;");
//3. 创建一个新的 jstring,准备赋值
//jstring NewStringUTF(const char* bytes)
jstring jstr = env->NewStringUTF("Java 非静态字段,C++ 已修改");
//4. 这是这个新的 jstring 给 nonStaticField 字段
//void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
env->SetObjectField(thiz, nonStaticFieldId, jstr);
}
代码执行结果如下:
changeNonStaticField before: Java 非静态字段
changeNonStaticField after: Java 非静态字段,C++ 已修改
静态变量的修改类似,仅贴出C++代码:
extern "C"
JNIEXPORT void JNICALL
Java_org_lulu_jnilearning_MainActivity_changeStaticField(JNIEnv *env, jobject thiz) {
//1. 获取当前实例的类对象,即 MainActivity.class
// 有两种方式
// 1.1 jclass FindClass(const char* name)
//jclass clazz = env->FindClass("org/lulu/jnilearning/MainActivity");
// 1.2 【推荐】jclass GetObjectClass(jobject obj)
jclass clazz = env->GetObjectClass(thiz);
//2. 获取要修改的静态字段 Id
//jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
jfieldID staticFieldId = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
//3. 创建一个新的 jstring,准备赋值
//jstring NewStringUTF(const char* bytes)
jstring jstr = env->NewStringUTF("Java 静态字段,C++ 已修改");
//4. 这是这个新的 jstring 给 staticField 字段
//void SetStaticObjectField(jclass clazz, jfieldID fieldID, jobject value)
env->SetStaticObjectField(clazz, staticFieldId, jstr);
}
最后
这篇文章就到这里了,希望大家能够学到一些有用的知识,也欢迎你们在评论区留言交流。如果你觉得这篇文章有趣或者有帮助,不妨给我点个赞或者分享给你的朋友。感谢你们的阅读,我们下次再见!