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构建so
或dll
文件。
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技术