Android NDK开发之旅(5):Android Studio中使用CMake进行NDK/JNI开发(高级)

版权声明:精髓原创,欢迎转载、指正、探讨。--蒋东国 https://blog.csdn.net/AndrExpert/article/details/82909572

转载请声明出处: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

  • 原型: 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

  • 原型:include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])
  • 参数:
     AFTER|BEFORE:指定源文件目录后,通常会被附加到编译的当前目录列表中,但也可以通过AFTERBEFORE来改变这种行为方式;
     SYSTEM:如果给出SYSTEM选项,编译器将被告知该目录是系统包含目录。
     dir1:源文件目录路径,可多个,用一个空格隔开;
  • 作用:导入给定源文件目录
  • 实例:include_directories(src/main/cpp/lame)

aux_source_directory

  • 原型:aux_source_directory(<dir> <variable>)
  • 参数:
     dir:源文件所在目录;
     variable:用于存储文件名称列表的变量;
  • 作用:收集指定目录中所有源文件的名称,并将列表存储在提供的variable中
  • 实例:aux_source_directory(src/main/cpp/lame SRC_LIST)

set

  • 原型:
    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 …)

add_library

用法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_directory_properties

  • 原型:
    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_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

  • 原型:
    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

  • 原型:
    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框架系列文章中进行深入讲解。

猜你喜欢

转载自blog.csdn.net/AndrExpert/article/details/82909572