当前位置: 首页 > news >正文

Android NDK—JNI基础

文章目录

    • JNI简介
    • JNI使用步骤
      • 使用javac或javah生成JNI头文件
      • Clion创建C++ library项目
      • 将dll或so文件导入Java工程中使用
    • JNI API
      • JNI访问Java对象API:
      • JNI访问Java成员变量的值
      • JNI访问Java静态变量的值
      • JNI访问Java成员方法
      • JNI访问Java静态方法
      • JNI访问Java构造方法
      • JNI创建引用
      • JNI 异常
    • 实践小案例
      • 静态注册
      • 动态注册
    • 参考

JNI简介

jni全称java native interface,我把它分为三部分,java代表java语言,native代表当前程序运行的本地环境,一般指windows/linux,而这些操作系统都是通过C/C++ 实现的,所以native通常也指C/C++ 语言,interface代表java跟native两者之间的通信接口,jni可以实现java和C/C++通信。它是java生态的特征,所以定义在jdk标准当中。

使用场景与优势:

  • 跨平台性:虽然java具有跨平台的特点,但必须依赖于JVM虚拟机,而JVM是跑在Linux或Windows平台上,若要也硬件打交道,必须经过C/C++ 来对硬件操作,这就涉及到Java如何调用C/C++的问题。
  • 效率:Java的执行效率远低于C/C++ 的执行效率,使用jni技术,在Java层调用C/C++ 代码,可以提高程序的执行效率,最大化利用机器的硬件资源。
  • 安全:native层的代码往往更加安全,反编译so文件比反编译jar文件要难得多,所以,我们往往把涉及到密码密钥相关的功能用C/C++实现,然后java层通过jni调用。

在这里插入图片描述

JNI是属于Java的,与Android 无直接关系。

JNI使用步骤

如果使用Android Studio工程模版来创建项目的,可以忽略步骤。

使用javac或javah生成JNI头文件

在IDEA创建一个Java工程,如下:

执行命令: javac -h outDir sourceFile

在这里插入图片描述

  • \-h . 头文件的输出目录, . 表示是当前目录,后面必需加个空格
  • JNIDemo.java 源文件

结果:

在这里插入图片描述

或使用javah生成头文件:

在这里插入图片描述

Clion创建C++ library项目

在这里插入图片描述

  • static:静态库
    • 静态库会将目标代码以及所有需要依赖的库文件进行整体打包,执行时不再依赖外部环境。 一般静态库往往比动态库要大。
    • 静态库在windows上为.lib文件,Linux上为.a文件
  • shared:动态库
    • 动态库则只会将目标代码打包,运行时需要依赖外部环境。
    • 动态库在windows上为.dll文件,Linux上为so文件

1、将生成的com_hzw_jni_JNIDemo.h头文件到项目根目录。

在这里插入图片描述

2、找到jni.h jni_md.h文件复制到项目根目录。

  • windows去本地jdk安装目中找<jdk安装目录>/include/jni.h和<jdk安装目录>/include/win32/jni_md.h

  • ubuntu去本地jdk安装目录找<jdk安装目录>/include/jni.h和<jdk安装目录>/include/linux/jni_md.h

然后将的头文件com_hzw_jni_JNIDemo.h#include <jni.h>改为#include "jni.h",避免报红报错。

3、创建JNIDemo.cpp实现头文件

#include "com_hzw_jni_JNIDemo.h"// Java_类名路径_方法名
extern "C"
JNIEXPORT jstring JNICALL Java_com_hzw_jni_JNIDemo_helloJni(JNIEnv *env, jclass clazz)JNIDemo.cppreturn env->NewStringUTF("I am from c++");
}

3、在CMakeLists.txt导入.cpp.h文件。

cmake_minimum_required(VERSION 3.26)
project(jni_demo_c) # 项目名称set(CMAKE_CXX_STANDARD 17)add_library(jni_demo_c SHARED library.cppcom_hzw_jni_JNIDemo.hJNIDemo.cpp)

4、最后Build构建sodll文件。

在这里插入图片描述

dll或so文件的命名规则是lib项目名称

将dll或so文件导入Java工程中使用

1、将dll文件复制到Java工程中的libs目录下。

在这里插入图片描述

2、将libs文件目录关联到Resource中

在这里插入图片描述

3、代码通过System.loadLibrary加载dll或so文件

public class JNIDemo {static {// dll文件名称System.loadLibrary("libjni_demo_c");}public static native String helloJni();public static void  main(String[] args){System.out.println(helloJni());}}

4、编译运行,设置路径-Djava.library.path后即可运行

在这里插入图片描述

上面通过一个简单的案例讲解了jni的使用流程,大部分步骤都是固定的。在Android Studio创建C/C++ library有固定模版使用。

在这里插入图片描述

JNI API

Java和C/C++通信是通过jni来完成的,那么在jni方法中就涉及到对Java变量的访问(变量类型包括基本数据类型和引用数据类型),以及方法都有一一映射,比如,Java中叫boolean,jni中叫jboolean

主要有基本数据类型引用数据类型方法签名(包含参数和返回值三个映射表。

表1-基本数据类型映射表:

在这里插入图片描述

表2-引用数据类型映射表:

在这里插入图片描述

表3-方法签名:

在这里插入图片描述

JNI提供了一系列访问Java层的类成员API,比如变量(包括静态变量)、方法(包括静态方法),如下:

JNI访问Java对象API:

方法名作用
GetObjectClass获取调用对象的类,我们称其为target
FindClass根据类名获取某个类,我们称其为target
IsInstanceOf判断一个类是否为某个类型
IsSameObject是否指向同一个对象

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,注意,方法名传<init>
NewObject创建对象

JNI创建引用

方法名作用
NewGlobalRef创建全局引用
NewWeakGlobalRef创建弱全局引用
NewLocalRef创建局部引用
DeleteGlobalRef释放全局对象,引用不主动释放会导致内存泄漏
DeleteLocalRef释放局部对象,引用不主动释放会导致内存泄漏

JNI 异常

方法名作用
ExceptionOccurred判断是否有异常发生
ExceptionClear清除异常
Throw往上(java层)抛出异常
ThrowNew往上(java层)抛出自定义异常

以上是常用的JNI API,其他API可以在jni.h文件中查看。或者官网文档 https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

实践小案例

直接在Android Studio里面创建了一个Native工程,写一个递增计数的案例

public class MainActivity extends AppCompatActivity {// Used to load the 'jni_demo' library on application startup.static {System.loadLibrary("jni_demo");}private ActivityMainBinding binding;private TextView tvCounter;private int num = 1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());tvCounter =  binding.tvCounter;tvCounter.setOnClickListener(view -> {jniTest();});}public native void  jniTest(); // 定义Native方法
}

静态注册

#include <jni.h>
#include <string>
#include <android/log.h>// 宏定义标识符
#define TAG "hzw"
// 定义LOGD类型 __VA_ARGS__ 是可变参数
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);extern "C" JNIEXPORT jstringJNICALL
Java_com_hzw_jni_1demo_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {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_hzw_jni_1demo_MainActivity_jniTest(JNIEnv *env, jobject thiz) {// 获取MainActivity的class对象jclass clazz = env->GetObjectClass(thiz);// 1、给num属性赋新值 + 1/*** 1: 获取MainActivity的class对象* 2: 变量名称* 3:变量类型,《方法签名》*/// 获取int num 变量// 获取num变量idjfieldID numFieldID = env->GetFieldID(clazz, "num", "I");// 获取变量num 的值jint oldValue = env->GetIntField(thiz, numFieldID);// num值 +1env->SetIntField(thiz, numFieldID, oldValue + 1);// 重新获取num值jint num = env->GetIntField(thiz, numFieldID);//    const char *num_char = reinterpret_cast<const char *>(env->NewStringUTF(
//            std::to_string(num).c_str()));LOGD("num: %d", num);// 获取TextView tvCounter 变量// 先获取tvCounter变量的idjfieldID tvCounterFieldID = env->GetFieldID(clazz, "tvCounter", "Landroid/widget/TextView;");// 获取TextView对象jobject tvCounterObj =env->GetObjectField(thiz, tvCounterFieldID);// 获取TextView的class对象jclass  tvCounterClass =  env->GetObjectClass(tvCounterObj);/*** 操作方法* 参数1:textview的class对象* 参数2:方法名称* 参数3:方法参数类型和返回值类型,具体见上《表3-方法签名》* 方法签名规则:method(参数类型)返回值类型 -- void name(int a,double b) (ID)V* public final void setText(@NonNull char[] text, int start, int len)  : ([CII)V***/jmethodID setTextMethodID =env->GetMethodID(tvCounterClass,"setText","(Ljava/lang/CharSequence;)V");// 将int 转成 string类型jstring str = env->NewStringUTF(std::to_string(num).c_str());// 调用setText方法 赋值env->CallVoidMethod(tvCounterObj, setTextMethodID, str);
}

最后build APK后,反解后在lib可以查看各个CPU构架的so文件。

在这里插入图片描述

在Android中,当程序在Java层运行System.loadLibrary("jnitest");这行代码后,程序会去载入libjni_demo.so文件。于此同时,产生一个Load事件,这个事件触发后,程序默认会在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,与Load事件相对,在载入的.so文件被卸载时,Unload事件被触发。此时,程序默认会去载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,然后卸载.so文件。因此开发者经常会在JNI_OnLoad中做一些初始化操作,动态注册就是在这里进行的,使用env->RegisterNatives(clazz, gMethods, numMethods)

动态注册

env->RegisterNatives(clazz, gMethods, numMethods)

  • 参数1:Java对应的类
  • 参数2:JNINativeMethod数组
  • 参数3:JNINativeMethod数组的长度,也就是要注册的方法的个数

JNINativeMethod是JNI中定义的一种结构体。

typedef struct {const char* name; //java中要注册的native方法名const char* signature;//方法签名void*       fnPtr;//对应映射到C/C++中的函数指针
} JNINativeMethod;

JNINativeMethod可知,如果Java层的类发生了改变,Native层相对静态注册就改动的比较少,只需修改方法名和方法签名即可。灵活性很强。

#include <jni.h>
#include <string>
#include <android/log.h>// 宏定义标识符
#define TAG "hzw"
// 定义LOGD类型 __VA_ARGS__ 是可变参数
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);*** 动态注册* @param env* @param thiz*/
void native_jniTest(JNIEnv *env, jobject thiz) {// 获取MainActivity的class对象jclass clazz = env->GetObjectClass(thiz);// 1、给num属性赋新值 + 1
/*** 1: 获取MainActivity的class对象* 2: 变量名称* 3:变量类型,《方法签名》*/
// 获取int num 变量
// 获取num变量idjfieldID numFieldID = env->GetFieldID(clazz, "num", "I");
// 获取变量num 的值jint oldValue = env->GetIntField(thiz, numFieldID);
// num值 +1env->SetIntField(thiz, numFieldID, oldValue + 1);// 重新获取num值jint num = env->GetIntField(thiz, numFieldID);//    const char *num_char = reinterpret_cast<const char *>(env->NewStringUTF(
//            std::to_string(num).c_str()));LOGD("num: %d", num);// 获取TextView tvCounter 变量
// 先获取tvCounter变量的idjfieldID tvCounterFieldID = env->GetFieldID(clazz, "tvCounter", "Landroid/widget/TextView;");
// 获取TextView对象jobject tvCounterObj = env->GetObjectField(thiz, tvCounterFieldID);
// 获取TextView的class对象jclass tvCounterClass = env->GetObjectClass(tvCounterObj);/*** 操作方法* 参数1:textview的class对象* 参数2:方法名称* 参数3:方法参数类型和返回值类型,具体见上《表3-方法签名》* 方法签名规则:method(参数类型)返回值类型 -- void name(int a,double b) (ID)V* public final void setText(@NonNull char[] text, int start, int len)  : ([CII)V***/jmethodID setTextMethodID = env->GetMethodID(tvCounterClass, "setText","(Ljava/lang/CharSequence;)V");
// 将int 转成 string类型jstring str = env->NewStringUTF(std::to_string(num).c_str());
// 调用setText方法 赋值env->CallVoidMethod(tvCounterObj, setTextMethodID, str);
}static const JNINativeMethod nativeMethod[] = {/*参数1:java中要注册的native方法名参数2:方法签名参数3:对应映射到C/C++中的函数指针*/{"jniTest", "()V", (void *) native_jniTest},
};extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {JNIEnv *env = NULL;// 初始化JNIEnvif (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {return JNI_FALSE;}// 找到需要动态注册的java类jclass jniClass = env->FindClass("com/hzw/jni_demo/MainActivity");if (nullptr == jniClass) {return JNI_FALSE;}// 动态注册if (env->RegisterNatives(jniClass, nativeMethod,sizeof(*nativeMethod) / sizeof(nativeMethod[0])) != JNI_OK) {return JNI_FALSE;}// 返回JNI使用的版本return JNI_VERSION_1_4;}

源码:https://gitee.com/common-apps/jni-demo

参考

  • Java 生成 JNI 头文件
  • 一篇文章教你完全掌握jni技术
http://www.lryc.cn/news/573586.html

相关文章:

  • Linux(3)
  • Kafka 原理与核心机制全解析
  • Vite 原理深入剖析
  • 【PyTorch革命】机器学习系统编程模型的演进之路
  • 从C++编程入手设计模式——命令模式
  • 【机器学习四大核心任务类型详解】分类、回归、聚类、降维智能决策指南
  • 8.特征提取与直方图
  • MATLAB GUI界面设计 第二章——APP Designer操作正式入门
  • Linux 下的 socket
  • Node.js爬虫 CheerioJS ‌轻量级解析、操作和渲染HTML及XML文档
  • 【机器学习的五大核心步骤】从零构建一个智能系统
  • STM32-GPIO-推挽输出详解
  • 深入解析Flink Local模式启动流程源码:揭开作业初始化的神秘面纱
  • Ubuntu20 搭建 Java、Redis、Nginx
  • GO 语言学习 之 helloWorld
  • 2025年SVN学习价值分析
  • react day.js使用及经典场景
  • 【RocketMQ 生产者和消费者】- 消费者重平衡(3)- 消费者 ID 对负载均衡的影响
  • 微前端MFE: 通过共享模块通信(模块联邦Module Federation)
  • 【机器学习四大核心任务类型详解】分类、回归、聚类、降维都是什么?
  • 【论文阅读笔记】TransparentGS:当高斯溅射学会“看穿”玻璃,如何攻克透明物体重建难题?
  • 【Nature Communications】超高介电常数材料 Hf0.5Zr0.5O2(HZO)
  • Oracle 11G RAC修改public ip vip private ip
  • 【数据治理】要点整理-《数据管理能力成熟度评估模型》国家标准(GB/T 36073—2018)
  • Linux的文件权限
  • 16_设备树中的remote-endpoint演示基于视频字符设备Linux内核模块
  • python源码:执行pdf合并/分页/图片管理功能
  • 计算机网络课程设计--基于TCP协议的文件传输系统
  • 案例练习二
  • rom定制系列------红米note11 5G版 MTK芯片强解bl锁修复bug 官方系统 面具root批量线刷版