音视频开发之旅 (二) --- cmake相关知识和Jni常用知识

    技术2022-07-11  153

    音视频开发之旅 (二) — cmake相关知识和Jni常用知识

    前言

    由于编译和使用ffmpeg涉及到 cmake和jni的相关知识,所以这一篇主要巩固这一块Android和C交互的相关知识点,从入门的同学角度来看,这是非常适合的一篇入门文章。如果已经熟悉这块的同学,可以将这一篇当作工具文章,方便查阅,若是想看demo的同学可以直接通过以下链接 相关代码在module-ffmpeg

    1. Cmake概述

    您可以向 Android 项目添加 C 和 C++ 代码,只需将相应的代码添加到项目模块的 cpp 目录中即可。在您构建项目时,这些代码会编译到一个可由 Gradle 与您的 APK 打包在一起的原生库中。然后,Java 或 Kotlin 代码即可通过 Java 原生接口 (JNI) 调用原生库中的函数。

    Android Studio 支持适用于跨平台项目的 CMake,以及速度比 CMake 更快但仅支持 Android 的 ndk-build。目前不支持在同一模块中同时使用 CMake 和 ndk-build。

    1.1 Cmake手动配置

    首先先下载ndk相关环境并配置 官网这篇配置ndk环境非常全面

    1.要手动配置 Gradle 以关联到您的原生库,您需要将 externalNativeBuild 块添加到模块级 build.gradle 文件中,例: android { …… defaultConfig { …… externalNativeBuild { cmake { //默认是cppFlags "" //如果要修改Customize C++ support部分,可在这里加入 cppFlags "" } } } buildTypes { release { …… } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.10.2" } } } 2.在当前模块的main下新建cpp文件夹,与java文件夹同层,并在cpp文件夹里新建 native-lib.cpp #include <jni.h> #include <string> #include <stdio.h> extern "C" JNIEXPORT jstring JNICALL Java_com_hugh_androidctest_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */thisObj) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } 3.在cpp文件夹里添加 CMakeList.txt 文件 复制或者右键新建file重命名都行 # For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.4.1) #创建并命名一个库,将其设置为静态 #或SHARED,并提供源代码的相对路径。 #你可以定义多个库,CMake为你构建它们。 # Gradle自动将共享库打包到APK中。 add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). native-lib.cpp ) #搜索指定的预构建库,并将路径存储为 #变量。因为CMake在搜索路径中包含了系统库 #默认情况下,你只需要指定公共NDK库的名称 #您想要添加。CMake在之前验证库是否存在 #完成其构建。 find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) #指定CMake应该链接到目标库的库。你 #可以链接多个库,比如本文中定义的库 #构建脚本、预构建的第三方库或系统库。 target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib} )

    其余cmake相关命令可以参考 cmake命令大全

    4.在所在的Activity当中调用相应的方法便能完成开发。 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_ccmain); mTvText = findViewById(R.id.tv_text); mTvText.setText(stringFromJNI()); } public native String stringFromJNI();

    2. JNI

    在上一个模块当中,大家已经接触到了jni的使用流程,在接下来的文字中,会重点讲常用的jni函数,如何将Android当中的变量或者对象与C桥接起来。

    2.1 JNI 基础概念

    Java基本数据类型与JNI的映射关系

    Java类型<-->JNI类型<-->C类型

    JNI基本数据类型(左边是Java,右边是JNI)

    boolean jboolean; byte jbyte; char jchar; short jshort; int jint; long jlong; float jfloat; double jdouble; void void

    JNI引用数据类型(左边是Java,右边是JNI)

    String jstring Object jobject //基本数据类型的数组 byte[] jByteArray //对象数组 Object[](String[]) jobjectArray

    域描述符

    Field DescriptorJava Language TypeZbooleanBbyteCcharSshortIintJlongFfloatDdouble“Ljava/lang/String;”String“[I”int[]“[Ljava/lang/Object;”Object[]

    方法描述符

    方法描述符是描述一个方法(或者说函数),主要描述方法的参数和返回类型,都是通过域描述符进行描述,方法描述符的构成为(形参对应的域描述符)返回类型对应域描述符。并且各个描述符之间没有空格和逗号或者是其他类型的间隔符号。字符V用于表示返回类型为void,而构造函数使用V表示他们的返回类型并且使用作为名字,下表为简单示例:

    Method DescriptorJava Language Type“()Ljava/lang/String;”String f();“(ILjava/lang/Class;)J”long f(int i, Class c);`"([B)V"String(byte[] bytes);

    2.2 JNI 实战

    上述讲了一堆的概念,现在还是跟着代码和运行结果来确认下到底是如何运用的,首先看下官方给的例子,了解下常用的参数都代表什么意思,一下代码均以cpp实现

    2.2.1 JNIEnv参数介绍

    extern "C" JNIEXPORT jstring JNICALL Java_com_hugh_ffmpeg_CCMainActivity_stringFromJNI( JNIEnv* env, jobject /* this */thisObj) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } JNIEnv 结构体指针 env二级指针(对应c,在C++是一个结构体的一级指针),由于需要用到JNIEnv变量,而JNIEnv是结构体指针,需要一个变量来表示JNIEnv,所以这个变量就是二级指针,而C++中有this关键字的,直接可以表示 每个native函数,都至少有两个参数(JNIEnv*,jclass或者jobject) 1.当native方法为静态方法时,jclass代表native方法所属类的class对象(JniTest.class) 2.当native方法为非静态方法时,jobject代表native方法所属类的对象

    2.2.2 jni访问修改Android的变量

    extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_accessField(JNIEnv *env, jobject jobj) { //得到jclass jclass jcla = env->GetObjectClass(jobj); //得到jfieldID,最后一个参数是签名,String对应的签名是Ljava/lang/String;(注意最后的分号) jfieldID jfID = env->GetFieldID(jcla, "mTestStr", "Ljava/lang/String;"); //得到key属性的值jstring jstring jstr = (jstring)env->GetObjectField(jobj,jfID); //jstring转化为C中的char* const char* oriText = env->GetStringUTFChars(jstr, NULL); //拼接得到新的字符串text="ddd good" char text[20] = "ddd "; strcat(text, oriText); //C中的char*转化为JNI中的jstring jstring jstrMod = env->NewStringUTF(text); //修改key env->SetObjectField(jobj, jfID, jstrMod); //只要使用了GetStringUTFChars,就需要释放 env->ReleaseStringUTFChars(jstr,oriText); }

    2.2.3 jni访问修改Android的静态变量

    //访问静态属性 extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_accessStaticField(JNIEnv *env, jobject jobj) { //得到jclass jclass jcla = env->GetObjectClass( jobj); //得到jfieldID jfieldID jfid = env->GetStaticFieldID(jcla, "mTestStaticCount", "I"); //得到静态属性的值mTestStaticCount jint count = env->GetStaticIntField(jcla, jfid); //自增 count++; //修改mTestStaticCount的值 env->SetStaticIntField( jcla, jfid, count); }

    2.2.4 jni访问方法

    //访问方法 extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_handleMethod(JNIEnv *env, jobject jobj) { //得到jclass jclass jcla = env->GetObjectClass(jobj); //得到jmethodID jmethodID jmid = env->GetMethodID(jcla, "getIntValue", "()I"); //调用java方法获取返回值,第四个参数100表示传入到java方法中的值 jint jRandom = env->CallIntMethod(jobj, jmid); //可以在Android Studio中Logcat显示,需要定义头文件#include <android/log.h> __android_log_print(ANDROID_LOG_DEBUG, "system.out", "getIntValue:%ld", jRandom); }

    2.2.5 jni访问对象的方法

    //调用对象的方法 extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_accessClassMethod(JNIEnv *env, jobject jobj) { //得到MainActivity对应的jclass jclass jcla = env->GetObjectClass(jobj); //得到xiaoming对象属性对应的jfieldID jfieldID jfid = env->GetFieldID( jcla, "xiaoming", "Lcom/hugh/ffmpeg/jnizz/People;"); //这里必须是父类对象的签名,否则会报NoSuchFieldError,因为Java中是父类引用指向子类对象 //得到xiaoming对象属性对应的jobject jobject animalObj = env->GetObjectField( jobj, jfid); // jclass animalCla =(*env)->GetObjectClass(env, animalObj); //这种方式,下面调用CallNonvirtualVoidMethod会执行子类的方法 //找到xiaoming对象对应的jclass jclass animalCla = env->FindClass("com/hugh/ffmpeg/jnizz/People"); //如果这里写成子类的全类名,下面调用CallNonvirtualVoidMethod会执行子类的方法 //得到getName对应的jmethodID jmethodID eatID = env->GetMethodID(animalCla, "getName", "()Ljava/lang/String;"); // (*env)->CallVoidMethod(env, animalCla, eatID); //这样调用会报错 //调用对象的方法 env->CallNonvirtualObjectMethod(animalObj, animalCla, eatID); //输出父类的方法 }

    2.2.6 jni数组相关操作

    //传入数组 extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_putArray(JNIEnv *env, jobject jobj, jintArray arr_) { //jintArray --> jint指针 --> C int 数组 jint *arr = env->GetIntArrayElements( arr_, NULL); //数组的长度 jint arrLength = env->GetArrayLength(arr_); //排序 // qsort(arr, arrLength, sizeof(jint), commpare); //同步 //0:Java数组进行更新,并且释放C/C++数组 //JNI_ABORT:Java数组不进行更新,但是释放C/C++数组 //JNI_COMMIT:Java数组进行更新,不释放C/C++数组(函数执行完后,数组还是会释放的) env->ReleaseIntArrayElements( arr_, arr, JNI_COMMIT); } //返回数组 extern "C" JNIEXPORT jintArray JNICALL Java_com_hugh_ffmpeg_CCMainActivity_getArray(JNIEnv *env, jobject jobj, jint arrLength) { //创建一个指定大小的数组 jintArray array = env->NewIntArray( arrLength); jint* elementp = env->GetIntArrayElements(array, NULL); jint* startP = elementp; int i = 0; for (; startP < elementp + arrLength; startP++) { (*startP) = i; i++; } //同步,如果没有同步Java层打印出来的数组里面的各个值为0 env->ReleaseIntArrayElements( array, elementp, 0); return array; }

    2.2.7 jni内部全局对象操作

    //全局引用 //共享(可以跨多个线程),手动控制内存使用 //创建 jstring jstr; extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_createGlobalRef(JNIEnv *env, jobject instance) { jstring obj = env->NewStringUTF( "people"); jstr =(jstring)env->NewGlobalRef(obj); } //获得 extern "C" JNIEXPORT jstring JNICALL Java_com_hugh_ffmpeg_CCMainActivity_getGlobalRef(JNIEnv *env, jobject instance) { return jstr; } //释放 extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_deleteGlobalRef(JNIEnv *env, jobject instance) { env->DeleteGlobalRef(jstr); }

    2.2.8 jni 静态变量操作

    //C++静态变量 extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_staticRef(JNIEnv *env, jobject jobj) { jclass jcla = env->GetObjectClass( jobj); //局部静态变量,作用域当然是函数中 static效果和android一样 //在第一次调用函数的时候会初始化,函数结束,但是它的值还会存在内存当中(只有当程序结束了才会销毁),只会声明一次 static jfieldID jfid = NULL; //如果加了static修饰,下面就只有一个打印,如果没加,for循环执行了多少次就打印多少次,这里是10次 if (jfid == NULL) { jfid = env->GetFieldID(jcla, "mTestStr", "Ljava/lang/String;"); __android_log_print(ANDROID_LOG_DEBUG, "system.out", "加了static"); } } extern "C" JNIEXPORT void JNICALL Java_com_hugh_ffmpeg_CCMainActivity_NostaticRef(JNIEnv *env, jobject jobj) { jclass jcla = env->GetObjectClass( jobj); //这边如果不用static关键词 jfieldID jfid = NULL; if (jfid == NULL) { jfid = env->GetFieldID(jcla, "mTestStr", "Ljava/lang/String;"); __android_log_print(ANDROID_LOG_DEBUG, "system.out", "不加static"); } }

    2.2.9 相关小提示

    JNI 引用变量 引用类型:局部引用和全局引用 作用:在JNI中告知虚拟机何时回收一个JNI变量

    局部引用,通过DeleteLocalRef手动释放对象 1.访问一个很大的java对象,使用完之后,还要进行复杂的耗时操作 2.创建了大量的局部引用,占用了太多的内存,而且这些局部引用跟后面的操作没有关联性

    小结

    这一章掌握好jni基础,在下一章进入ffmpeg的使用中会事半功倍

    Processed: 0.009, SQL: 9