目录
- 一、JNI简介
- 1.1 什么是JNI
- 1.2 用途
- 1.3 优点
- 二、初探JNI
- 2.1 新建cpp\cmake
- 2.2 build.gradle配置
- 2.3 java层配置
- 2.4 cmake和c++
- 三、API详解
- 3.1 JNI API
- 3.1.1 数据类型
- 3.1.2 方法
- 3.2 CMake脚本
- 四、再探JNI
一、JNI简介
1.1 什么是JNI
JNI(Java Native Interface)是Java提供的一种机制,用于实现Java和本地(Native)代码之间的交互。通过JNI,Java程序可以调用本地代码(如C、C++)中的函数,实现跨语言的互操作性。
1.2 用途
JNI主要用于以下几个方面:
调用系统级别的库和函数:可以使用JNI调用操作系统提供的底层功能,如文件操作、网络通信等。
提高性能:通过JNI将性能关键的部分用本地代码实现,可以提高程序的执行效率。
访问硬件资源:JNI可以用于访问硬件资源,如摄像头、传感器等。
调用第三方库:可以使用JNI调用第三方的本地库,以实现更多功能。
1.3 优点
使用JNI具有以下几个优点:
跨语言支持:可以与本地代码(如C、C++)进行无缝交互,扩展了Java的能力。
性能优势:通过JNI可以将性能关键的部分用本地代码实现,提高程序的执行效率。
访问系统资源:可以通过JNI访问系统级别的资源和功能,扩展了Java程序的能力。
调用第三方库:可以利用JNI调用第三方的本地库,扩展了Java程序的功能。
native层的代码往往更加安全,反编译so文件比反编译jar文件要难得多,往往把涉及到密码密钥相关的功能用C/C++实现,然后java层通过jni调用
通过JNI,Java程序可以充分利用本地代码的优势,实现更多功能和性能优化,同时也扩展了Java的应用领域。
二、初探JNI
为了更直接了解JNI,用一个简单的示例先来看一下JNI。
2.1 新建cpp\cmake
使用安卓Studio
在app/src/main文件夹下新建cpp文件夹
cpp文件夹下新建CMakeLists.txt和native-lib.cpp
2.2 build.gradle配置
配置模块名称"myjni"和产出的架构平台
defaultConfig {
applicationId "com.henry.basic"
namespace "com.henry.basic"
......
ndk {
// 指定库名称
moduleName "myjni"
// 指定需要产出哪些架构平台
abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64"
}
}
指定CMake脚本路径和版本号
android {
compileSdk 34
......
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
}
2.3 java层配置
定义一个JNIDemo 类
静态加载myjni 库
native方法返回String字符串,一会在c++层面实现。
public class JNIDemo {
static {
System.loadLibrary("myjni");
}
public JNIDemo() {
}
public native String helloJni();
}
static { System.loadLibrary("myjni"); }
会在类被加载到内存中时执行。当首次使用该类时,Java虚拟机会加载并初始化这个类,执行静态代码块中的代码,从而加载本地库 myjni。这确保了在调用任何类方法或创建类实例之前,本地库已经被加载进内存,以便后续的JNI调用可以正常进行。
Launcher中的Activity
在Oncreate中实例化JNIDemo,并调用native方法。
public class jniActivity extends AppCompatActivity {
String TAG = "henry";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dialog_test);
JNIDemo demo = new JNIDemo();
Log.d(TAG, " : " + demo.helloJni());
}
}
2.4 cmake和c++
上层和配置已经完成
接下来就剩下c++层面的实现和cmake脚本的编写了。
初学者看到cmake脚本肯定会头大,建议新建工程时选择C++,工程会自动生成Cmake脚本和cpp层面的实现,在此基础上学习Cmake。
回到我们当前的工程编写c++和Cmake
native-lib.cpp:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_henry_cmaketest_JNIDemo_helloJni(JNIEnv *env, jobject thiz) {
// TODO: implement helloJni()
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
1.#include <jni.h>:包含了JNI的头文件,提供了JNI的相关函数和数据结构的定义。
2.#include < string>:包含了C++标准库中的字符串处理相关的头文件。
3.extern “C”:指定了使用C语言的调用约定,确保编译器正确处理函数名。
4.JNIEXPORT jstring JNICALL:定义了本地方法的返回类型为 jstring,表示返回一个Java的字符串对象。JNIEXPORT 和 JNICALL 是JNI的宏定义,用于声明本地方法。
5.Java_com_henry_cmaketest_JNIDemo_helloJni(JNIEnv *env, jobject thiz):实现了一个名为 helloJni 的本地方法,该方法与Java中的 JNIDemo 类中的 helloJni 方法对应。JNIEnv *env 是JNI环境指针,jobject thiz 是Java对象。
std::string hello = “Hello from C++”;:创建一个C++字符串 hello,内容为 “Hello from C++”。
return env->NewStringUTF(hello.c_str());:使用JNI环境指针 env 的 NewStringUTF 方法将C++字符串转换为Java字符串对象,并返回给Java调用方。
CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
project("myjni")
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
1.指定了CMake的最低版本要求为3.22.1
2.定义了项目名称为 “myjni”。
3.添加一个名为 ${CMAKE_PROJECT_NAME} 的共享库,这里会被替换为 “myjni”。共享库类型为 SHARED,表示构建一个共享库。native-lib.cpp 是该共享库的源文件,用于实现JNI的本地方法。
4.将目标库 ${CMAKE_PROJECT_NAME} 与指定的库进行链接。在这里,目标库会链接到 android 库和 log 库。android 库提供了Android平台相关的功能和API,而 log 库用于在Android平台上输出日志。
运行,看一下LOG输出: 正是我们native-lib.cpp里面定义的字符串。
本地使用Everything搜一下myjni字段:
打开.bat:
相关命令带一些参数
打开txt:
先编译cpp文件
然后链接共享库生成libmyjni.so
打开相关.so所在文件夹:四个架构的so库都已生成。
三、API详解
初探JNI已经完毕,接下来需要归纳一些知识了,稍微大一点的项目用到的不仅仅是上面的皮毛,比如CMake脚本的各种命令所代表的含义?native-lib.cpp里的JNIEXPORT、jstring 、JNICALL都是什么意思?
3.1 JNI API
上例只是简单的返回了一个字符串,实际上我们还可以做很多事情,jni.h都给我们定义好了标准,我们按照它的标准来即可。
比如,java中叫boolean,jni中叫jboolean,jni给我们提供了若干个映射表,将java中的类型与jni中的类型进行了一 一映射,其中包括基本数据类型映射,引用数据类型映射,方法签名(包含参数和返回值)映射,以下是这三个映射表:
3.1.1 数据类型
基本数据类型映射表
引用数据类型映射表
方法签名
示例
public native String stringJni();
public native float floatJni(int number,Boolean enabled);
对应的native层:
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_henry_cmaketest_JNIDemo_floatJni(JNIEnv *env, jobject thiz, jint number, jobject enabled) {
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_henry_cmaketest_JNIDemo_stringJni(JNIEnv *env, jobject thiz) {
}
顺便讲一下J开头所代表的含义:
符号 | 描述 |
---|---|
extern “C” | 用于指定函数按照 C 的方式进行编译。在 JNI 开发中,由于 JNI 是用 C 语言编写的,因此需要使用 extern “C” 来告诉编译器按照 C 的方式进行编译,以确保函数名不会被 C++ 的名称修饰(name mangling)。 |
JNIEXPORT | 这是一个宏定义,用于指示编译器将函数导出为共享库中的一个 JNI 函数。在 JNI 开发中,通常会使用这个宏定义来声明 JNI 函数。 |
JNICALL | 这是一个宏定义,用于指示 JNI 函数的调用约定。在 JNI 开发中,通常会使用这个宏定义来声明 JNI 函数的调用约定。 |
JNIEnv *env | 这是一个指向 JNI 环境的指针,用于在本地代码中与 Java 虚拟机进行交互。通过 env 可以调用 JNI 提供的函数来获取 Java 对象、调用 Java 方法等操作。 |
jobject thiz | 这是一个代表当前对象的引用,通常用于在本地代码中调用 Java 对象的方法。在 JNI 中,thiz 代表当前调用 JNI 函数的 Java 对象。 |
3.1.2 方法
jni访问调用对象
方法 | 描述 |
---|---|
GetObjectClass | 获取调用对象的类,我们称其为target |
FindClass | 根据类名获取某个类,我们称其为target |
IsInstanceOf | 判断一个类是否为某个类型 |
IsSamObject | 是否指向同一个对象 |
jni访问java成员变量的值
方法 | 描述 |
---|---|
GetFieldId | 根据变量名获取target中成员变量的ID |
GetIntField | 根据变量ID获取int变量的值,对应的还有byte,boolean,long等 |
SetIntField | 修改int变量的值,对应的还有byte,boolean,long等 |
jni访问java静态变量的值
方法 | 描述 |
---|---|
GetStaticFieldId | 根据变量名获取target中静态变量的ID |
GetStaticIntField | 根据变量ID获取int静态变量的值,对应的还有byte,boolean,long等 |
SetStaticIntField | 修改int静态变量的值,对应的还有byte,boolean,long等 |
jni访问java成员方法
方法 | 描述 |
---|---|
GetMethodID | 根据方法名获取target中成员方法的ID |
CallVoidMethod | 执行无返回值成员方法 |
CallIntMethod | 执行int返回值成员方法,对应的还有byte,boolean,long等 |
jni访问java静态方法
方法 | 描述 |
---|---|
GetStaticMethodID | 根据方法名获取target中静态方法的ID |
CallStaticVoidMethod | 执行无返回值静态方法 |
CallStaticIntMethod | 执行int返回值静态方法,对应的还有byte,boolean,long等 |
jni访问java构造方法
方法 | 描述 |
---|---|
GetMethodID | 根据方法名获取target中构造方法的ID,注意,方法名传 |
NewObject | 创建对象 |
jni创建引用
方法 | 描述 |
---|---|
NewGlobalRef | 创建全局引用 |
NewWeakGlobalRef | 创建弱全局引用 |
NewLocalRef | 创建局部引用 |
DeleteGlobalRef | 释放全局对象,引用不主动释放会导致内存泄漏 |
DeleteLocalRef | 释放局部对象,引用不主动释放会导致内存泄漏 |
除此之外,jni还提供了异常处理机制,处理方式跟java一样有两种,要么往上(java层)抛,要么自己捕获处理
方法 | 描述 |
---|---|
ExceptionOccurred | 判断是否有异常发生 |
ExceptionClear | 清除异常 |
Throw | 往上(java层)抛出异常 |
ThrowNew | 往上(java层)抛出自定义异常 |
以上只是常用API,其他的可以自行到jni.h文件里去查看。
3.2 CMake脚本
CMake编写规则:
CMakeLists会自动创建两个变量,PROJECT_SOURCE_DIR
和PROJECT_NAME
字段 | 描述 | 示例 |
---|---|---|
cmake_minimum_required | 指定所需的最低 CMake 版本。 | cmake_minimum_required(VERSION 3.10) |
project | 定义项目的名称 | project(MyProject) |
PROJECT_SOURCE_DIR | 本CMakeLists.txt所在的文件夹路径 | |
PROJECT_NAME | 本CMakeLists.txt的project名称 | add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp) |
add_executable | 添加可执行文件,指定可执行文件的名称和源文件。 | add_executable(my_executable main.cpp) |
add_library | 添加库文件,指定库文件的名称和源文件。 | add_library(my_library my_source.cpp) |
target_link_libraries | 链接库文件到可执行文件或其他库文件。 | target_link_libraries(my_executable my_library) |
include_directories | 添加头文件搜索路径。 | include_directories(include) |
link_directories | 添加库文件搜索路径。 | link_directories(lib) |
set | 设置变量。 | set(SRC_FILES main.cpp) |
if | 条件判断。 | if(CONDITION)…endif() |
foreach | 遍历列表。 | foreach(item IN LISTS LIST_VARIABLE) …endforeach() |
message | 输出消息。 | message(“Hello, CMake!”) |
install | 安装目标文件到指定目录。 | install(TARGETS my_executable DESTINATION bin) |
find_package | 查找外部库。 | find_package(OpenGL REQUIRED) |
add_definitions | 添加编译器选项。 | add_definitions(-DDEBUG) |
add_subdirectory | 添加子目录。 | add_subdirectory(subdir) |
find_library | 在 CMake 中用于查找指定的库文件。 | find_library(VAR name)VAR:指定一个变量,用于存储查找到的库文件的路径。name:要查找的库文件的名称。 |
set_target_properties | 用于设置目标的属性。 | set_target_properties(my_executable PROPERTIES CXX_STANDARD 11) |
IMPORTED_LOCATION | 指定 IMPORTED 目标的位置。 | set_target_properties(my_library PROPERTIES IMPORTED_LOCATION /path/to/libmylib.so) |
log-lib | 是一个变量,通常用于存储 Android NDK 中日志库的名称 | target_link_libraries(my_executable ${log-lib})。 |
log | log 是 Android NDK 中的一个系统库,用于输出日志信息。 | target_link_libraries(my_executable log) 来链接 log 库。 |
PROPERTIES | PROPERTIES 是用于设置目标属性的关键字 通过 set_target_properties 函数,可以设置目标的各种属性,如编译选项、输出路径等。 | set_target_properties(my_executable PROPERTIES CXX_STANDARD 11) 设置可执行文件的 C++ 标准为 C++11 |
四、再探JNI
这一次,多用一些JNI相关知识写一个demo
新建一个test方法,并添加到native-lib.cpp中。
再新建一个mycpp.cpp文件,activity中新建一个test2()方法,jni实现在mycpp.cpp文件中
CMakeLists.txt中添加mycpp.cpp:
cmake_minimum_required(VERSION 3.22.1)
project("myjni")
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp mycpp.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
Activity:
public class jniActivity extends AppCompatActivity {
static {
System.loadLibrary("myjni");
}
private int num = 1;
String TAG = "henry";
private TextView tv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.jnitest);
JNIDemo demo = new JNIDemo();
tv = findViewById(R.id.jniview);
Log.d(TAG, " : " + demo.stringJni());
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
test();
test2();
}
});
}
public native void test();
public native void test2();
}
mycpp.cpp:
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "henry"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__) // 定义LOGD类型
extern "C"
JNIEXPORT void JNICALL
Java_com_henry_cmaketest_jniActivity_test2(JNIEnv *env, jobject thiz) {
LOGD("-------------new cpp test");
}
native-lib.cpp:
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "henry"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__) // 定义LOGD类型
extern "C"
JNIEXPORT jstring JNICALL
Java_com_henry_cmaketest_JNIDemo_stringJni(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
/**
* 参数一:JNIEnv* env表示指向可用JNI函数表的接口指针,所有跟jni相关的操作都需要通过env来完成
* 参数二:jobject是调用该方法的java对象,这里是MainActivity调用的,所以thiz代表MainActivity
* 方法名:Java_包名_类名_方法名
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_henry_cmaketest_jniActivity_test(JNIEnv *env, jobject thiz) {
//获取Activity的class对象
jclass clazz = env->GetObjectClass(thiz);
//获取MainActivity中num变量id
/**
参数1:MainActivity的class对象
参数2:变量名称
参数3:变量类型,具体见上《表3-方法签名》
**/
jfieldID numFieldId = env->GetFieldID(clazz, "num", "I");
//根据变量id获取num的值
jint oldValue = env->GetIntField(thiz, numFieldId);
//将num变量的值+1
env->SetIntField(thiz, numFieldId, oldValue + 1);
//重新获取num的值
jint num = env->GetIntField(thiz, numFieldId);
//先获取tv变量id
jfieldID tvFieldId = env->GetFieldID(clazz, "tv", "Landroid/widget/TextView;");
//根据变量id获取textview对象
jobject tvObject = env->GetObjectField(thiz, tvFieldId);
//获取textview的class对象
jclass tvClass = env->GetObjectClass(tvObject);
//获取setText方法ID
/**
参数1:textview的class对象
参数2:方法名称
参数3:方法参数类型和返回值类型,具体见上《表3-方法签名》
**/
jmethodID methodId = env->GetMethodID(tvClass, "setText", "([CII)V");
//获取setText所需的参数
//先将num转化为jstring
char buf[64];
sprintf(buf, "%d", num);
jstring pJstring = env->NewStringUTF(buf);
const char *value = env->GetStringUTFChars(pJstring, JNI_FALSE);
//创建char数组,长度为字符串num的长度
jcharArray charArray = env->NewCharArray(strlen(value));
//开辟jchar内存空间
jchar *pArray = (jchar *) calloc(strlen(value), sizeof(jchar));
//将num字符串缓冲到内存空间中
for (int i = 0; i < strlen(value); ++i) {
*(pArray + i) = *(value + i);
}
//将缓冲的值写入到上面创建的char数组中
env->SetCharArrayRegion(charArray, 0, strlen(value), pArray);
//调用setText方法
env->CallVoidMethod(tvObject, methodId, charArray, 0, env->GetArrayLength(charArray));
//释放资源
env->ReleaseCharArrayElements(charArray, env->GetCharArrayElements(charArray, JNI_FALSE), 0);
free(pArray);
pArray = NULL;
}
看一下效果:
参考链接:
CMakeLists.txt的超傻瓜手把手教程
JNI 从入门到实践,爆肝万字详解!
基础JNI语法和常见使用