初识 JNI

作者: Kuky_xs | 来源:发表于2019-11-07 00:12 被阅读0次

    JNI 作为 Java/Kotlin(原生端) 同 C/C++ 端交互的工具,是学习 ffmpeg 的一个前提,这边做一个学习过程中的记录。通过 Android Studio 可以快速创建一个 JNI 项目(创建时候选择 Native C++ 即可,会自动配置 CMakeList 等文件),该文基于 AS 3.5

    loadLiabry

    src 文件夹下相比一般的 AS 项目多了 cpp 文件夹,该文件夹下有一个 .cpp 文件和 CMakeLists.txt 文件,.cpp 文件用来写 native 端实现的方法,CMakeLists 用来做些 cpp 的配置,目前可以忽略

    main
    │  AndroidManifest.xml
    ├─cpp
    │      native-lib.cpp
    │      CMakeLists.txt
    ├─java
    │
    ├─res
    

    接着在 MainActivity 中有这么一行代码

        companion object {
            init {
                System.loadLibrary("native-lib")
            }
        }
    

    通过 loadLibrary 方法,加载编译的链接 so 库,so 库的源码就是前面提到的 native-lib.cpp 文件了

    原生调用 cpp 方法

    那么在 Kotlin 中如何调用 cpp 的方法呢,可以看到 MainActivity 中有一个使用 external 修饰的方法(如果是 java 则使用 native 关键词修饰)

    external fun stringFromJNI(): String
    

    通过该方法,会去调用 cpp 层的 native 方法,可以看下 native-lib.cpp 文件,内部定义了一个方法

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_xxx_MainActivity_stringFromJNI(JNIEnv *env, jobject/*this*/) {
        std:string hello = "Hello from c++";
        return env->NewStringUTF(hello.c_str());
    }
    

    可以看到该方法的命名方式为 Java_包名_类名_方法名(包名的 . 替换成 _ 即可),通过这种命名方式来查找 Kotlin 层的调用方法,该方法中 extern "C" 的作用是让 C++ 支持 C 的方法,JNIEXPORT xxx JNICALL 代表这是一个 JNI 方法,xxx 表示返回的方法类型,在 JNI 中,都有 Kotlin 对应的数据类型

    JNI 数据类型

    JNI 对应 Java 的数据类型如下,也可以直接查看 jni.h 头文件

    JNI类型 Java类型 类型描述
    jboolean boolean 无符号8位
    jbyte byte 无符号8位
    jchar char 无符号16位
    jshort short 有符号16位
    jint int 有符号32位
    jlong long 有符号64位
    jfloat float 有符号32位
    jdouble double 有符号64位

    因为 String 不属于基本类型,所以不定义在这,需要返回 jsrting 类型,只能通过 char * 进行相应的转换,所以上述的函数中,使用 env->NewStringUTF(hello.c_str()) 方法,生成 jstring 并返回,然后在 Kotlin 层通过调用 stringFromJNI 方法就可以将 native 层返回的字符串显示出来,JNI 的基本使用就这么多啦,接着通过一些使用,熟悉一些方法,比如实现字符串的拼接

    external fun stringCat(a: String, b: String): String
    

    回到 c++ 层做具体的实现,前面提到因为在 C++ 中字符串拼接不能直接通过 jstring 相加实现,需要通过 char * 进行拼接,所以就需要封装一个 jstring2Char 的方法进行转换

    char *jstring2Char(JNIEnv *env, jstring jstr) {
        char *rtn = nullptr;
    
        jclass clazz = env->FindClass("java/lang/String");
        jstring strenCode = env->NewStringUTF("UTF-8");
        jmethodID mid = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
    
        auto barr = (jbyteArray) (env->CallObjectMethod(jstr, mid, strenCode));
        jsize alen = env->GetArrayLength(barr);
        jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
        
        if (alen > 0) {
            // malloc(bytes) 方法分配 bytes 字节,并返回这块内存的指针,
            // malloc 分配的内存记得使用 free 进行释放,否则会内存泄漏
            rtn = static_cast<char *>(malloc(static_cast<size_t>(alen + 1)));
            // memcpy(void*dest, const void *src, size_t n)
            // 由 src 指向地址为起始地址的连续 n 个字节的数据复制到以 destin 指向地址为起始地址的空间内
            memcpy(rtn, ba, static_cast<size_t>(alen));
            rtn[alen] = 0;
        }
    
        env->ReleaseByteArrayElements(barr, ba, 0);
        return rtn;
    }
    

    定义完转换方法,直接调用即可,记得释放内存

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_xxx_MainActivity_stringCat(JNIEnv *env, jobject, jstring a, jstring b){
        char *first = jstring2Char(env, a);
        char *second = jstring2Char(env, b);
        std::strcat(first, second);
        free(first);
        free(second);
        return env->NewStringUTF(first);
    }
    

    静态 JNI 方法

    在很多情况下,都不会将 JNI 方法直接定义在 Activity,而是封装到公共方法中,方便调用,那么在公共方法类调用除了通过该类的实例,调用相应方法,还有就是设置该方法为静态方法,那么这种情况和上述有啥区别呢,其实区别不是很大,只需要将 native 端的方法中的参数 jobject 替换成 jclass 即可,但是在 Kotlin 端,除了在半生对象中声明该 native 方法,还需要增加 JvmStatic 注解才行,例如有如下的一个方法

    class JniUtils {
        companion object {
            @JvmStatic
            external fun plus(a: Int, b: Int): Int
        }
    }
    

    那么在 native 端生成 JNI 方法和前面提到的类似,只需替换参数类型即可

    extern "C" JNIEXPORT jint JNICALL
    Java_com_xxx_JniUtils_plus(JNIEnv *env, jclass, jint , jint b){
        return a + b;
    }
    

    C++ 调用 Kotlin 方法

    前面介绍了如何在 Kotlin 中调用 native 方法,当然,在 c++ 层也可以调用 Kotlin 层的方法。假设在 MainActivity 中有一个 callMe(message: String)call(message:String) 方法,在调用 call 的时候,同时内部调用 callMe 方法,当然直接调用很简单,这边通过 JNI 来实现

    fun callMe(message: String){
        Log.e(TAG, message) // 只做简单的打印
    }
    
    external fun call(message: String)
    

    native 实现 call 方法上面已经介绍了,接下来介绍在 JNI 内部调用 callMe 方法

    extern "C" JNIEXPORT void JNICALL
    Java_com_xxx_MainActivity_call(JNIEnv *env, jobject instance, jstring msg){
        const char *methodName = "callMe"; // 指定需要调用的方法名
        jclass clazz = env->FindClass("com.xxx.MainActivity"); //查找对应的类,指定对应的包名和类
        // 根据所在类和方法名查找方法的 ID,最后一个参数为方法的签名,稍后做解释
        jmethodID mid = env->GetMethodId(clazz, methodName, "(Ljava/lang/String;)V"); 
        env->CallVoidMethod(instance, mid, msg); // 根据返回的类型,调用方法,传入相应参数
    }
    

    Kotlin 层调用 call 方法的时候,就会通过 JNI 调用 callMe 方法,执行 callMe 的内部逻辑。在上面提到了「签名」这个东西,这边列出签名的表示方法

    类型 签名
    boolean Z
    byte B
    char C
    short S
    int I
    long J
    float F
    double D
    void V
    数组 [
    String/Object Ljava/lang/String; Ljava/lang/Object;
    普通类(com.example.className) Lcom/example/className;
    嵌套类(com.example.className.Inner) Lcom/example/className$Inner;

    所以方法的签名的规则就是根据传入的参数类型和返回的类型,替换成相应的签名即可,例如:call(Student s, int a): String 方法的签名为 (Lcom/xxx/Student;I)Ljava/lang/String; 如果是内部类则使用 $ 表示嵌套

    C++ 获取 Kotlin 的内部参数

    假设我们在 MainActivity 有个私有参数 name,如果外部有个类需要获取这个参数,可以通过 MainActivty 内部的共有方法来获取,假如没有这个共有方法该咋办呢,当然我们可以通过 JNI 来获取

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_xxx_MainActivity_getField(JNIEnv *env, jobjcet instance){
        jclass clazz = env->FindClass("com.xxx.MainActivity"); // 根据类的包名来查找相应的类
        // 根据类和参数名来获取该参数,第三个参数为参数的签名,即类型在 JNI 对应的签名
        jfieldID fid = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
        // 因为 String 不是基本类型,所以只能通过 GetObjectField 进行获取,然后进行强转
        // 如果是 int 等基本类型,提供了 GetIntField 等获取方法,auto 为可自行根据结果判断类型
        auto name = (jstring)(env->GetObjectField(instance, fid));
        return name;
    }
    

    当在外部通过 getField 方法即可获取到该私有属性,这个例子仅为例子而已...

    C++ 获取普通类的参数信息

    假设我们有一个类,例如 Student 里面有一些名字,年龄等属性,然后通过 JNI 将这些属性转成 String 返回,那么就需要涉及到获取参数的字段信息了

    // 定义一个普通类 Student
    data class Student(val firstName: String, val lastName: String, val age: Int)
    
    // 在 MAinActivity 定义一个转换的方法
    external fun printStudent(Student student): String
    

    那么在 C++ 层就需要将 student 内部的信息都获取出来,并拼接到字符串,然后返回

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_xxx_MainActivity_printStudent(JNIEnv *env, jobject, jobject student){
        jcalss clazz = env->GetObjectClass(student); // 获取传入参数对应的类
        // 通过参数名和签名,去对应的 class 获取相应的 FieldID,
        // 然后根据 FiedlID 通过 GetObjectField 方法获取对应的属性
        auto firstName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "firstName", "Ljava/lang/String;")));
        auto lastName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "lastName", "Ljava/lang/String;")));
        // int 为基本类型,可直接通过获取对应类型属性的方法获取
        auto age = env->GetIntField(student, env->GetFieldID(clazz, "age", "I"));
        
        char *cFirstName = jstring2Char(firstName);
        char *cLastName = jstring2Char(lastName);
        std::string cAge = std::to_string(age);
        
        strcat(cFirstName, " ");
        strcat(cFirstName, cLastName);
        strcat(cFirstName,  " is ");
        strcat(cFirstName, cAge.c_str());
        strcat(cFirstName, " years old");
        
        free(cFirstName);
        free(cLastName);
        
        return env->NewStringUTF(cFirstName);
    }
    

    当外部调用 printStudent 方法的时候就会将 student 的属性打印出来

    动态注册

    在前面的 JNI 方法中,每个方法都需要写很长的一段类名,非常容易出错,那么能不能省略包名呢,当然是可以,通过动态注册就可以让这个麻烦的方法名变得简略

    动态注册,需要指定一个方法列表,用来存放同个包名下的方法,存放的方式如下:

    { Kotlin 层方法名, 方法前面, JNI 函数指针} // 函数指针固定为 ```(void *) JNI 方法名```
    

    例如我们前面提到的方法,放到一个列表中

    static JNINativeMethod jniMethods[] = {
        {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
        {"stringCat", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void *) stringCat},
        {"call", "(Ljava/lang/String;)V", (void *) call},
        {"getField", "()Ljava/lang/String;", (void *) getField},
        {"printStudent", "(Lcom/xxx/Student;)Ljava/lang/String;", (void *) printStudent},
    };
    

    接着就是需要注册这些方法了,封装一个通用的方法,注册成功返回 JNI_TRUE 否则 JNI_FALSE

    static int registerNativeMethods(JNIEnv *env, const char *className, 
                                     JNINativeMethod *getMethods, int sumNum){
        jclass clazz = env->FindClass(className); // 根据类名去查找相应类,包含 JNINativeMethod 列表所有方法
    
        if (clazz == nullptr) return JNI_FALSE; // 未找到 class 则认为注册失败
    
        // 根据所有的方法名和数量进行注册,如果结果返回小于 0 则认为注册失败
        if (env->RegisterNatives(clazz, getMethods, methodSum) < 0) return JNI_FALSE;
    
        return JNI_TRUE;
    }
    

    接着就需要实现 JNI_OnLoad 方法(定义在 jni.h 头文件中),对上述的方法进行注册,该方法会返回一个版本号

    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
        JNIEnv *env = nullptr;
    
        // 检测环境失败返回 -1
        if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
            return -1;
        }
    
        assert(env != nullptr);
    
        // 注册失败返回 -1
        if (!registerNativeMethods(
                env, jniClazz, jniMethods, sizeof(jniMethods) / sizeof(jniMethods[0]))) {
            return -1;
        }
    
        return JNI_VERSION_1_6;
    }
    

    这样几步就完成了 JNI 方法的动态注册,只需要全局定义 className 即可,不需要每次都在方法声明完整包路径

    内存释放

    C++ 中,非常重要的一步就是内存释放,否则就会造成内存泄漏,分分钟给你炸开

    哪些需要手动释放
    • 不需要手动释放(基本类型):jint,jlong 等等
    • 需要手动释放(引用类型,数组家族):jstring,jobject ,jobjectArray,jintArray ,jclass ,jmethodID
    释放方法(该部分参考自《JNI手动释放内存》)
    • jstring & char *
      // 创建 jstring 和 char*
      jstring jstr = (jstring)(jniEnv->CallObjectMethod(jniEnv, mPerson, getName));
      char* cstr = (char*) jniEnv->GetStringUTFChars(jniEnv,jstr, 0);
       
      // 释放
      jniEnv->ReleaseStringUTFChars(jniEnv, jstr, cstr);
      jniEnv->DeleteLocalRef(jniEnv, jstr);jbyteArray audioArray = jnienv->NewByteArray(frameSize);
       
      jnienv->DeleteLocalRef(audioArray)
      
    • jobject,jobjectArray,jclass ,jmethodID 等引用类型
      jniEnv->DeleteLocalRef(jniEnv, XXX);
      
    • jbyteArray
      jbyteArray arr = jnienv->NewByteArray(frameSize);
      jnienv->DeleteLocalRef(arr);
      
    • GetByteArrayElements
      jbyte* array= jniEnv->GetByteArrayElements(env,jarray,&isCopy);
      jniEnv->ReleaseByteArrayElements(env,jarray,array,0);
      
    • NewGlobalRef
      jobject ref= env->NewGlobalRef(customObj);
      env->DeleteGlobalRef(customObj);
      

    举个例子

    Android 中,经常需要用到 Context 获取一些相关的信息,这边举个获取屏幕信息的例子

    #include <jni.h>
    #include <string>
    #include <iostream>
    #include <android/log.h>
    
    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI", __VA_ARGS__)
    
    // 获取当前的 Context
    jobject getAndroidApplication(JNIEnv *env) {
        jclass activityThreadClazz = env->FindClass("android/app/ActivityThread");
    
        jmethodID jCurrentActivityThread =
                env->GetStaticMethodID(activityThreadClazz,
                                       "currentActivityThread", "()Landroid/app/ActivityThread;");
    
        jobject currentActivityThread =
                env->CallStaticObjectMethod(activityThreadClazz, jCurrentActivityThread);
    
        jmethodID jGetApplication =
                env->GetMethodID(activityThreadClazz, "getApplication", "()Landroid/app/Application;");
    
        return env->CallObjectMethod(currentActivityThread, jGetApplication);
    }
    
    extern "C" JNIEXPORT void JNICALL
    Java_com_demo_kuky_jniwidth_MainActivity_jniDensity(JNIEnv *env, jobject) {
    
        jobject instance = getAndroidApplication(env);
        jclass contextClazz = env->GetObjectClass(instance);
        // 获取 `getResources` 方法
        jmethodID getResources = env->GetMethodID(contextClazz, "getResources",
                                                  "()Landroid/content/res/Resources;");
    
        jobject resourceInstance = env->CallObjectMethod(instance, getResources);
        jclass resourceClazz = env->GetObjectClass(resourceInstance);
        // 获取 Resources 下的 `getDisplayMetrics` 方法
        jmethodID getDisplayMetrics = env->GetMethodID(resourceClazz, "getDisplayMetrics",
                                                       "()Landroid/util/DisplayMetrics;");
    
        jobject metricsInstance = env->CallObjectMethod(resourceInstance, getDisplayMetrics);
        jclass metricsClazz = env->GetObjectClass(metricsInstance);
    
        // 获取 DisplayMetrics 下的一些参数
        jfieldID densityId = env->GetFieldID(metricsClazz, "density", "F");
        jfloat density = env->GetFloatField(metricsInstance, densityId);
    
        jfieldID widthId = env->GetFieldID(metricsClazz, "widthPixels", "I");
        jint width = env->GetIntField(metricsInstance, widthId);
    
        jfieldID heightId = env->GetFieldID(metricsClazz, "heightPixels", "I");
        jint height = env->GetIntField(metricsInstance, heightId);
    
        LOGE("get density: %f, width: %d, height: %d", density, width, height);
    }
    

    目前使用到的就那么多啦,后面有更多的方法涉及到,再进行添加,Enjoy it ~

    相关文章

      网友评论

        本文标题:初识 JNI

        本文链接:https://www.haomeiwen.com/subject/bnxmbctx.html