JNI全称Java Native Interface ,主要目的是用于实现与其他语言交互而使用,我们遇到比较多应该是C/C++的使用,透过java调用C/C++中的函数,反过来也可以从C/C++回调java中的方法,实现双向交互。当然JNI也存在着许多的缺点,一旦程序使用了JNI的技术,那么也将失去了平台的可移植性,使用不当容易造成内存泄露。那为什么还要使用JNI?我的理解:
1. C/C++已经有许多优秀的开发库,如opencv、ffmpeg等;
2. 在处理图像数据等复杂的大量计算下,C/C++的运行效率比Java高的多;
3. .jar文件容易被反编译,.so文件不易被反编译;
一、JNI的基本类型
C/C++ | JNI | Java | bit |
typedef unsigned char | jboolean | boolean | 8 |
typedef signed char | jbyte | byte | 8 |
typedef unsigned short | jchar | char | 16 |
typedef short | jshort | short | 16 |
typedef long | jlong | long | 64 |
typedef float | jfloat | float | 32 |
typedef double | jdouble | double | 64 |
typedef int | jint | int | 32 |
二、JNI数组类型及其它
JNI | JAVA |
jobject | Object |
jclass | Class |
jstring | String |
jobjectArray | Object[] |
jbyteArray | byte[] |
jcharArray | char[] |
jshortArray | short[] |
jintArray | int[] |
jlongArray | long[] |
jfloatArray | float[] |
jdoubleArray | double[] |
jbooleanArray | boolean[] |
jthrowable | Throwable |
三、JNI中的重要的数据结构JavaVM、JNIEnv
在C中JavaVM的数据结构如下:
typedef const struct JNIInvokeInterface* JavaVM;
struct JNIInvokeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
在C++中定义的JavaVM其实也多少的区别,仅仅是在JNIInvokeInterface外包一层包裹作为其中的一个元素,调用同样的函数全都转到调用JNIInvokeInterface中,那么接下的JNIEnv也一样道理。JNIInvokeInterface结构体中,主要声明了几个JNI使用的关键函数,如GetEnv,AttchCurrentThread,值得注意的是JNIEnv是一个线程相关变量,就是说在不同的线程当中的JNIEnv都是不同,相互独立的,而JavaVM则是描述JVM的变量,一个JVM进程中仅有一个JavaVM,所以我们可以将JavaVM作为全局变量使用,当我们需要使用JNIEnv重JNI中开启的线程回调我们Java程序方法时,我们则需要使用JavaVM获取当前的环境变量JNIEnv,调用AttachCurrentThread函数添加到当前线程中。
在C中JNIEnv的数据结构如下:
typedef const struct JNINativeInterface* JNIEnv;
struct JNINativeInterface { //这里仅列出常用部分,每一种类型都有相应的定义
...
jclass (*FindClass)(JNIEnv*, const char*);
jobject (*NewGlobalRef)(JNIEnv*, jobject);
void (*DeleteGlobalRef)(JNIEnv*, jobject);
void (*DeleteLocalRef)(JNIEnv*, jobject);
jobject (*NewLocalRef)(JNIEnv*, jobject);
jobject (*NewObject)(JNIEnv*, jclass, jmethodID, ...);
jstring (*NewStringUTF)(JNIEnv*, const char*);
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
jintArray (*NewIntArray)(JNIEnv*, jsize);
void (*ReleaseIntArrayElements)(JNIEnv*, jintArray, jint*, jint);
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jint (*GetIntField)(JNIEnv*, jobject, jfieldID);
void (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);
jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char*, const char*);
jint (*GetStaticIntField)(JNIEnv*, jobject, jfieldID);
void (*SetStaticIntField)(JNIEnv*, jobject, jfieldID, jint);
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
jint (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...);
jint (*GetJavaVM)(JNIEnv*, JavaVM**);
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
jint (*UnregisterNatives)(JNIEnv*, jclass);
...//JNI对异常处理部分后边介绍
}
最后补充两个关键的函数声明与一个数据结构:
JNIEXPORT jnit JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT jnit JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);
其中JNI_OnLoad是在链接库加载完成时调用,JNI_OnUnLoad在链接库被移除时调用。
在C中JNINativeMethod的数据结构如下:
typedef struct {
const char* name; //java端的方法名
const char* signature; //java端的方法名的签名,后边会详细叙述
void* fnPtr; //jni端的函数指针
} JNINativeMethod;
四、Java如何调用JNI函数
1. 静态注册
定义一个Java类,需要使用JNI的函数必须声明为native类型,如public native String display();
package org.penguin.demo
public class Test{
static{ System.loadLibrary("test");} // 加载库文件 libtest.so
public native String display();
}
创建jni目录,定义c/c++文件实现display函数,如定义文件test.cpp
#include <jni.h> //这个是必须的
#ifdef __cplusplus__
extern "c" {
#endif
jstring Java_org_penguin_demo_Test_display(JNIEnv *env , jobject thiz)
{
return env->NewStringUTF("test");
// C语言 return (*env)->NewStringUTF(env, test);
}
#ifdef __cplusplus__
}
#endif
定义编译脚本Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := \
test.cpp
include $(BUILD_SHARED_LIBRARY)
当我们调用System.loadLibrary("test");后,调用display函数,那么它将会找一个叫Java_[全限定类名,其中"/"转为"_"的格式],我们可以看到这种方式,函数名字非常的长,那么动态注册的方式。
2. 动态注册
这里我们只需要将test.cpp文件内容修改一下即可
#include <jni.h> //这个是必须的
#include<stddef.h>
#ifdef __cplusplus__
extern "C" {
#endif
jstring jni_display(JNIEnv *env , jobject thiz)
{
return env->NewStringUTF("test");
}
//方法签名 “(参数类型签名1参数类型签名2...参数类型签名n)返回值类型签名”
static JNINativeMethod gMethods[] =
{
{ "display" , "()Ljava/lang/String;" , (void*)jni_display },
}
static int registerNativeMethods(JNIEnv *env ,const char * className,
JNINativeMethod* getMethods , int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if(clazz == NULL)return JNI_FALSE;
if( env->RegisterNatives(clazz , getMethos , numMethos ) < 0 )
{
return JNI_FALSE;
}
return JNI_TRUE;
}
jint JNI_OnLoad(JavaVM *vm , void * reserved)
{
jint result = JNI_ERR;
JNIEnv *env = NULL;
if( vm->GetEnv( (void**)&env , JNI_VERSION_1_4 ) != JNI_OK ) return result;
if( registerNativeMethods( env , "org/penguin/demo/Test" , gMethods ,
sizeof(gMethods ) / sizeof(gMethods [0])) != JNI_TRUE ) {
return result;
}
return JNI_VERSION_1_4;
}
#ifdef __cplusplus__
}
#endif
当我们Java端调用System.loadLibray("test"); 后,将会回调JNI_OnLoad函数,我们就可以在此处注册函数,或是初始化某些东西,我们可以通过调用JNIEnv中的函数RegisterNatives来注册函数Jni函数映射。
3. 关于方法描述的签名规则
3.1 格式:(参数类型签名1参数类型签名2...参数类型签名n)返回值类型签名,例:在Java中有这么一个方法String display(String name, int what){...},那么其相应的方法签名如:(Ljava/lang/String;I)Ljava/lang/String;
3.2 关于FastJNI:方法签名增加“!”区分,!(Ljava/lang/String;I)Ljava/lang/String;,需要注意的是FastJNI的函数里的处理不应依赖其它方法,处理不当容易引起ART死锁现象。
Java 类型 | 签名 | Java 类型 | 签名 |
boolean | Z | String | Ljava/lang/String; |
byte | B | Object | Ljava/lang/Object; |
char | C | int[] | [I |
long | J | String | [Ljava/lang/String; |
float | F | Object[] | [java/lang/Object; |
double | D | int[][] | [[I |
short | S | Object[][] | [[Ljava/lang/Object; |
int | I | 类 | L全限定类名; |
数组 | [数据类型签名 |
|
|
注:还可以通过javap -s -p xxx.class 获取方法签名
五、JNI如何回调Java函的方法
在JNI中有个数据结构JNIEnv的环境变量,它可以帮我们调用Java层的方法,具体操作如下
1. 同样我们Java中定义一个类为Test.java
package org.penguin.demo
public class Test {
static { System.loadLibrary("test"); }
public native void init();
public void onCompleted(){ }
}
2. 同样在jni目录下定义test.cpp实现native方法
#include <jni.h>
#include<stddef.h>
#ifdef __cplusplus__
extren "C" {
#endif
void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
jclass clazz = env->FindClass("org/penguin/demo/Test"); //查找Test这个类
if( clazz == NULL ) return;
//查找这个方法
jmethodID onCompletedID = env->GetMethodID(clazz , "onCompleted", "()V");
if( jmethodID == NULL) return;
env->CallVoidMethod( thiz, onCompletedID );
// 这里是不是觉得和反射很像
}
#ifdef __cplusplus__
}
#endif
在这里当我调用在java端调用init后,将会init()---> Java_org_penguin_demo_Test_init(..)-->onCompleted()
六、如何在JNI上打印LOG
在第三方apk开发中与直接在android源码环境下的开发库是存在着差异的,在编译器中使用NDK是没有utils/Log.h这么的一个头文件的,那么在编译中如何打印LOG,在三方apk开发中可以导入android/log.h这个头文件。
在jni目录下的Android.mk中添加LOCAL_LDLIBS := -llog
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := test.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
在jni目录下新建文件test_log.h
#include <android/log.h>
#define LOG_TAG
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)
test.cpp 中调用,这里的Java类就沿用前面模块的吧
#include <jni.h>
#include <test_log.h>
#define LOG_TAG “Penguin”
#ifdef __cplusplus__
extren “C” {
#endif
namespace {
void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
LOGW(“Test init fail ! %s%s” , “jni” , “warn” );
}
}
#ifdef __cplusplus__
}
#endif
这时运行程序,我们adb logcat -s Penguin就成捕捉到LOG了。
七、如何在JNI中持有Java对象
有些时候,我们在jni层需要开启线程做耗时的操作,这时候我们又怎么知道这个操作跑完了,这时候我们就需要通过回调Java层的方法来告诉Java层,程序已经准备好了。
在Java层同样有类Test.java如下:
package org.penguin.demo;
import android.util.Log;
public class Test {
static{
System.loadLibrary("test");
}
public native void init();
public void jniCallback(){
Log.d("Penguin","callback");
}
public void onCompleted(){
Log.d("Penguin","onCompleted");
}
}
在JNI层的test.cpp如下:
#include <jni.h>
#include <stddef.h> //定义NULL的头文件
#include <pthread.h> //linux线程的头文件
#include <unistd.h> //sleep的头文件
#include <test_log.h>//沿用前边定义的
#define LOG_TAG "Penguin"
#ifdef __cplusplus
extern "C" {
#endif
namespace android {
static struct fields_t
{
jmethodID mCallback; //保存回调方法的id
jclass mClassTest; //保存被回调的类
jobject globalWeakRef; //保存Java传入对象
} fields;
static JavaVM * gJavaVM; //保存Java虚拟机
void *native_exec(void *arg) //线程的方法体
{
LOGW("native_exec");
JNIEnv *env;
//gJavaVM->GetEnv((void**)&env,JNI_VERSION_1_4); //这里不能用这个方法
gJavaVM->AttachCurrentThread(&env,NULL); //将线程添加到Java环境中
if(env == NULL)return NULL;
int i = 0;
while(i < 10){
env->CallVoidMethod(fields.globalWeakRef,fields.mCallback);
sleep(5); //每隔5s回调一次
i++;
}
gJavaVM->DetachCurrentThread();
}
void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
env->GetJavaVM(&gJavaVM); //保存JavaVM,在三方apk开发才这么做,android
//源码环境下可以直接通过AndroidRuntime::getJNIEnv()来取得可用的env
fields.mClassTest = env->FindClass("org/penguin/demo/Test");
if(fields.mClassTest == NULL)return;
jmethodID onCompletedID =
env->GetMethodID(fields.mClassTest,"onCompleted","()V");
if(onCompletedID == NULL)return;
LOGW("CallVoidMethod");
env->CallVoidMethod(thiz,onCompletedID);
//获取和保存要回调的Java方法
fields.mCallback = env->GetMethodID(fields.mClassTest,"jniCallback","()V");
if(fields.mCallback == NULL)return;
fields.globalWeakRef = env->NewGlobalRef(thiz);//保存传入的java对象
LOGW("pthread_create");
pthread_t thid; //android源码环境下直接AndroidRuntime::createJavaThread(..);
pthread_create(&thid,NULL,native_exec,NULL); //启动一个线程
}
}
#ifdef __cplusplus
}
#endif
为什么在jni中的线程里不能直接JavaVM->GetEnv((void**)&env,JNI_VERSION_1_4);
因为JNIEnv是一个与线程相关的变量,就是说每一个不同的线程中JNIEnv都不同,相互独立的,而JavaVM描述的是虚拟机,一个虚拟机进程中仅有一个JavaVM,值得注意的是在android里,每一个应用进程都创建了一个自己的虚拟机,所以这个JavaVM可以在一个进程里全局使用,还有一点是JNIEnv不在任意线程中使用,必须是java线程,或者是让已有的native线程通过调用AttachCurrentThread添加到java环境中。
这里建议传入的java对象是个弱引用fields.globalWeakRef = env->NewGlobalRef(thiz); ,如init函数改为init(Test test);相应的JNI函数里的处理thiz改为test这个jobject对象。
在android源码环境下有更好封装,android_runtime/AndroidRuntime.h就直接通过AndroidRuntime::getJNIEnv()取得可用的JNIEnv指针,也可以直接创建一个Java线程,通过AndroidRuntime::createJavaThread(...)这个方法。
八、在Java中持久化保存JNI的指针地址
一般的,我们会将JNI中创建的持久引用保存到Java层中,通过在Java类中声明一个成员变量,然后在JNI层查找到该变量,往变量注入JNI引用地址,需要用到时,重新在JNI层通过获取回这个变量的值进行类型强转来取回之前的JNI引用,这是一个在android系统上的一个惯用技巧。
在Java部分的Test类中
package org.penguin.demo;
public class Test {
static {
System.loadLibrary(“test”);
}
private long nNativeContext;
public native void init();
public native void start();
}
在JNI部分的test.cpp这么定义
#include <jni.h>
#include <stddef.h>
#include <test_log.h>
#define LOG_TAG "Penguin"
#ifdef __cplusplus
extern "C" {
#endif
namespace android {
static jfieldID nNativeContext; //用于保留Java的变量
class Test {
public:
void jniStart()
{
LOGW("jniStart");
}
};
void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
LOGW("init");
Test *test = new Test();
jclass clazz = env->FindClass("org/penguin/demo/Test");
//取得Java层中nNativeContext成员变量的ID
nNativeContext = env->GetFieldID(clazz,"nNativeContext","J");
//将JNI Test对象地址写入到nNativeContext
env->SetLongField(thiz,nNativeContext,(jlong)test);
}
void Java_org_penguin_demo_Test_start(JNIEnv *env , jobject thiz)
{
LOGW("start");
//从Java层中取回刚被初始化的test对象
Test *test = (Test*)env->GetLongField(thiz,nNativeContext);
test->jniStart();
}
}
#ifdef __cplusplus
}
#endif
还有一点值得注意的是,为什么我要使用long类型而不用int型来保存JNI的指针,在32位处理器中long和int同占32位,而在64位处理器long型占64位,int型占32位,在64位的机器中保存指针就要使用长整型来保存,这时候就不是用int型了。