音视频开发之旅 (二) — 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 Descriptor | Java Language Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
“Ljava/lang/String;” | String |
“[I” | int[] |
“[Ljava/lang/Object;” | Object[] |
方法描述符
方法描述符是描述一个方法(或者说函数),主要描述方法的参数和返回类型,都是通过域描述符进行描述,方法描述符的构成为(形参对应的域描述符)返回类型对应域描述符。并且各个描述符之间没有空格和逗号或者是其他类型的间隔符号。字符V用于表示返回类型为void,而构造函数使用V表示他们的返回类型并且使用作为名字,下表为简单示例:
Method Descriptor | Java 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的使用中会事半功倍