一篇文章学会jni技术及核心用法

665 阅读4分钟

本文只针对对jni有了初步了解的同学,帮助其快速掌握jni的核心用法

本文涉及到的源码:github.com/bytebitx/ND…

JNI中重要的是三个表

jni也有基本的数据类型和引用类型,和java的基本数据类型及引用对应如下:

基本数据类型映射表
java类型jni类型描述
booleanjboolean无符号8位
bytejbyte有符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat32位
doublejdouble64位
引用类型映射表
java引用类型jni类型
All objectsjobject
java.lang.Classjclass
java.lang.Stringjstring
java.lang.Throwablejthrowable
Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
char[]jcharArray
short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray
方法签名
java类型字符
voidV
booleanZ
intI
longJ
floatF
doubleD
byteB
charC
shortS
int[][I (数组以‘[’开始)
StringLjava/lang/String; (记得要加分号)
Object[][java/lang/object;

JNI方法注册方式

jni分为静态注册和动态注册,Android Studio默认使用的是静态注册。 查找静态注册的jni方法,按照jni规范的命名规则进行查找,格式为Java_类路径_方法名

动态注册:

在Android中,当程序在Java层运行System.loadLibrary这行代码后,程序会去载入so文件。于此同时,产生一个Load事件,这个事件触发后,程序默认会在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行。因此开发者经常会在JNI_OnLoad中做一些初始化操作,动态注册就是在这里进行的,使用env->RegisterNatives(clazz, gMethods, numMethods)。

  • 参数1:Java对应的类。
  • 参数2:JNINativeMethod数组。
  • 参数3:JNINativeMethod数组的长度,也就是要注册的方法的个数。

JNINativeMethod是jni中定义的一个结构体

typedef struct {
    const char* name;  // 函数名称
    const char* signature; // 函数签名
    void*       fnPtr; // 函数具体实现的指针
} JNINativeMethod;

案例

MainActivity中有三个方法,如下:

external fun accessBasicVar(l: Long, b: Boolean, s: String): ArrayList<Any>
external fun accessReferenceVar(testCallBack: TestCallBack)
external fun accessMainInstance(mainActivity: MainActivity)

现在需要动态注册accessBasicVar,accessReferenceVar,accessMainInstance这三个函数,应该怎么写呢?

JNINativeMethod jniNativeMethod[] = {
        {"accessBasicVar", "(JZLjava/lang/String;)Ljava/util/ArrayList;", (void*) accessBasicVar}, // 这里的名称和第一个参数的名称可以不一致
        {"accessReferenceVar", "(Lcom/bytebitx/ndkdemo/MainActivity$TestCallBack;)V", (void*) accessReferenceVar},
        {"accessMainInstance", "(Lcom/bytebitx/ndkdemo/MainActivity;)V", (void*) accessMainInstance},
};

if (env->RegisterNatives(cls, jniNativeMethod, 3) < JNI_OK) {
    return JNI_FALSE;
}

JNINativeMethod结构体中第三个参数的名称可以和java中对应的方法不一致,这里为了简便,所以写成了一样

accessBasicVar这个函数表示jni访问java的基本数据类型

JNIEXPORT JNICALL jobject accessBasicVar(JNIEnv *env, jobject thiz, jlong l, jboolean b, jstring s) {
    LOGD("dynamic register accessBasicVar");
    // 根据对象获取对应的class
    jclass cls = env->GetObjectClass(thiz);

    // 获取基本数据类型的fieldID
    jfieldID lField = env->GetFieldID(cls, "basicL", "J");
    jfieldID bField = env->GetFieldID(cls, "basicB", "Z");
    jfieldID sField = env->GetFieldID(cls, "basicS", "Ljava/lang/String;");
    jlong basicL = 2 + l;
    jboolean basicB = !b;
    jstring basicS = env->NewStringUTF("this is basic jni string");
    // 重新赋值基本数据类型
    env->SetLongField(thiz, lField, basicL);
    env->SetBooleanField(thiz, bField, basicB);
    env->SetObjectField(thiz, sField, basicS);

    // 获取静态的基本数据类型变量的fieldID
    jfieldID sLField = env->GetStaticFieldID(cls, "sBasicL", "J");
    jfieldID sBField = env->GetStaticFieldID(cls, "sBasicB", "Z");
    jfieldID sSField = env->GetStaticFieldID(cls, "sBasicS", "Ljava/lang/String;");
    jlong sLong = 1 + l;
    jboolean sBoolean = b;
    jstring sString = env->NewStringUTF("this is new jni string");
    // 重新给静态的基本数据类型变量赋值
    env->SetStaticLongField(cls, sLField, sLong);
    env->SetStaticBooleanField(cls, sBField, sBoolean);
    env->SetStaticObjectField(cls, sSField, sString);

    // 实例化ArrayList
    jclass listCls = env->FindClass("java/util/ArrayList");
    jmethodID cmid = env->GetMethodID(listCls, "<init>", "()V");
    jobject arrayList = env->NewObject(listCls, cmid);
    jmethodID addMid = env->GetMethodID(listCls, "add", "(Ljava/lang/Object;)Z");

    // 由于创建ArrayList对象的时候,泛型是Object,是属于引用类型,所以需要创建基本数据类型
    // 对应的引用类型,才能往ArrayList中添加
    jclass longCls = env->FindClass("java/lang/Long");
    jmethodID longConstructorMid = env->GetMethodID(longCls, "<init>", "(J)V");
    jobject longObj = env->NewObject(longCls, longConstructorMid, basicL);
    jobject sLongObj = env->NewObject(longCls, longConstructorMid, sLong);
    // 调用ArrayList对象的add方法
    env->CallBooleanMethod(arrayList, addMid, longObj);
    env->CallBooleanMethod(arrayList, addMid, sLongObj);

    jclass booleanCls = env->FindClass("java/lang/Boolean");
    jmethodID boolConstructorMid = env->GetMethodID(booleanCls, "<init>", "(Z)V");
    jobject boolObj = env->NewObject(booleanCls, boolConstructorMid, basicB);
    jobject sBoolObj = env->NewObject(booleanCls, boolConstructorMid, sBoolean);
    env->CallBooleanMethod(arrayList, addMid, boolObj);
    env->CallBooleanMethod(arrayList, addMid, sBoolObj);

    env->CallBooleanMethod(arrayList, addMid, basicS);
    env->CallBooleanMethod(arrayList, addMid, sString);

    return arrayList;
}

accessReferenceVar这个函数表示jni访问java的引用数据类型,例如这里需要访问MainActivity中的成员变量testCallBack,该变量的类型是TestCallBack

JNIEXPORT void JNICALL accessReferenceVar(JNIEnv *env, jobject thiz,
                                                          jobject test_call_back) {

    // 获取对象对应的class对象
    jclass testCls = env->GetObjectClass(test_call_back);
    // 获取类中的方法id
    jmethodID frameMid = env->GetMethodID(testCls, "onVideoFrame", "(IJ)V");
    jlong bitrate = 1;
    // 调用test_call_back对象的onVideoFrame方法
    env->CallVoidMethod(test_call_back, frameMid, 1, bitrate);

    jmethodID successMid = env->GetMethodID(testCls, "onConnectSuccess", "(II)V");
    env->CallVoidMethod(test_call_back, successMid, 0, 0);

    jmethodID errMid = env->GetMethodID(testCls, "onConnectError", "()V");
    env->CallVoidMethod(test_call_back, errMid);

}

accessMainInstance这个函数其实也是jni访问java的引用数据类型,只是这个引用是MainActivity对象

//为什么 thiz 和 instance 不是同一个内存地址?
//JNI 是基于句柄的:jobject 是一个指向 JVM 内部数据结构的句柄,而不是直接的内存指针。不同的 jobject 可以引用相同的 Java 对象。
//不同的调用上下文:在 JNI 方法中,thiz 代表的是调用该方法的对象实例,而 instance 是作为参数传递的对象引用。
// 尽管它们引用相同的 Java 对象,但它们在 JNI 中被视为不同的句柄。
JNIEXPORT JNICALL void accessMainInstance(JNIEnv *env, jobject thiz, jobject main_activity) {
    LOGD("dynamic register nativeMainActivity");
    if (env->IsSameObject(thiz, main_activity)) { // true
        LOGD("same object");
    } else {
        LOGD("not same object");
    }
    LOGD("Java object address: %p\n", thiz);
    LOGD("Java object address: %p\n", main_activity);
    // 打印的地址不一样
    jclass cls = env->GetObjectClass(thiz); // 使用thiz和main_activity是相同效果
    jmethodID resumeMid = env->GetMethodID(cls, "onResume", "()V");
    env->CallVoidMethod(thiz, resumeMid);
}

如果想动态注册staticAccessNormalL函数呢?

//-------------------------------------注册kotlin的静态方法---------------------------------------

jclass clsCompanion = env->FindClass("com/bytebitx/ndkdemo/MainActivity$Companion");
if (clsCompanion == nullptr) {
    return JNI_FALSE;
}
JNINativeMethod jniNativeCompanionMethod[] = {
        {"staticAccessNormalL", "(J)J", (void*) staticAccessL},
};
if (env->RegisterNatives(clsCompanion, jniNativeCompanionMethod, 1) < JNI_OK) {
    return JNI_FALSE;
}

staticAccessL实现方式如下:

JNIEXPORT JNICALL jlong staticAccessL(JNIEnv *env, jobject thiz, jlong l) {
    LOGD("dynamic register accessStaticL");
    // 根据对象获取对应的class
    jclass cls = env->GetObjectClass(thiz);
    jmethodID setLMid = env->GetMethodID(cls, "setSBasicL", "(J)V");
    jlong sLong = 11 + l;
    env->CallVoidMethod(thiz, setLMid, sLong);
    return sLong;
}

在jni中要想实例化一个类,并调用其方法,那么实现方式如下:

//-------------------------------------实例化java类并调用java类的方法---------------------------------------
// 这个字符串表示需要实例化的java类的路径
jclass realBackCls = env->FindClass("com/bytebitx/ndkdemo/MainActivity$RealPlayCallBack");
// 获取构造方法的methodID
jmethodID cmid = env->GetMethodID(realBackCls, "<init>", "()V");
// 实例化MainActivity$RealPlayCallBack类,相当于java的new关键字
jobject realObj = env->NewObject(realBackCls, cmid);
// 获取类中对应的方法
// 参数1:对象
// 参数2:方法名称
// 参数3:方法签名(写法参见上面的签名表)
jmethodID framesMid = env->GetMethodID(realBackCls, "onVideoFrame", "(IJ)V");
jlong bitrate = 1;
env->CallVoidMethod(realObj, framesMid, 0, bitrate);

jmethodID connectSucMid = env->GetMethodID(realBackCls, "onConnectSuccess", "(II)V");
env->CallVoidMethod(realObj, connectSucMid, 1, 1);

jmethodID connectErrMid = env->GetMethodID(realBackCls, "onConnectError", "()V");
env->CallVoidMethod(realObj, connectErrMid);

以上都是jni在主线程访问java变量或者方法,那如果想在子线程访问变量和方法,又该怎么办呢? 例如在MainActivity中有一个这样的native方法:

// 子线程回调callback方法
external fun accessCallBackByChildThread(testCallBack: TestCallBack)

在jni中动态注册该native方法,方式和上面的一致,在JNINativeMethod中添加函数名,函数签名及函数具体的实现即可。

jobject testObj = nullptr;
jmethodID fMid = nullptr;


// 具体的实现方式,注意AttachCurrentThread方法必现和DetachCurrentThread方法成对使用
void *testCallBack(void *) {
    JNIEnv *env;
    jint result = getJvm()->AttachCurrentThread(&env, nullptr);
    if (result == 0) { // 获取env实例成功
        jlong bitrate = 50;
        env->CallVoidMethod(testObj, fMid, 50, bitrate);
        env->DeleteGlobalRef(testObj);
    }
    getJvm()->DetachCurrentThread();
    pthread_exit(0);
}


JNIEXPORT void JNICALL accessCallBackByChildThread(JNIEnv *env, jobject thiz, jobject test_call_back) {
    jclass testCls = env->GetObjectClass(test_call_back);
    testObj = env->NewGlobalRef(test_call_back);
    fMid = env->GetMethodID(testCls, "onVideoFrame", "(IJ)V");
    // jni中创建线程(其实就是c/c++创建线程的方式)
    pthread_t pthreadHandle;
    pthread_create(&pthreadHandle, nullptr, testCallBack, nullptr);
}

以上基本上就是jni的核心用法了,掌握了以上的方法,jni基本就算掌握了,剩下的就是c/c++语法的学习了。

参考文章:mp.weixin.qq.com/s/UIkHnLatz…