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

音视频开发之旅 (二) — 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 Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
“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的使用中会事半功倍

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值