JNI 一

JNI概述

JNI(Java Native Interface)意为JAVA本地调用,它允许Java代码和其他语言写的代码进行交互,简单的说,一种在Java虚拟机控制下执行代码的标准机制。
Android NDK(Native Development Kit )是一套工具集合,允许你用像C/C++语言那样实现应用程序的一部分。

JNI和NDK的区别?

  • 从工具上说,NDK其实多了一个把.so和.apk打包的工具,而JNI开发并没有打包,只是把.so文件放到文件系统的特定位置。
  • 从编译库说,NDK开发C/C++只能能使用NDK自带的有限的头文件,而使用JNI则可以使用文件系统中带的头文件。
  • 从编写方式说,它们一样。

JNI 元素

JNI组织结构

JNI函数表的组成就像C++的虚函数表,虚拟机可以运行多张函数表。
JNI接口指针仅在当前线程中起作用,指针不能从一个线程进入另一个线程,但可以在不同的线程中调用本地方法。

github

原始数据

jobject 对象引用类型
github

Java类型 本地类型(JNI) 描述
boolean(布尔型) jboolean 无符号8个比特
byte(字节型) jbyte 有符号8个比特
char(字符型) jchar 无符号16个比特
short(短整型) jshort 有符号16个比特
int(整型) jint 有符号32个比特
long(长整型) jlong 有符号64个比特
float(浮点型) jfloat 32个比特
double(双精度浮点型) jdouble 64个比特
void(空型) void N/A

函数操作

函数 Java 数组类型 本地类型 说明
GetBooleanArrayElements jbooleanArray jboolean ReleaseBooleanArrayElements 释放
GetByteArrayElements jbyteArray jbyte ReleaseByteArrayElements 释放
GetCharArrayElements jcharArray jchar ReleaseShortArrayElements 释放
GetShortArrayElements jshortArray jshort ReleaseBooleanArrayElements 释放
GetIntArrayElements jintArray jint ReleaseIntArrayElements 释放
GetLongArrayElements jlongArray jlong ReleaseLongArrayElements 释放
GetFloatArrayElements jfloatArray jfloat ReleaseFloatArrayElements 释放
GetDoubleArrayElements jdoubleArray jdouble ReleaseDoubleArrayElements 释放
GetObjectArrayElement 自定义对象 object
SetObjectArrayElement 自定义对象 object
GetArrayLength 获取数组大小
New<Type>Array 创建一个指定长度的原始数据类型的数组
GetPrimitiveArrayCritical 得到指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。
ReleasePrimitiveArrayCritical 释放指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。
NewStringUTF jstring类型的方法转换
GetStringUTFChars jstring类型的方法转换
DefineClass 从原始类数据的缓冲区中加载类
FindClass 该函数用于加载本地定义的类。它将搜索由CLASSPATH 环境变量为具有指定名称的类所指定的目录和 zip文件
GetObjectClass 通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为NULL,否则获取的class肯定返回也为NULL
GetSuperclass 获取父类或者说超类 。 如果 clazz 代表类class而非类 object,则该函数返回由 clazz 所指定的类的超类。 如果 clazz指定类 object 或代表某个接口,则该函数返回NULL
IsAssignableFrom 确定 clazz1 的对象是否可安全地强制转换为clazz2
Throw 抛出 java.lang.Throwable 对象
ThrowNew 利用指定类的消息(由 message 指定)构造异常对象并抛出该异常
ExceptionOccurred 确定是否某个异常正被抛出。在平台相关代码调用 ExceptionClear() 或 Java 代码处理该异常前,异常将始终保持抛出状态
ExceptionDescribe 将异常及堆栈的回溯输出到系统错误报告信道(例如 stderr)。该例程可便利调试操作
ExceptionClear 清除当前抛出的任何异常。如果当前无异常,则此例程不产生任何效果
FatalError 抛出致命错误并且不希望虚拟机进行修复。该函数无返回值
NewGlobalRef 创建 obj 参数所引用对象的新全局引用。obj 参数既可以是全局引用,也可以是局部引用。全局引用通过调用DeleteGlobalRef() 来显式撤消。
DeleteGlobalRef 删除 globalRef 所指向的全局引用
DeleteLocalRef 删除 localRef所指向的局部引用
AllocObject 分配新 Java 对象而不调用该对象的任何构造函数。返回该对象的引用。clazz 参数务必不要引用数组类。
getObjectClass 返回对象的类
IsSameObject 测试两个引用是否引用同一 Java 对象
NewString 利用 Unicode 字符数组构造新的 java.lang.String 对象
GetStringLength 返回 Java 字符串的长度(Unicode 字符数)
GetStringChars 返回指向字符串的 Unicode 字符数组的指针。该指针在调用 ReleaseStringchars() 前一直有效
ReleaseStringChars 通知虚拟机平台相关代码无需再访问 chars。参数chars 是一个指针,可通过 GetStringChars() 从 string 获得
NewStringUTF 利用 UTF-8 字符数组构造新 java.lang.String 对象
GetStringUTFLength 以字节为单位返回字符串的 UTF-8 长度
GetStringUTFChars 返回指向字符串的 UTF-8 字符数组的指针。该数组在被ReleaseStringUTFChars() 释放前将一直有效
ReleaseStringUTFChars 通知虚拟机平台相关代码无需再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars() 获得
NewObjectArray 构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement
Set<PrimitiveType>ArrayRegion 将基本类型数组的某一区域从缓冲区中复制回来的一组函数
GetFieldID 返回类的实例(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的Get<type>FieldSet<type>Field系列使用域 ID 检索对象域。GetFieldID() 不能用于获取数组的长度域。应使用GetArrayLength()。
Get<type>Field 该访问器例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用GetFieldID() 而得到的域 ID 指定。
Set<type>Field 该访问器例程系列设置对象的实例(非静态)属性的值。要访问的属性由通过调用SetFieldID() 而得到的属性 ID指定。
GetStaticFieldID GetStatic<type>Field SetStatic<type>Field 同上,只不过是静态属性。
GetMethodID 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 作为方法名,同时将void (V) 作为返回类型。
CallVoidMethod
CallObjectMethod
CallBooleanMethod
CallByteMethod
CallCharMethod
CallShortMethod
CallIntMethod
CallLongMethod
CallFloatMethod
CallDoubleMethod
GetStaticMethodID 调用静态方法
Call<type>Method
RegisterNatives 向 clazz 参数指定的类注册本地方法。methods 参数将指定 JNINativeMethod 结构的数组,其中包含本地方法的名称、签名和函数指针。nMethods 参数将指定数组中的本地方法数。
UnregisterNatives 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。

域描述符

Java 语言
Z boolean
B byte
C char
S short
I int
J long
F float
D double
  • 引用类型则为 L + 该类型类描述符。
  • 数组,其为 : [ + 其类型的域描述符。
  • 多维数组则是 n个[ +该类型的域描述符, N代表的是几维数组。
    String类型的域描述符为 Ljava/lang/String;    

    [ + 其类型的域描述符 + ;
    int[] 其描述符为[I
    float[] 其描述符为[F
    String[] 其描述符为[Ljava/lang/String;
    Object[]类型的域描述符为[Ljava/lang/Object;
    int [][] 其描述符为[[I
    float[][] 其描述符为[[F

将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回类型描述符。对于没有返回值的,用V(表示void型)表示。

Java层方法                                               JNI函数签名  
String test () Ljava/lang/String;
int f (int i, Object object) (ILjava/lang/Object;)I
void set (byte[] bytes) ([B)V

JNIEnv与JavaVM

JNIEnv 概念 : 是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境 ;

JNIEnv 与 JavaVM : 注意区分这两个概念;

  • JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个;
  • JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv;

JNIEnv 体系结构
线程相关 : JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv。

JNIEnv 不能跨线程 :

  • 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
  • 本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此可以接受不同的 JNIEnv;

JNIEnv 结构 :
由上面的代码可以得出, JNIEnv 是一个指针, 指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针 数组, 这个数组中存放了大量的 JNI 函数指针, 这些指针指向了具体的 JNI 函数;

UTF-8编码
JNI使用改进的UTF-8字符串来表示不同的字符类型。Java使用UTF-16编码。UTF-8编码主要使用于C语言,因为它的编码用\u000表示为0xc0,而不是通常的0×00。非空ASCII字符改进后的字符串编码中可以用一个字节表示。

异常
JNI允许用户使用Java异常处理。大部分JNI方法会返回错误代码但本身并不会报出异常。因此,很有必要在代码本身进行处理,将异常抛给Java。在JNI内部,首先会检查调用函数返回的错误代码,之后会调用ExpectOccurred()返回一个错误对象。

JNI函数实战

so的入口函数

JNI_OnLoad()JNI_OnUnload()

当VM(Virtual Machine)执行到System.loadLibrary()函数时,首先会去执行C组件里的JNI_OnLoad()函数。

  • 告诉VM此C组件使用那一个JNI版本。如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。
  • 由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。

返回值

返回字符串

public native String print();


jstring res = env->NewStringUTF("hello");
return res;

返回数组

public native List<String> addList();

jobjectArray pArray = env->NewObjectArray(4, env->FindClass("java/lang/String"), 0);
for (int i = 0; i < 4; ++i) {
jstring s = env->NewStringUTF("A");
env->SetObjectArrayElement(pArray, i, s);
}

返回自定义对象

public native TestInfo createTest(int data1, String data2);

// 构建TestInfo对象
jclass pJclass = env->FindClass("org/xx/xxxx/test/entity/TestInfo");
jmethodID method_init = env->GetMethodID(pJclass, "<init>", "()V");
jfieldID nameField = env->GetFieldID(pJclass, "name", "Ljava/lang/String;");
jfieldID dataField = env->GetFieldID(pJclass, "data", "Ljava/lang/Integer;");
jobject obj = env->NewObject(pJclass, method_init);

// 构建Integer对象及其字段
jclass integerClass = env->FindClass("java/lang/Integer");
jmethodID integerMethod = env->GetMethodID(integerClass, "<init>", "(I)V");
jobject integerObj = env->NewObject(integerClass, integerMethod, d1);
env->SetObjectField(obj, dataField, integerObj);

// 构建String字段
env->SetObjectField(obj, nameField, env->NewStringUTF("Data"));
return obj;

返回自定义对象的数组

public native List<TestInfo> addList(int data1, String data2);

jclass arrClass = env->FindClass("Ljava/util/ArrayList;");
jmethodID arrMethod = env->GetMethodID(arrClass, "<init>", "()V");
jobject arrObj = env->NewObject(arrClass, arrMethod);

jmethodID addMethod = env->GetMethodID(arrClass, "add", "(Ljava/lang/Object;)Z");
for (int i = 0; i < 3; ++i) {
jobject item = createItem(env, d1, d2);
env->CallBooleanMethod(arrObj, addMethod, item);
}

return arrObj;

这里的createItem是对返回自定义对象功能的一个封装函数:

jobject createItem(JNIEnv *env, jint d1, jstring d2) {
jclass pJclass = env->FindClass("org/hy/modules/test/entity/TestInfo");
jmethodID method_init = env->GetMethodID(pJclass, "<init>", "()V");
jfieldID nameField = env->GetFieldID(pJclass, "name", "Ljava/lang/String;");
jfieldID dataField = env->GetFieldID(pJclass, "data", "Ljava/lang/Integer;");

jobject obj = env->NewObject(pJclass, method_init);
env->SetObjectField(obj, nameField, env->NewStringUTF("Data"));

jclass integerClass = env->FindClass("java/lang/Integer");
jmethodID integerMethod = env->GetMethodID(integerClass, "<init>", "(I)V");
jobject integerObj = env->NewObject(integerClass, integerMethod, d1);
env->SetObjectField(obj, dataField, integerObj);
return obj;
}

操作Java层的类

public native void testThisObjField(TestInfo info);

jclass pJclass = env->GetObjectClass(obj);
jfieldID fieldId = env->GetFieldID(pJclass, "name", "Ljava/lang/String;");

// 获得该属性的值
jstring data = (jstring)env->GetObjectField(obj, fieldId);
const char *datas = env->GetStringUTFChars(data, NULL);
// 释放局部引
env->ReleaseStringUTFChars(data, datas);

char * new_data = "aaabbb";
jstring pJstring = env->NewStringUTF(new_data);
env->SetObjectField(obj, fieldId, pJstring);

回调Java层方法

这里分为两种

  1. 在jni中找到类,创建对象,调用该对象函数
  2. 将接口为参数给jni函数,jni调用该接口中的函数,完成接口回调功能

首先是第一个:

public native void testCallBackToMethod();

jclass pJclass = env->FindClass("org/hy/modules/jni/PrintNative");
jmethodID pJmethodId = env->GetMethodID(pJclass, "<init>", "()V");
jobject pJobject = env->NewObject(pJclass, pJmethodId);

jmethodID testMethodId = env->GetMethodID(pJclass, "test", "(I)I");
jint i = env->CallIntMethod(pJobject, testMethodId, 2);
printf("%d", i);

这里只是简单调用了对象中的函数

下面就是接口回调函数

public native void testCallBack(CallBack callBack);

jclass callback_cls = env->GetObjectClass(callback);
jmethodID callback_mth = env->GetMethodID(callback_cls, "callback", "(Ljava/lang/String;)V");
env->CallVoidMethod(callback, callback_mth, env->NewStringUTF("aabbcc"));

可以看到她不需要找类,创建新的对象,直接找到需要调用的函数名称并调用即可。

注意,上面是在主进程中使用,如果在jni中的线程中单独使用,则需要特殊处理

JavaVM *g_VM;
env->GetJavaVM(&g_VM);

...

JNIEnv *env;
//获取当前native线程是否有没有被附加到jvm环境中
int getEnvStat = g_VM->GetEnv((void **) &env,JNI_VERSION_1_6);

if (getEnvStat == JNI_EDETACHED) {
// 关联线程
if (g_VM->AttachCurrentThread(&env, NULL) != 0) {
return;
}
mNeedDetach = JNI_TRUE;
}
//通过jcallback 获取到要回调的类
jclass javaClass = env->GetObjectClass(callback);

g_VM->DetachCurrentThread();

jbytearray转c++byte数组

jbyte * arrayBody = env->GetByteArrayElements(data,0);
jsize theArrayLengthJ = env->GetArrayLength(data);
BYTE * starter = (BYTE *)arrayBody;

jbyteArray 转 c++中的BYTE[]

jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);
jsize oldsize = env->GetArrayLength(strIn);
BYTE* bytearr = (BYTE*)olddata;
int len = (int)oldsize;

C++中的BYTE[]转jbyteArray

jbyte *by = (jbyte*)pData;
jbyteArray jarray = env->NewByteArray(nOutSize);
env->SetByteArrayRegin(jarray, 0, nOutSize, by);

jbyteArray 转 char *

char* data = (char*)env->GetByteArrayElements(strIn, 0);

char* 转jstring

jstring str = env->GetStringUTFChars(char_data, NULL);

jstring 转char*

const char *char_data = env->GetStringUTFChars(jstring_data, NULL);