NDK(二):JNI与Java回调以及静动态注册

上篇文章NDK(一):编写第一个JNI项目,讲到了怎样用Android Studio创建一个项目去编写JNI代码,接下来,就具体介绍JNI与Java之间的调用。
包括简单的参数传递回调,创建pthread线程,以及静动态注册

台风“山竹”的到来,导致哪里都去不了,待在家终于把这篇文章码完!

[TOC]

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 双浮点型数组

宏定义输出日志语句

1
2
3
4
5
// native-lib.cpp文件

#include <android/log.h>
// 宏定义jni的输出日志语句,方便使用
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,"StudyNDK",__VA_ARGS__);

参数介绍

extern “C”

指示编译器这部分代码按C语言进行编译

JNIEXPORT和JNICALL

JNIEXPORT 和 JNICALL,定义在jni_md.h头文件中,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数,在Java虚拟机加载的时候会链接对应的native方法。

JNIEXPORT:
  • 在 Windows 中,定义为

    1
    #define JNIEXPORT __declspec(dllexport)

    因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。

  • 在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为

    1
    define JNIEXPORT  __attribute__ ((visibility ("default")))
JNICALL:
  • 在Windows中定义为:_stdcall ,一种函数调用约定
  • 在类Unix中无定义,可以省略不加

Java调用Native

不传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// java文件
Log.i(TAG, "stringFromJNI() = "+stringFromJNI());

public native String stringFromJNI();

// native-lib.cpp
extern "C" JNIEXPORT jstring

JNICALL
Java_com_guidongyuan_studyndk_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
LOGI("Java调用native")
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

//output
Java调用native
stringFromJNI() = Hello from C++

传递变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// java文件
Log.i(TAG, "passValueToJNI() = "+passValueToJNI(100, "passToJNI"));

/** 传递基本数据类型和String类型参数给JNI */
public native String passValueToJNI(int intValue,String strValue);

// native-lib.cpp文件
extern "C"
JNIEXPORT jstring JNICALL
Java_com_guidongyuan_studyndk_MainActivity_passValueToJNI(JNIEnv *env, jobject instance,
jint intValue, jstring strValue_) {
LOGI("基本数据类型:intValue = %d\n", intValue);

const char *strValue = env->GetStringUTFChars(strValue_, 0);
LOGI("string数据类型:strValue = %s\n", strValue);

// 释放
env->ReleaseStringUTFChars(strValue_, strValue);
return env->NewStringUTF("passValueToJNI 回调");
}

// output
基本数据类型:intValue = 100
string数据类型:strValue = passToJNI
passValueToJNI() = passValueToJNI 回调

传递数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// java文件
int[] intArrays = new int[]{0, 1, 2};
String[] strArrays = new String[]{"zero","first","second"};
Log.i(TAG, "passArrayToJNI() = "+passArrayToJNI(intArrays, strArrays));
// 验证在JNI的修改是否有效
for (int i = 0;i < intArrays.length;i++){
Log.i(TAG, "修改后 int数据为:i = " + intArrays[i]);
}

/** 传递数组给JNI */
public native String passArrayToJNI(int[] intArray, String strArray[]);

// native-lib.cpp文件
extern "C"
JNIEXPORT jstring JNICALL
Java_com_guidongyuan_studyndk_MainActivity_passArrayToJNI(JNIEnv *env, jobject instance, jintArray intArray_, jobjectArray strArray) {

// 获得基本数据类型数组
// 参数intArray_类型为jintArray,可以看到是typedef _jintArray* jintArray;这样定义的
// 传递的为指针地址,指向数组首元素地址

// 获取数组长度
int intLength = env->GetArrayLength(intArray_);
// 如果为Boolean则调用GetBooleanArrayElements不同的对应
jint *intArray = env->GetIntArrayElements(intArray_, NULL);
for (int i = 0; i < intLength; ++i) {
LOGI("int数据为:i = %d ",*(intArray+i));
// 因为传递的为指针地址,所以,在这里进行修改,会影响到java代码中的值
*(intArray + i) = *(intArray + i) + 10;
}
// 释放
env->ReleaseIntArrayElements(intArray_, intArray, 0);

// 获得字符串类型数组
int strLength = env->GetArrayLength(strArray);
for (int i = 0; i < strLength; ++i) {
// object类型转成jstring类型
jstring str = static_cast<jstring>(env->GetObjectArrayElement(strArray, i));
// 需要转成char* 类型再输出,否则会出错
const char *c_str = const_cast<char *>(env->GetStringUTFChars(str, 0));
LOGI("string数据为 i = %s ",c_str);
env->ReleaseStringUTFChars(str, c_str);
}

return env->NewStringUTF("passArrayToJNI 回调");
}

// output
int数据为:i = 0
int数据为:i = 1
int数据为:i = 2
string数据为 i = zero
string数据为 i = first
string数据为 i = second
passArrayToJNI() = passArrayToJNI 回调
// 可以看到,传递到JNI被修改的参数有效了
修改后 int数据为:i = 10
修改后 int数据为:i = 11
修改后 int数据为:i = 12

传递对象

传递的对象,通过GetObjectClass()可以获取到对应的class对象,然后通过该class对象调用对象的成员属性,方法。这里先不介绍,放到Native调用Java章节再介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// java文件
Log.i(TAG, "passBeanToJNI = "+passBeanToJNI(new Bean()));

/** 传递对象 */
public native String passBeanToJNI(Bean bean);

// native-lib.cpp
extern "C"
JNIEXPORT jstring JNICALL
Java_com_guidongyuan_studyndk_MainActivity_passBeanToJNI(JNIEnv *env, jobject instance, jobject bean) {

// 传递对象
// 获取java对应的class对象
jclass beanClass = env->GetObjectClass(bean);
return env->NewStringUTF("passBeanToJNI 回调");
}

// output
passBeanToJNI = passBeanToJNI 回调

Native调用Java

基本数据类型签名

基本数据类型的签名采用一系列大写字母来表示, 如下表所示:

Java类型 签名
boolean Z
short S
float F
byte B
int I
double D
char C
long J
void V
引用类型 L + 全限定名 + ;
数组 [+类型签名

调用Java方法

Native调用Java的流程

  • GetObjectClass()获取jclass对象
  • GetMethodID()传入jclass对象、方法名称和方法参数数据类型签名,获取方法Id
  • CallVoidMethod()传入方法id和参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Java文件
public native void callJNI();

public void callFromJNI(){
Log.i(TAG, "callFromJNI");
}

public void callFromJNI(int i) {
Log.i(TAG, "callFromJNI i = "+i);
}

public void callFromJNI(String string) {
Log.i(TAG, "callFromJNI string = "+string);
}

public static void callStaticFromJNI(){
Log.i(TAG, "callStaticFromJNI");
}

// native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_callJNI(JNIEnv *env, jobject instance) {

// 获取jclass对象
jclass _jclass = env->GetObjectClass(instance);

// 调用无参方法
jmethodID methodId = env->GetMethodID(_jclass, "callFromJNI", "()V");
env->CallVoidMethod(instance, methodId);
// 调用int类型参数的方法
jmethodID methodWithIntId = env->GetMethodID(_jclass, "callFromJNI", "(I)V");
env->CallVoidMethod(instance, methodWithIntId, 100);
// 调用string类型参数的方法
jmethodID methodWithStringId = env->GetMethodID(_jclass, "callFromJNI", "(Ljava/lang/String;)V");
env->CallVoidMethod(instance, methodWithStringId, env->NewStringUTF("callJNI 回调"));

// 调用静态方法
jmethodID methodStaticId = env->GetStaticMethodID(_jclass, "callStaticFromJNI", "()V");
// 第一个参数,传入的为jclass对象,而不是jobject
env->CallStaticVoidMethod(_jclass, methodStaticId);
}

// output
callFromJNI
callFromJNI i = 100
callFromJNI string = callJNI 回调
callStaticFromJNI

调用Java变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Java文件
public int age = 18;
public String name = "yuan";
public static String school = "GDUT";

callJNI();
Log.i(TAG, "修改后 age = " +age + ",name = "+name + ",school = "+school);

// native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_callJNI(JNIEnv *env, jobject instance) {
// 调用int数据类型变量
jfieldID fieldIntId = env->GetFieldID(_jclass, "age", "I");
int ageInt = env->GetIntField(instance, fieldIntId);
LOGI("获取Java变量 age = %d ", ageInt);
// 修改变量内容
env->SetIntField(instance, fieldIntId, 19);

// 调用String数据类型变量
jfieldID fieldStrId = env->GetFieldID(_jclass, "name", "Ljava/lang/String;");
jstring nameStr = static_cast<jstring>(env->GetObjectField(instance, fieldStrId));
const char* nameStrChar = env->GetStringUTFChars(nameStr, 0);
LOGI("获取Java变量 name = %s ",nameStrChar);
// 修改string变量内容
env->SetObjectField(instance, fieldStrId, env->NewStringUTF("Change yuan"));

// 调用静态变量
jfieldID fieldStaticId = env->GetStaticFieldID(_jclass, "school", "Ljava/lang/String;");
jstring schoolStr = static_cast<jstring>(env->GetStaticObjectField(_jclass, fieldStaticId));
const char* schoolStrChar = env->GetStringUTFChars(schoolStr, 0);
LOGI("获取Java静态变量 school = %s ",schoolStrChar);
// 修改static变量内容
env->SetStaticObjectField(_jclass, fieldStaticId, env->NewStringUTF("Change GDUT"));
}

// output
获取Java变量 age = 18
获取Java变量 name = yuan
获取Java静态变量 school = GDUT
修改后 age = 19,name = Change yuan,school = Change GDUT

创建Java对象

上面介绍的,都是通过传递当前对象到native方法中,如果要使用其他对象,就需要自行创建了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Bean.java文件
package com.guidongyuan.studyndk;

public class Bean {
private static final String TAG = "StudyNDK";
private int i = 0;

public int getI() {
Log.i(TAG, "getI i = "+i);
return i;
}

public void setI(int i) {
Log.i(TAG, "setI i = "+i);
this.i = i;
}
}

// native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_callJNI(JNIEnv *env, jobject instance) {
// 创建对象,需要传递完整包路径
jclass beanClass = env->FindClass("com/guidongyuan/studyndk/Bean");
jmethodID constructMethodId = env->GetMethodID(beanClass, "<init>", "()V");
jobject beanObject = env->NewObject(beanClass, constructMethodId);

jmethodID getIId = env->GetMethodID(beanClass,"getI","()I");
jint iValue = env->CallIntMethod(beanObject,getIId);
LOGI("获取Java 变量 i = %d",iValue);

jmethodID setIId = env->GetMethodID(beanClass, "setI", "(I)V");
env->CallVoidMethod(beanObject, setIId, 1);
}

// output
getI i = 0
获取Java 变量 i = 0
setI i = 1

创建Native线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Java文件
createThread();

public native void createThread();

public void updateUI(){
Log.i(TAG, "updateUI");
}

// native-lib.cpp
JavaVM *_vm;
jobject _instance = 0;

// Java中执行完System.loadLibrary,就会自动调用该方法
int JNI_OnLoad(JavaVM *vm, void *re) {
LOGI("调用JNI_Onload");
_vm = vm;
return JNI_VERSION_1_6;
}

void *pthreaTask(void *args){
LOGI("pthreaTask");
JNIEnv *env = nullptr;
jint i = _vm->AttachCurrentThread(&env, 0);
if (i != JNI_OK){
return 0;
}
jclass _jclass = env->GetObjectClass(_instance);
jmethodID fieldId = env->GetMethodID(_jclass, "updateUI", "()V");
env->CallVoidMethod(_instance, fieldId);
_vm->DetachCurrentThread();
return 0;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_createThread(JNIEnv *env, jobject instance) {
// 把instant设置为全局引用
_instance = env->NewGlobalRef(instance);
pthread_t _pthread;
pthread_create(&_pthread, 0, pthreaTask, NULL);
// 避免线程方法还没有执行,该方法就先执行完毕,把instance释放掉了
pthread_join(_pthread, 0);
env->DeleteGlobalRef(_instance);
LOGI("createThread 执行结束");
}

// output
pthreaTask
updateUI
createThread 执行结束

JavaVM与JNIEnv

JNIEnv

JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的。JNIEnv的主要作用有两点:

  1. 调用Java的方法。
  2. 操作Java(获取Java中的变量和对象等等)

通过上面的代码实例,可以看到JNIEnv的作用,但是,在多线程中,因为不能跨进程,所以,需要通过JavaVM获取当前线程的JNIEnv。

JavaVM

JavaVM,是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。
通过JavaVM的AttachCurrentThread()函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。记得在线程退出前,要调用DetachCurrentThread()函数来释放资源。

JNI_OnLoad

怎样获取到JavaVM的引用呢?可以通过JNI_Onload函数。Java代码在调用System.loadLibrary()函数时, 内部就会去查找so中的JNI_OnLoad 函数,如果存在此函数则会调用。并且会传递JavaVM的引用,把其设置为全局引用就可以了。

1
2
3
4
5
6
7
8
9
// JNI_OnLoad会告诉 VM 此 native 组件使用的 JNI 版本
// 对应了Java版本,android中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6
// 如果使用JDK1.8,也有 JNI_VERSION_1_8
// 使用上,使用哪个都可以
JavaVM *_vm;
int JNI_OnLoad(JavaVM *vm,void *re){
_vm = vm;
return JNI_VERSION_1_6;
}

pthread_create参数传递

上面实例代码中,把instance传递到其他线程中,是通过声明为一个全局引用,pthread_create()方法中,发现第4个参数为传递的变量,在这里花费很多时间,一直在探索为啥把instance传递进去,确实无效的。测试结果发现,如果是基本数据类型,是可以传递的,jobject确实没办法。具体原因暂时一直没找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Java文件

// native-lib.cpp
void *pthreaTask(void *args){
LOGI("pthreaTask");
JNIEnv *env;
jint i = _vm->AttachCurrentThread(&env, 0);
if (i != JNI_OK){
return 0;
}
jint value = *((jint *)args);
LOGI("jint 值 value = %d",value);
_vm->DetachCurrentThread();
return 0;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_createThread(JNIEnv *env, jobject instance) {

// 传递int数据类型
int i = 100;
int* pInt = &i;
pthread_t _pthread;
pthread_create(&_pthread, 0, pthreaTask, pInt);
pthread_join(_pthread, 0);
}

// output
jint 值 value = 100

静态注册和动态注册

静态注册:在此之前我们一直在jni中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 来进行与java方法的匹配,这种方式我们称之为静态注册。

动态注册:动态注册则意味着方法名可以不用这么长了,在android aosp源码中就大量的使用了动态注册的形式。

不过在Android Studio中,写了native方法后可以自动添加静态方法,也不太需要去写特别上的方法,所以,就看使用的取舍了。

上面介绍的,通过JNI_OnLoad()可以获取到JavaVM的引用,该方法在动态注册中也使用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Java文件
public native void dynamicRegister();
public native static String dynamicRegisterStatic(int i);

// native-lib.cpp
// 需要动态注册native方法的类名
// 混淆的时候,要注意,不能被混淆,否则会失败
static const char* mClassName = "com/guidongyuan/studyndk/MainActivity";

void dynamicRegisterNative(JNIEnv *env, jobject instance){
LOGI("dynamicRegisterNative 动态注册");
}

jstring dynamicRegisterNativeStatic(JNIEnv *env, jobject instance, jint i){
return env->NewStringUTF("dynamicRegisterNativeStatic 动态注册");
}

// 需要动态注册的方法数组
// {"java本地方法名","签名",java方法对应jni中的方法名}
static const JNINativeMethod method[] = {
{"dynamicRegister", "()V", (void *)dynamicRegisterNative},
{"dynamicRegisterStatic", "(I)Ljava/lang/String;", (jstring *)dynamicRegisterNativeStatic}
};

int JNI_OnLoad(JavaVM *vm, void *re) {
LOGI("调用JNI_Onload");
_vm = vm;

JNIEnv *env = nullptr;
int i = vm->AttachCurrentThread(&env, 0);
if (i != JNI_OK) {
return 0;
}
jclass mainActivityCls = env->FindClass(mClassName);
// 传递方法数组的名称和数目
// 注册 如果小于0则注册失败
i = env->RegisterNatives(mainActivityCls,method,sizeof(method)/ sizeof(JNINativeMethod));
if(i != JNI_OK )
{
return -1;
}

return JNI_VERSION_1_6;
}

完整代码

  • 上面的实例代码都上传到github地址,可以进行详细查看

参考资料

公众号:亦袁非猿

欢迎关注,交流学习