转载请声明出处:https://blog.csdn.net/AndrExpert/article/details/82909572
在Android Studio中使用CMake进行NDK/JNI开发(初级)一文中,我们详细介绍了如何在Android Studio中使用cmake编译环境开发原生库(.so)。本文将在此基础上,进一步学习CmakeLists.txt脚本文件的语法规则,同时阐述如何在AS中编译和使用第三方库,最终生成新的功能原生库。
1. 使用Cmake编译第三方库源码
为了更好地学习CmakeLists.txt脚本文件的语法规则,这里我们以Lame库编译封装
为例,Lame是Mike Cheng于1998年发起的一个开源项目,是目前最好的MP3编码引擎。关于如何将Lame库源码导入到AS,请详见我这篇文章Android直播开发之旅(4):MP3编码格式分析与lame库编译封装。这里我们仅关注该文开源项目Lame4Mp3的CmakeLists.txt,首先看下项目代码结构和lame目录下Lame库源码(部分):
接下来,我们继续讲解如何在项目中使用Lame库,然后编译成原生库so并打包到APK。首先,我们在Android项目的cpp目录中创建自己的C/C++源文件,然后在该源文件中调用lame库中的API,这里跟C/C++开发过程一样的。LameMp3.c源码如下:
//
// Created by jianddongguo on 2018/9/30.
//
#include <jni.h>
#include "LameMp3.h"
#include "lame/lame.h"
static lame_global_flags *gfp = NULL;
JNIEXPORT void JNICALL
Java_com_jiangdg_natives_LameMp3_lameInit(JNIEnv *env, jclass type, jint inSampleRate,
jint outChannelNum, jint outSampleRate, jint outBitRate,
jint quality) {
if(gfp != NULL){
lame_close(gfp);
gfp = NULL;
}
// 初始化
gfp = lame_init();
LOGI("初始化lame库完成");
//配置参数
lame_set_in_samplerate(gfp,inSampleRate);
lame_set_num_channels(gfp,outChannelNum);
lame_set_out_samplerate(gfp,outSampleRate);
lame_set_brate(gfp,outBitRate);
lame_set_quality(gfp,quality);
lame_init_params(gfp);
LOGI("配置lame参数完成");
}
JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_LameMp3_lameFlush(JNIEnv *env, jclass type, jbyteArray mp3buf_) {
jbyte *mp3buf = (*env)->GetByteArrayElements(env, mp3buf_, NULL);
jsize len = (*env)->GetArrayLength(env,mp3buf_);
int resut = lame_encode_flush(gfp,mp3buf,len);
(*env)->ReleaseByteArrayElements(env, mp3buf_, mp3buf, 0);
LOG_I("写入mp3数据到文件,返回结果=%d",resut);
return resut;
}
JNIEXPORT void JNICALL
Java_com_jiangdg_natives_LameMp3_lameClose(JNIEnv *env, jclass type) {
lame_close(gfp);
gfp = NULL;
LOGI("释放lame资源");
}
JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_LameMp3_lameEncode(JNIEnv *env, jclass type, jshortArray letftBuf_,
jshortArray rightBuf_, jint sampleRate,
jbyteArray mp3Buf_) {
if(letftBuf_ == NULL || mp3Buf_ == NULL){
LOGI("letftBuf和rightBuf 或mp3Buf_不能为空");
return -1;
}
jshort *letftBuf = NULL;
jshort *rightBuf = NULL;
if(letftBuf_ != NULL){
letftBuf = (*env)->GetShortArrayElements(env, letftBuf_, NULL);
}
if(rightBuf_ != NULL){
rightBuf = (*env)->GetShortArrayElements(env, rightBuf_, NULL);
}
jbyte *mp3Buf = (*env)->GetByteArrayElements(env, mp3Buf_, NULL);
jsize readSizes = (*env)->GetArrayLength(env,mp3Buf_);
// 编码
int result = lame_encode_buffer(gfp,letftBuf,rightBuf,sampleRate,mp3Buf,readSizes);
// 释放资源
if(letftBuf_ != NULL){
(*env)->ReleaseShortArrayElements(env, letftBuf_, letftBuf, 0);
}
if(rightBuf_ != NULL){
(*env)->ReleaseShortArrayElements(env, rightBuf_, rightBuf, 0);
}
(*env)->ReleaseByteArrayElements(env, mp3Buf_, mp3Buf, 0);
LOG_I("编码pcm为mp3,数据长度=%d",result);
return result;
}
然后,编写CmakeLists.txt脚本文件,用于指定源文件路径、生成so文件名,引入NDK原生库以及链接so到APK等。CmakeLists.txt内容如下:
# 指定Cmake版本
cmake_minimum_required(VERSION 3.4.1)
# 导入lame第三库相关头文件路径
include_directories(src/main/cpp/lame)
# 查找lame目录下的所有源文件,将输出结果列表储存在SRC_LIST变量中
aux_source_directory(src/main/cpp/lame SRC_LIST)
# 设置so动态库最后的输出路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
# 指定编译的源码和编译生成so库文件名、类型
add_library(
# 生成库的名称,会自动添加前缀"lib"
LameMp3
# 生成库类型,动态库-SHARED,静态库-STATIC
SHARED
# 编译的源文件,包括自己和lame库c或c++源文件
src/main/cpp/LameMp3.c ${SRC_LIST})
# 导入NDK原生库-日志库
find_library( # 变量,保存log库的路径
log-lib
# 指定需要查找NDK哪个原生库(log库),便于cmake能够定位到
log )
# 将相关库链接到LameMp3,可以链接多个库
target_link_libraries(
# 指定被链接库名称,即最终输出库LameMp3
LameMp3
# NDK中的log库
${log-lib} )
2. 详解CmakeLists.txt语法与实战案例
在上一小节中,我们学习了如何使用CmakeList.txt脚本文件指定编译动态库所需的源头文件、源文件、引用和链接NDK原生so库等操作。本节就在此基础上详细介绍CmakeLists.txt脚本文件常用的语法规则,然后再给出一个ffmpeg框架在AS中如何使用的实战案例。
2.1 CmakeList.txt语法分析
- 原型: cmake_minimum_required(VERSION
<min>
[...<max>
] [FATAL_ERROR])- 参数:
min:执行项目使用的Cmake最低版本;
max:可选,执行最大版本;
FATAL_ERROR:2.6版本以上可忽略;- 作用:指定项目所需Cmake的最低版本版本。如果真实编译环境的Cmake版本低于min,则停止编译并报error。
- 实例: cmake_minimum_required(VERSION 3.4.1)
- 原型:include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])
- 参数:
AFTER|BEFORE:指定源文件目录后,通常会被附加到编译的当前目录列表中,但也可以通过AFTER
或BEFORE
来改变这种行为方式;
SYSTEM:如果给出SYSTEM选项,编译器将被告知该目录是系统包含目录。
dir1:源文件目录路径,可多个,用一个空格隔开;- 作用:导入给定源文件目录
- 实例:include_directories(src/main/cpp/lame)
- 原型:aux_source_directory(
<dir> <variable>
)- 参数:
dir:源文件所在目录;
variable:用于存储文件名称列表的变量;- 作用:收集指定目录中所有源文件的名称,并将列表存储在提供的variable中
- 实例:aux_source_directory(src/main/cpp/lame SRC_LIST)
- 原型:
set(<variable> <value>
… [PARENT_SCOPE])
set(<variable> <value>
… CACHE<type> <docstring>
[FORCE])
set(ENV{<variable>} <value>
…)- 参数:
variable:给定变量,除了可以自定义变量,Cmake还提供了很多variable值,常见的有CMAKE_LIBRARY_OUTPUT_DIRECTORY用于改变so输出路径,具体详见cmake-variables;
value:变量的值
FORCE:由于缓存条目旨在提供用户可设置的值,因此默认情况下不会覆盖现有的缓存条目。 使用FORCE选项覆盖现有条目。- 作用:设置Cmake变量,有三种情形
将当前function和目录范围设定为给定变量variable;
设置给定的缓存(缓存条目);
将当前执行环境变量设定为给定值variable;- 实例:
set(libs “${CMAKE_SOURCE_DIR}/src/main/jniLibs”)
set(SRC_LIST main.c t1.c t2.c)
set(ANDROID_NDK_REVISION 13)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY …)
用法1
- 原型:
add_library(< name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2 …])- 参数:
name
:被构建库目标名称
[STATIC | SHARED ..
:被构建库的类型,STATIC为静态库,生成的库 以libname.a形式存在,SHARED 为动态库,生成的库以libname.so形式存在;
EXCLUDE_FROM_ALL
:对应的一些属性会在目标被创建时被设置;
[source1] [source2 ...]
:指定源文件,多个文件由一个空格隔开;- 作用:添加一个库
- 实例: add_library(LameMp3 SHARED src/main/cpp/LameMp3.c ${SRC_LIST})
用法2
- 原型:
add_library(< name> <SHARED|STATIC|MODULE|OBJECT|UNKNOWN> IMPORTED
[GLOBAL])- 参数:
name
:导入已知库的名称;
SHARED|STATIC|IMPORTED..
:通常为SHARED IMPORTED;- 作用:导入一个已经存在的库。注:导入库一般配合set_target_properties使用
- 实例: add_library(libswscale-4 SHARED IMPORTED )
find_library- 原型:find_library (
<VAR>
name1 [path1 path2 …])- 参数:
name1 :- 作用:在path1查找相关库,并将其路径保存到指定变量name中
- 实例:find_library( log-lib log )
- 原型:
set_target_properties(target1 target2 …
PROPERTIES prop1 value1
prop2 value2 …)- 参数:
target1 target2 ...
:目标名称;
prop1 prop2 ...
:代表属性,取值为INCLUDE_DIRECTORIES、IMPORTED_LOCATION、LINK_DIRECTORIES等;
value1 value2 ...
:被设置的属性值;- 作用:设置目标属性值
- 实例:
set_target_properties(libavcodec-57 PROPERTIES IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libavcodec-57.so")
- 原型:
target_link_libraries(< target> … < item>… …)- 参数:
target
:目标库
item
:被连接的库- 作用:将若干库链接到目标库文件,需要注意的是链接的顺序应该符合gcc链接的顺序规则,即被链接的库放在依赖它的库的后面,比如lib1依赖于lib2,lib2依赖于lib3,那么target_link_libraries(name lib1 lib2 lib3)。
- 实例:target_link_libraries(LameMp3 ${log-lib} )
- 原型:
add_subdirectory(source_dir [binary_dir]
[EXCLUDE_FROM_ALL])- 参数:
source_dir
:外部文件夹目录
binary_dir
:用于指定外部文件夹在输出文件夹中的位置- 作用:添加外部项目文件夹到build任务列表中,该文件夹可包含自己的CmakeLists.txt文件。
- 实例:add_subdirectory( ${CASSDK_DIR}/cassdk cassdk.out)
- 原型:
find_library (< VAR> name1 [path1 path2 …])
find_library (
< VAR>
name | NAMES name1 [name2 …] [NAMES_PER_DIR]
[HINTS path1 [path2 … ENV var]]
[PATHS path1 [path2 … ENV var]]
[PATH_SUFFIXES suffix1 [suffix2 …]]
[DOC “cache documentation string”]
[NO_DEFAULT_PATH]
[NO_CMAKE_ENVIRONMENT_PATH]
[NO_CMAKE_PATH]
[NO_SYSTEM_ENVIRONMENT_PATH]
[NO_CMAKE_SYSTEM_PATH]
[CMAKE_FIND_ROOT_PATH_BOTH |
ONLY_CMAKE_FIND_ROOT_PATH |
NO_CMAKE_FIND_ROOT_PATH]
)- 参数:
name1 [name2 ...]
:为库指定一个或多个可能的名称
PATHS
:需指定要搜索的目录- 作用:查找库所在路径。cmake会在目录中查找,如果所有目录中都没有,值log-lib就会被赋为NO_DEFAULT_PATH
- 实例:find_library(log-lib log)
2.2 实战案例:使用ffmpeg第三方框架
在第一个小节中,我们着重介绍了如何在AS中使用Cmake编译第三方源码,使得能够轻易的调用第三方源码中的API实现开发相关的功能动态库。本节将以使用ffmpeg开源框架解析rtsp数据流为例,阐述导入和使用已知第三方库的方法步骤。导入流程如下:
1. 创建Android项目,使其支持C/C++开发,详见 Android NDK开发之旅(2):Android Studio中使用CMake进行NDK/JNI开发(初级)。然后,将ffmpeg相关头文件(compat…ffmpeg.h,如何获取详见Amdroid直播开发之旅(5):详解ffmpeg编译与在Android平台上的移植)拷贝到cpp目录下,如下如所示。其中nativesurface.cpp…vdecode.cpp这几个文件为自定义的C/C++开发源文件。示意图如下:
2. 在工程的main目录下创建jniLibs目录,将编译好的ffmpeg相关库(.so、.a)拷贝到jniLibs目录下,注意不同的架构需放入相应的目录,常见的架构有“armeabi”、“armeabi-v7a”、"x86"等等。示意图如下:
3. 编写CmakeLists.txt脚本文件编译规则,需要实现包括导入ffmpeg相关头文件、导入ffmpeg库、指定自定义构建库信息以及将ffmpeg库链接到自定义库等。CmakeLists.txt代码如下:
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
# 设置so输出路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
# 赋值变量libs
set(libs "${CMAKE_SOURCE_DIR}/src/main/jniLibs")
# 导入ffmpeg相关头文件
include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
#-----------------------------导入ffmpeg库--------------------------------------
add_library(libavcodec-57 SHARED IMPORTED )
set_target_properties(libavcodec-57 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libavcodec-57.so")
add_library(libavdevice-57 SHARED IMPORTED )
set_target_properties(libavdevice-57 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libavdevice-57.so")
add_library(libavfilter-6 SHARED IMPORTED )
set_target_properties(libavfilter-6 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libavfilter-6.so")
add_library(libavformat-57 SHARED IMPORTED )
set_target_properties(libavformat-57 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libavformat-57.so")
add_library(libavutil-55 SHARED IMPORTED )
set_target_properties(libavutil-55 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libavutil-55.so")
add_library(libpostproc-54 SHARED IMPORTED )
set_target_properties(libpostproc-54 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libpostproc-54.so")
add_library(libswresample-2 SHARED IMPORTED )
set_target_properties(libswresample-2 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libswresample-2.so")
add_library(libswscale-4 SHARED IMPORTED )
set_target_properties(libswscale-4 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libswscale-4.so")
#------------------------------配置、链接动态库--------------------------------
#设置变量CMAKE_CXX_FLAGG修改编译选项[只针对c和c++编译器]
#这里使cmkae编译带有c++11特性,其中-std用于指定c/c++标准;
# -fexceptions用于开启编译器异常捕获机制;-frtti用于支持RTTI,配合异常处理
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -fexceptions -frtti")
#指定自定义构建库信息
add_library( # 库名称
decstream
# 库类型为动态库
SHARED
# 将被编译的源文件
src/main/cpp/nativesurface.cpp
src/main/cpp/vdecode.cpp
src/main/cpp/util.cpp
src/main/cpp/openstream.cpp)
#查找NDK中原生库log-lib
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)
#将ffmpeg库、原生库android、log链接到自定义库decstream
target_link_libraries(decstream android log
libavcodec-57
libavdevice-57
libavfilter-6
libavformat-57
libavutil-55
libpostproc-54
libswresample-2
libswscale-4
${log-lib}
)
4. 修改app的gradle文件,配置cmake参数、指定编译的目标平台、关联CmakeLists.txt文件。其中,externalNativeBuild{…}可用于指定cmake编译选项等,ndk选项用于指定需要编译哪种平台的so。代码如下:
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.jiangdg.ffmpeg"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// 配置本地的build选项
externalNativeBuild {
cmake {
// 是否让CMake构建原生库时支持“NEON”,默认的是不支持
// DANDROID_TOOLCHAIN支持CMake构建原生库
arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang","-DCMAKE_BUILD_TYPE=Release"
//'-DANDROID_STL=gnustl_static'
// 指定使用c++11特效,开启异常处理机制
cppFlags "-std=c++11","-frtti", "-fexceptions"
}
}
// 设置输出指定目标平台so
ndk{
abiFilters 'armeabi'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 指定CmakeLists.txt的路径
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
}
考虑到本文的主题以及篇幅不宜过长,这里就不详细解析如何使用ffmpeg框架解析rtsp数据流的代码实现,该部分将在ffmpeg框架系列文章中进行深入讲解。