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

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

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

宏定义输出日志语句

// native-lib.cpp文件

#include 
// 宏定义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 中,定义为

    #define JNIEXPORT __declspec(dllexport)
    

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

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

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

Java调用Native

不传递参数

// 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++

传递变量

// 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 回调

传递数组

// 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(env->GetObjectArrayElement(strArray, i));
        // 需要转成char* 类型再输出,否则会出错
        const char *c_str = const_cast(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章节再介绍。

// 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和参数
// 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变量

// 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(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(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方法中,如果要使用其他对象,就需要自行创建了。

// 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, "", "()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线程

// 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的引用,把其设置为全局引用就可以了。

// 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确实没办法。具体原因暂时一直没找到。

// 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的引用,该方法在动态注册中也使用到。

// 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地址,可以进行详细查看

参考资料

  • Android JNI编程—JNI基础

  • googlesamples/android-ndk

    google官方ndk的demo,基本覆盖了各种jni的写法,强烈推荐参考的dmeo

  • JNI完全指南(十)——JavaVM与JNIEnv

    对JavaVM与JNIEnv介绍得比较详细的一篇文章

  • Android深入理解JNI(二)类型转换、方法签名和JNIEnv

  • Android NDK — Native 线程 pthread

    详细介绍pthread的文章

NDK(二):JNI与Java回调以及静动态注册_第1张图片

你可能感兴趣的:(NDK(二):JNI与Java回调以及静动态注册)