Android 音视频基础6——交叉编译动态库

前言

前面学了一些Android音视频的基础,包括AudioTrack、AudioRecorder、MediaRecorder、MediaMuxer、MediaExtractor、MediaCodec以及Camera API,这些是Android提供的基础内容,而要深入了解音视频编译一些优秀的开源项目是必须的,这就要求掌握交叉编译的基础。

交叉编译

简单地来说,交叉编译就是程序的编译环境和实际运行环境不一致,即在一个平台上生成另一个平台上的可执行代码。Android要使用交叉编译,就离不开NDK(Native Develop Kit),即原生开发工具集,它包含了API、交叉编译器、调试器、构建工具等,是一个综合的工具集,设计到以下常用组件:

  • ARM交叉编译器(对应于toolschains)
  • 构建工具(对应于build目录)
  • Java原生头文件
  • C库
  • Math库
  • 最小的C++库
  • ZLib库
  • POSIX线程
  • Android日志库
  • Android原生应用API
  • OpenGL ES库
  • OpenSL ES库

其目录结构如下:

  • ndk脚本
    • ndk-build,该 Shell 脚本是 Android NDK 构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了;
    • ndk-gdb,该 Shell 脚本允许用 GUN 调试器调试 Native 代码,并且可以配置到 AS 中,可以做到像调试 Java 代码一样调试 Native 代码;
    • ndk-stack,该 Shell 脚本可以帮组分析 Native 代码崩溃时的堆栈信息;
  • build目录,包含了NDK构建系统的所有模块;
  • platforms,该目录包含支持不同 Android 目标版本的头文件和库文件, NDK 构建系统会根据具体的配置来引用指定平台下的头文件和库文件;
  • toolchains,该目录包含目前 NDK 所支持的不同平台下的交叉编译器 - ARM 、X86、MIPS ,目前比较常用的是 ARM 。构建系统会根据具体的配置选择不同的交叉编译器;

编译前的配置

首先,在Mac的Android Studio中下载对应的ndk版本,具体如下:

Cmd + ,调出Preferences设置面板,选择Android SDK->SDK Tools,就可以看到NDK的下载配置了,勾选右下角的show package details就可以看到不同的NDK版本;

然后,配置NDK的环境变量,使用文本工具打开.bash_profile文件,按如下输入:

1
2
export NDK_HOME=你的NDK安装目录
export PATH=$PATH:$NDK_HOME

修改后保存,打开终端,输入:

1
source ~/.bash_profile 

最后输入如下命令测试环境变量是否配置成功

1
2
3
4
5
6
7
8
9
ndk-build -v
// 输入如下内容证明配置成功:
GNU Make 3.81
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for x86_64-apple-darwin

尝试编译

先写一个简单的test.c文件

1
2
3
4
5
#include <stdio.h>
int main(){
printf(" 执行成功 ! \n");
return 19921001;
}

上面配置好后,就尝试编译以下test.c文件(文件位于~/Desktop目录),输入下面命令,然后运行:

1
2
3
4
5
6
7
/Users/randy/Library/Android/sdk/ndk-r17/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc -o test test.c

// 输出如下
test.c:1:19: fatal error: stdio.h: No such file or directory
#include <stdio.h>
^
compilation terminated.

上面错误表示找不到头文件,所以要想编译生工,我们必须告诉编译器头文件在哪儿?

指定头文件编译

上面找不到stdio.h文件,利用Mac的查找功能,在NDK根目录下查找,发现该文件位于/Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include中,修改如下:

1
2
3
4
5
6
7
/Users/randy/Library/Android/sdk/ndk-r17/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/Users/randy/Library/Android/sdk/ndk-r17/platforms/android-21/arch-arm -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include -pie -o test test.c
// 输出如下:
In file included from /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/sys/types.h:36:0,
from /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/stdio.h:42,
from test.c:1:
/Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/linux/types.h:21:23: fatal error: asm/types.h: No such file or directory
#include <asm/types.h>

这回提示找不到asm/types.h文件,同样的方法,先查找一下该文件,发现它在这个目录中:

/Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/arm-linux-androideabi,所以同样需要通过-isystem ?来指定这个路径,修改后再运行:

1
2
/Users/randy/Library/Android/sdk/ndk-r17/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/Users/randy/Library/Android/sdk/ndk-r17/platforms/android-21/arch-arm -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/arm-linux-androideabi -pie -o test test.c
// 没有报错,正常运行,同时发现Desktop目录下生产了test文件

// 执行一下命令看看能否运行这个test文件

1
2
3
4
./test
// 输出如下
zsh: exec format error: ./test
// 即可执行文件格式错误,者个输出结果是正常的,因为这个test文件是针对升级平台的,Mac下无法运行

将该文件推送到手机,然后运行:

1
2
3
4
5
6
adb push test /data/local/tmp // 推动到手机
// 在手机的shell 环境下运行
cd /data/local/tmp
./test
// 输出结果
执行成功

基于以上步骤,我们利用NDK交叉编译(Mac平台编译手机平台可以直接运行的test文件)成功了。

关于上面用到的选项可以使用—help来查看具体说明(这是gcc的帮助查看选项),还可以参考

动态库和静态库

Linux平台下静态库以.a(Ar Archive类型文件)结尾,动态库以.so结尾。

编译动态库

利用arm-linux-androideabi-g++生成.so文件,具体如下:

1
/Users/randy/Library/Android/sdk/ndk-r17/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/Users/randy/Library/Android/sdk/ndk-r17/platforms/android-21/arch-arm -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/arm-linux-androideabi -fpic -shared test.c -o libTest.so

运行完会发现test.c所在目录下多了一个libTest.so文件;

编译静态库

利用arm-linux-androideabi-g++生成.o文件,利用arm-linux-androideabi-ar生成.a文件,具体步骤如下:

1
2
3
4
5
6
7
// arm-linux-androideabi-g++生成.o文件
/Users/randy/Library/Android/sdk/ndk-r17/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/Users/randy/Library/Android/sdk/ndk-r17/platforms/android-21/arch-arm -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include -isystem /Users/randy/Library/Android/sdk/ndk-r17/sysroot/usr/include/arm-linux-androideabi -fpic -c test.c -o test.o
// 上面的命令表示编译test.c文件,并输出test.o文件

// arm-linux-androideabi-ar 生成.a文件
/Users/randy/Library/Android/sdk/ndk-r17/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-ar r test.a test.o
// 上面的命令表示将test.o打包成test.a文件

运行完成后就会发现在test.c这个文件所在目录下多了一个test.a,这样我们就生成了test.a这个静态库了。

动态库与静态库的区别

在与静态库链接时,静态库中所有被使用的函数的机器码在编译的时候都被拷贝到最终的可执行文件中,并且会被添加到和它链接的每个程序中,这样运行时就不用在找它以来的函数库了(都拷贝到了可执行文件中),运行速度较快,当然,这也导致最终的可执行文件代码变多,占用内存变多,总的来说就是用空间换时间

与动态库链接时,与动态库链接的可执行文件只包含需要函数的引用表,而不是所有的函数代码,只在程序执行时那些需要的函数代码才被拷贝到内存中,这就和静态库相比,他的可执行文件代码少,占用内存小,但由于运行时需要链接那些函数库(因为它本身只有函数的引用,需要根据引用去加载),导致执行时间慢一些,总的来说就是时间换空间。

还有一点需要注意的是,如果我们要修改函数库,使用动态库的程序只需要将动态库重新编译就可以了,而使用静态库的程序则需要将静态库重新编译好后,将程序再重新编译一遍

mk和CMake

上面讲的是利用NDK将c文件交叉编译成动态库和静态库,但实际项目中makefile和cmake来构建一个C/C++的Android程序,并使用上面生成的静态库test.a和动态库testLib.so。

mk

Android.mk

关于Android.mk文件的详细说明,请参考Google官网的Android.mk说明,这里只列出部分常用内容。

Android.mk文件位于项目jni目录的子目录中,用于向构建系统描述源文件和共享库。本质上是一个微小的GNU makefile 片段,构建系统会将其解析一次或多次。Android.mk 文件用于定义 Application.mk、构建系统和环境变量所未定义的项目级设置。它还可替换特定模块的项目级设置。

  • LOCAL_PATH := $(call my-dir),Android.mk文件使用前必须先定义一个LOCAL_PATH,此变量表示源文件在开发树种的位置,即调用my-dir函数,返回当前文件所在的目录;
  • include $(CLEAR_VARS)
  • LOCAL_MODULE := test,LOCAL_MODULE变量存储要构建的模块的名称,这个名称对于每个模块来说要是唯一的,不能包含空格。构建系统最终生成共享文件,会自动为LOCAL_MODULE添加前缀或后缀:libhello-jni.so;
  • LOCAL_SRC_FILES := test.c 列举用来生成库的源文件,多个用空格隔开;
  • include $(BUILD_SHARED_LIBRARY).mk文件的最后一行,其实类似的 include 还有很多,都是构建系统提供的内置变量,该变量的意义是构建动态库,其他的内置变量还包括如下几种:
    • BUILD_SHARED_LIBRARY,构建动态库;
    • BUILD_STATIC_LIBRARY,构建静态库;
    • PREBUILT_STATIC_LIBRARY: 对已有的静态库进行包装,使其成为一个模块,此时LOCAL_SRC_FILES不能是源文件,而是.a(静态库文件);
    • PREBUILT_SHARED_LIBRARY: 对已有的静态库进行包装,使其成为一个模块此时LOCAL_SRC_FILES不能是源文件,而是`.so(动态库文件);

Application.mk

Application.mk指定ndk-build的项目级设置,主要定义提供项目级别的变量控制,常用的有如下:

  • APP_ABI,指定要进行哪些ABI平台的构建,默认为all,多个用空格隔开,如果gradle文件的externalNativeBuild指定了abiFilters,则会忽略APP_ABI的设置
指令集
32 位 ARMv7 APP_ABI := armeabi-v7a
64 位 ARMv8 (AArch64) APP_ABI := arm64-v8a
x86 APP_ABI := x86
x86-64 APP_ABI := x86_64
所有支持的 ABI(默认) APP_ABI := all
  • APP_DEBUG,构建应用是否可调式(True为可调式);
  • APP_MODULES,要构建的模块的显式列表,此列表的元素是模块在 Android.mk 文件的 LOCAL_MODULE 中显示的名称。默认情况下,ndk-build 将构建所有共享库、可执行文件及其依赖项;
  • APP_PLATFORM,会声明构建此应用所面向的 Android API 级别,并对应于应用的 minSdkVersion
  • APP_PROJECT_PATH,项目根目录的绝对路径;
  • APP_STL,用于此应用的C++标准库;

下面简单写下在采用mk构建是gradle文件的要点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apply plugin: 'com.android.application'

android {
compileSdkVersion 30
buildToolsVersion "30.0.0"

defaultConfig {
...

externalNativeBuild {
ndkBuild {
// 表明支持哪些ABI
abiFilters 'arm64-v8a'
}
}
}

buildTypes {
release {
...
}
}

externalNativeBuild {
ndkBuild {
// make file文件路径
path 'src/main/jni/Android.mk'
}
}

}

dependencies {
...
}

CMake

关于CMake的介绍,请参考CMake官网,在Android Studio 2.2之前的版本,做NDK开发采用的是Android.mk、Application.mk来进行C/C++项目的构建的,Android Studio 2.2之后,就采用CMake来构建了,CMake构建较Android.mk、Application.mk更方便。

CMake主要包含了四类命令:

  • Scripting Command,脚本命令,只要支持CMake,这些命令都可用;
  • Project Command,这些命令只有在CMake 项目中才能使用;
  • CTest Command,这些命令只能在CTest脚本中才能使用;
  • Deprecated Command,已经废弃的命令,这些命令只为了向后兼容;

这里主要介绍脚本命令中在Android C/C++项目中编译用到的命令。

基础如法

下面这个代码演示了CMake的基本语法,说明参见注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 指定CMake最小版本
cmake_mimum_required(VERSION 3.10)
# 设置项目名称
project(demo)
# 设置编译类型
add_executable(demo test.cpp) # 生成可执行文件
add_library(common STATIC test.cpp) # 生成静态库
add_library(common SHARED test.cpp) # 生成动态库

# 明确指定包含哪些源文件
add_library(demo test.cpp test1.cpp test2.cpp)

# 自定义搜索规则并加载文件,这里参数的意义可以查询CMake官网说明
file(GLOB SRC_LIST "*.cpp" "protocol/*.cpp") # 类似于正则匹配(但更简单),找到符合规则的文件,然后以列表形式存入SAR_LIST中
add_library(demo ${SRC_LIST}) #加载上面找到的文件

## 或者
file(GLOB SRC_LIST "*.cpp")
file(GLOB SRC_PROTOCOL "*.cpp")
add_library(demo ${SRC_LIST} ${SRC_PROTOCOL})

# 查找指定的库文件,找到NDK下的lib库(路径),并将其存入log-lib变量中
find_library(
log-lib # 这个log-lib是自己指定的一个变量
lib # lib指定是NDK下的lib库
)

# 设置包含的目录
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 设置链接库搜索目录
link_directories(
${CMAKE_CURRENT_SOURCE_DIR}/libs
)

# 设置目标库需要链接的库
target_link_libraries(
demo # 目标库
${log-lib} # demo库要用到NDK的lib,通过这个将其链接起来
)

# 指定链接动态库或者静态库
target_link_libraries(demo libtest.a) # 链接libtest.a
target_link_libraries(demo libtest.so)# 链接libtest.so

上面的$CMAKE_CURRENT_SOURCE_DIR是CMake定义的一个常量,类似的常量可以参见CMake 常量

配置CMake

CMake构建脚本是一个纯文本文件,必须命名为CMakeLists.txt,并在其中包含CMake构建C/C++库时需要用到的命令。如果原生源代码文件没有改文件,需要自己建立一个,并在其中包含适当的CMake命令。例如Android Studio C/C++项目模板生成的CMakeList.txt如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 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)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your 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 )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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 )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib} )

介绍了上面的基础,下面实际操作一把。

CMake构建C/C++项目

静态库构建

  1. Activity中定义native接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package com.rainmonth;

    import androidx.appcompat.app.AppCompatActivity;

    import android.os.Bundle;
    import android.widget.TextView;

    public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
    System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Example of a call to a native method
    TextView tv = findViewById(R.id.sample_text);
    tv.setText(stringFromJNI());
    }

    /**
    * 定义原生接口
    */
    public native String stringFromJNI();
    }
  2. 编写cpp文件,在项目的main目录下新建cpp目录,然后新建文件native-lib.cpp,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <jni.h>
    #include <string>
    #include <android/log.h>

    extern "C" {
    int main();
    }

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_rainmonth_MainActivity_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    std::string hello = "Hello from C++";
    // 注意和.c的区别

    __android_log_print(ANDROID_LOG_DEBUG, "Randy", "main---->:%d", main());
    return env->NewStringUTF(hello.c_str());
    }
  3. 编写CMakeList.txt文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 指定最小版本
    cmake_minimum_required(VERSION 3.4.1)

    # 打印日志
    message("当前CMake路径:${CMAKE_SOURCE_DIR}")
    message("当前CMAKE_ANDROID_ARCH_ABI路径:${CMAKE_ANDROID_ARCH_ABI}")

    # 批量引入源文件
    file(GLOB allCpp *.cpp)

    add_library( # 设置库的名称
    native-lib
    # 设置为动态库
    SHARED
    # Provides a relative path to your source file(s).
    ${allCpp} )

    # 表明要静态导入test_a这个库
    add_library(test_a STATIC IMPORTED)

    # test_a这个库的真正位置在 ${CMAKE_SOURCE_DIR}/test.a
    set_target_properties(test_a PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/test.a)

搜索指定的预构建库并将路径存储为变量,对于NDK库,只需指定名称,因为CMake默认include路径包含NDK路径

find_library( # 找到的库存储到的位置
log-lib

             # 要查找的NDK库名称
             log )

message(“当前的log路:${log-lib}”)

开始连接指定的库

target_link_libraries( # Specifies the target library.
native-lib
${log-lib}
test_a
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
4. build.gradle配置

```groovy
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
// cFlags ""
// cppFlags ""
// arguments ""
// abiFilters ""
// targets ""
//
abiFilters 'armeabi-v7a'//编译armeabi-v7a平台

}
}

ndk {
abiFilters 'armeabi-v7a'//编译armeabi-v7a平台
}
}
...
sourceSets {
main {
jniLibs.srcDirs = ['jniLibs']
}
}

externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
// project ""
}
}
}

dependencies {
...
}

运行项目,运行结果,test.a静态库中定义的函数main正确执行。

下面看动态库的构建

动态库构建

  1. 与静态库类似,只是需要将Test.so库加载进去,其他部分相同:

    1
    2
    3
    4
    static {
    System.loadLibrary("Test");
    System.loadLibrary("native-lib");
    }
  2. 将生成的动态库加入到main目录下的jinLibs文件夹下相应的ABI目录里面;

  3. 编写动态编译的CMakeList.txt:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    ####################动态构建开始####################
    cmake_minimum_required(VERSION 3.4.1)

    message("动态构建开始>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")

    # 打印日志
    message("当前CMake路径:${CMAKE_SOURCE_DIR}")
    message("当前CMAKE_ANDROID_ARCH_ABI路径:${CMAKE_ANDROID_ARCH_ABI}")

    # 批量引入源文件
    file(GLOB allCpp *.cpp)

    add_library( # 设置库的名称
    native-lib
    # 设置为动态库
    SHARED
    # Provides a relative path to your source file(s).
    ${allCpp})

    # 导入动态库
    add_library(test_so SHARED IMPORTED)
    # 早起的cmake ANDROID_ABI == 当前CPU平台
    set_target_properties(test_so PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libTest.so)

    # 搜索指定的预构建库并将路径存储为变量,对于NDK库,只需指定名称,因为CMake默认include路径包含NDK路径
    find_library( # 找到的库存储到的位置
    log-lib
    # 要查找的NDK库名称
    log)
    message("当前的log路:${log-lib}")

    # 开始连接指定的库
    target_link_libraries( # Specifies the target library.
    native-lib
    ${log-lib}
    test_so
    )

    ####################动态构建结束####################
  4. gradle文件配置同上面静态库的配置。

    上面的的配置在实际运行时,System.loadLibrary("native-lib") 这一行总报找不到libTest.so这个库,暂时没找到原因。

总结

以上就是交叉编译动态库的一些基础知识,还有就是Android通过CMake来编译动态库和静态库的一些实际操作,算是为编译FFMpeg做个预热了。