1、什么是JNI? 为什么需要JNI?
- JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作,Java调用C/C++在Java语言里面本来就有的,并非Android自创的。JNI就是Java调用C++的规范。当然,一般的Java程序使用的JNI标准可能和android不一样,Android的JNI更简单。
- 因为在我们的实际项目需求中,需要Java代码与C/C++代码进行交互,那么通过JNI可以实现Java代码与C/C++代码的交互。
2、如何使用JNI编程?
2.1 、JNI的命名规则
extern "C" JNIEXPORT jstring JNICALL Java_com_hellondk_NDKTool_test(JNIEnv *env, jclass clazz)
-
jstring
是返回值类型; -
Java_com_hellondk
是包名前缀必须是Java_开头; -
NDKTool
是类名,即声明Native方法的类名; -
test
是方法名; -
JNIEnv
方法参数是必须加的,表示JNI环境; -
jclass
方法参数是必须加的,当然这个参数如果声明的Native方法是非静态的那么这个参数就是jobject,这个参数表示在本地方法中声明的对象引用,如果是静态方法就是Class对象; - 其中
JNIEXPORT
和JNICALL
是JNI固定保留的关键字不要修改;
2.2、 如何实现JNI?
2.2.1、JNI开发流程的步骤:
- 在Java中先声明一个native方法;
- 注册Native方法,即使用JNI实现在Java中声明的方法,有两种实现方式;
- 编译动态库,即在CmakeLists.txt文件中配置编译生成动态库;
实现Hello JNI
在Java中先声明一个native方法;
public class NDKTool {
static {
System.loadLibrary("native-lib");
}
public static native String test();
}
注册Native方法,即使用JNI实现在Java中声明的方法,有两种实现方式;
extern "C" JNIEXPORT jstring JNICALL Java_com_hellondk_myapplication_NDKTool_test(JNIEnv *env, jclass clazz) {
return env->NewStringUTF("Java_com_hellondk_myapplication_NDKTool_test");
}
编译动态库,即在CmakeLists.txt文件中配置编译生成动态库;
cmake_minimum_required(VERSION 3.4.1)
include_directories(${CMAKE_SOURCE_DIR}/inc)
add_library(native-lib SHARED native-lib.cpp)
find_library(log-lib log)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}")
target_link_libraries( native-lib)
3、JNI数据类型
Java类 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型 |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
4、在JNI反射调
4.1、基本数据类型的签名采用一系列大写字母来表示, 如下表所示:
Java类型 | 签名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用类型 | L + 全限定名 + ; |
数组 | [+类型签名 |
在C/C++中反射创建Java的对象,调用Java的方法
#NDKTool
class NDKTool{
public native void cReflectJava(ReflectBean bean);
}
#JNI
extern "C" JNIEXPORT void JNICALL Java_com_helondk_myapplication_NDKTool_cReflectJava(
JNIEnv *env,
jobject thiz, //非静态方法,就是方法定义本身对象NDKTool的对象
jobject bean) {
//NDKTool
jclass pJclass = env->GetObjectClass(thiz);
jmethodID methodId = env->GetMethodID(pJclass, "printf", "()V");
env->CallVoidMethod(thiz, methodId);
jclass beanCls = env->GetObjectClass(bean);
//反射setName方法
jmethodID setMethodId = env->GetMethodID(beanCls, "setName", "(Ljava/lang/String;)V");
jstring pJstring = env->NewStringUTF("999999");
//调用setName方法
env->CallVoidMethod(bean, setMethodId, pJstring);
jmethodID getMethodId = env->GetMethodID(beanCls, "getName", "()Ljava/lang/String;");
//调用getName方法
jobject getMethodRes = env->CallObjectMethod(bean, getMethodId);
//将jbject转换为jstring
jstring pJobject = static_cast<jstring>(getMethodRes);
const char *utfChars = env->GetStringUTFChars(pJobject, NULL);
LOGE("get返回值:%s", utfChars);
// 静态方法
jmethodID staticMethodId = env->GetStaticMethodID(beanCls, "setAddress",
"()Ljava/lang/String;");
jobject staticMethodVal = env->CallStaticObjectMethod(beanCls, staticMethodId);
jstring staticMethodValStr = static_cast<jstring>(staticMethodVal);
const char *chars = env->GetStringUTFChars(staticMethodValStr, NULL);
LOGE("静态方法返回:%s", chars)
//对象Field
jfieldID pJfieldId = env->GetFieldID(beanCls, "name", "Ljava/lang/String;");
jstring filedStr = env->NewStringUTF("reflect filed");
env->SetObjectField(bean, pJfieldId, filedStr);
//静态Filed
jfieldID staticFieldId = env->GetStaticFieldID(beanCls, "address", "Ljava/lang/String;");
jstring staticFiledStr = env->NewStringUTF("static reflect filed");
env->SetStaticObjectField(beanCls, staticFieldId, staticFiledStr);
env->DeleteLocalRef(beanCls);
env->DeleteLocalRef(pJclass);
env->DeleteLocalRef(pJstring);
}
一般在JNI中,如果你想调用Java的方法或者修改Java对象的属性值,你只能通过反射,比如:音频播放进度回调;
5、JNI引用
在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
5.1 、局部引用
大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。因此无法跨线程、跨方法使用。
5.1.1、释放一个局部引用有两种方式:
- 本地方法执行完毕后VM自动释放;
- 通过DeleteLocalRef手动释放;
VM会自动释放局部引用,为什么还需要手动释放呢?因为局部引用会阻止它所引用的对象被GC回收。
5.2 、 全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。由 NewGlobalRef 函数创建。
5.3、弱全局引用
- 与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 。
- 在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。
- 在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象;
6、JNI中的JNI_OnLoad方法
当我们调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数则调用。JNI_OnLoad会,告诉 VM 此 native 组件使用的 JNI 的版本。对应了Java版本,android中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6。
jint JNI_OnLoad(JavaVM* vm, void* reserved){
return JNI_VERSION_1_4;
}
7、动态注册
在此之前我们一直在jni中使用的Java_com_hellondk_NDKTool_test来进行与java方法的匹配,这种方式我们称之为静态注册。而动态注册则意味着方法名可以不用这么长了,在android aosp源码中就大量的使用了动态注册的形式。
#Java
public static native void dynomicRegisterTest(int a);
#C++
JavaVM *_vm;
//需要动态注册的方法数组
static const JNINativeMethod methods[] = {
{"dynomicRegisterTest", "(I)V", (void *) dynomicRegisterTest},
{"dynomicRegisterTest2", "(I)I", (int *) dynomicRegisterTest2},
{"dynomicRegisterTest3", "(I)I", (int *) dynomicRegisterTest3}
};
//需要动态注册native方法的类名
static const char *className = "com/youbesun/myapplication/NDKTool";
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
_vm = vm;
//获得JNIEnv(线程相关的)
JNIEnv *env = JNI_OK;
int r = vm->GetEnv((void **) &env, JNI_VERSION_1_6);
//小于0失败,等于0成功
if (r != JNI_OK) {
return JNI_ERR;
}
jclass registerClass = env->FindClass(className);
//注册Native
//参数3:方法数量
env->RegisterNatives(registerClass, methods, sizeof(methods) / sizeof(JNINativeMethod));
env->DeleteLocalRef(registerClass);
return JNI_VERSION_1_6;
}
8、native线程调用Java
native调用java需要使用JNIEnv这个结构体,而JNIEnv是由Jvm传入与线程相关的变量。
但是可以通过JavaVM的AttachCurrentThread方法来获取到当前线程中的JNIEnv指针。
//线程================
struct Context {
jobject cls;//注意你要穿全局变量
};
void *threadTask(void *args) {
JNIEnv *env;
jint i = _vm->AttachCurrentThread(&env, 0);
if (i != 0) {
return 0;
}
Context *context = static_cast<Context *>(args);
jclass cls = env->GetObjectClass(context->cls);
jmethodID methodId = env->GetMethodID(cls, "printf", "()V");
env->CallVoidMethod(context->cls, methodId);
delete (context);
context = 0;
_vm->DetachCurrentThread();
return 0;
}
extern "C" JNIEXPORT void JNICALL Java_com_hellondk_NDKTool_testThread(JNIEnv *env, jobject cls) {
pthread_t pid;//线程Id,句柄
Context *context = new Context;
context->cls = env->NewGlobalRef(cls);//注意你要穿全局变量
//启动线程
pthread_create(&pid, 0, threadTask, context);
}
总结
- 对于
JNI
特别是做音视频开发的,JNI
是必不可少的,因为需要和C/C++
交互,比如你想用FFmpeg
开发音视频,但是FFmpeg
只提供了C/C++
代码,所以这时候你就需要去编译FFmpeg
,使用FFmpeg
生成的动态库导入我们的项目,然后在使用JNI
去调用FFmpeg
提供的功能,所以JNI
只启动类似于中介者的方式去组织Java
和FFmpeg
交互; - 最后
JNI
和Android NDK
是没有关系的,Android NDK 就是一套工具集合,允许你使用C/C++
语言来实现应用程序的部分功能。
网友评论