音视频交叉编译动态库、静态库的学习

#前言
该篇文章主要介绍 Android 端利用 NDK 工具库来对 C/C++ 进行交叉编译,并通过 makefile 和 cmake 来构建 Android 项目。
#编译器
了解 c/c++ 编译器的基本使用,能够在后续移植第三方框架进行交叉编译时,清楚的了解应该传递什么参数。
1. clang

clang 是一个C、C++、Object-C的轻量级编译器。基于LLVM( LLVM是以C++编写而成的构架编译器的框架系统,可以说是一个用于开发编译器相关的库),对比 gcc,它具有编译速度更快、编译产出更小等优点,但是某些软件在使用 clang 编译时候因为源码中内容的问题会出现错误。
2. gcc

GNU C 编译器。原本只能处理 C 语言,但是它很快扩展,变得可处理 C++。( GNU目标是创建一套完全*的操作系统)。

3. g++

GNU c++ 编译器,后缀为 .c 的源文件,gcc 把它当作是 C 程序,而 g++ 当作是 C++ 程序;后缀为 .cpp 的,两者都会认为是 c++ 程序,g++ 会自动链接 c++ 标准库 stl ,gcc 不会,gcc 不会定义 __cplusplus 宏,而 g++ 会。
#编译原理
一个 C/C++ 文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)、和链接(linking)才能变成可执行文件。

我们先在 linux 系统上创建一个 test.c 文件,编写一个最简单的 c 程序,代码如下:
```#include<stdio.h>
int main(){
    printf(" 执行成功 ! \n");
return 19921001;
}
```
1. 预处理阶段

预处理阶段主要处理 include 和 define 等。它把 #include 包含进来的 .h 文件插入到 #include 所在的位置,把源程序中使用到的用 #define 定义的宏用实际的字符串代替。
我们可以通过以下命令来对 c/c++ 文件预处理,命令如下:
```
gcc -E test.c -o test.i //-E 的作用是让 gcc 在预处理结束后停止编译
```
![](https://upload-images.jianshu.io/upload_images/26377919-4f25346d3787f70a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看到输入该命令之后就会生成一个 test.i 文件。

2. 编译阶段

在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作。

我们可以通过如下命令来处理 test.i 文件,编译成汇编文件,命令如下:
```
gcc -S test.i -o test.s//-S 的作用是编译结束生成汇编文件。
```
![](https://upload-images.jianshu.io/upload_images/26377919-881ff5f3961a2cc8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

3. 汇编阶段

汇编阶段把 .S 文件翻译成二进制机器指令文件 .o ,这个阶段接收.c ,.i ,.s 的文件都没有问题。

下面我们通过以下命令生成二进制机器指令文件 .o 文件:
```
gcc -c test.s -o test.o
```
![](https://upload-images.jianshu.io/upload_images/26377919-3c7ac2de8bd2b038.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
最后我们通过实际操作,对编译有了一定的了解,当然你也可以直接通过如下命令一步到位:
```
gcc test.c -o test
```
到这里我们成功的在 linux 平台生成了可执行文件,试想一下我们可以将这个可执行文件拷贝到安卓手机上执行吗?我们也不猜想了,实际测试下就行,我们把 test 可执行文件 push 到手机 /data/local/tmp 里面, 如下所示:
![image.png](https://upload-images.jianshu.io/upload_images/26377919-da92d49aa2e975c0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看到 test 在手机 /data/local/tmp 的路径下是有可读可写可执行的权限,但是最后执行不成功,这是为什么呢? 其实 主要原因是两个平台的 CPU 指令集不一样,根本就无法识别指令。那么怎么解决这个问题呢? 下面就要用到今天一个比较重要的知识点了, 利用 Android NDK 工具包来对 C/C++ 代码进行交叉编译 。

#交叉编译
简单地来说,交叉编译就是程序的编译环境和实际运行环境不一致,即在一个平台上生成另一个平台上的可执行代码。

在音视频开发中了解交叉编译是很有必要的,因为无论在哪一种移动平台下开发,第三方库都是需要进行交叉编译的。下面我们就以之前的例子来讲解如何在 linux 环境下交叉编译出移动平台上的可执行代码。

Android 原生开发包 (NDK) 可用于 Android 平台上的 C++ 开发,NDK 不仅仅是一个单一功能的工具,还是一个包含了 API 、交叉编译器、调试器、构建工具等得综合工具集。

##了解 NDK
下面大致列举了一下经常会用到的组件。

* ARM 交叉编译器
* 构建工具
* Java 原生接口头文件
* C 库
* Math 库
* 最小的 C++ 库
* ZLib 压缩库
* POSIX 线程
* Android 日志库
* Android 原生应用 API
* OpenGL ES 库
* OpenSL ES 库

下面来看一下 Android 所提供的 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 。构建系统会根据具体的配置选择不同的交叉编译器。

下面我们就来为交叉编译的环境变量配置

##环境变量配置
* ndk 在 Linux 上的环境变量配置:
```
//1. vim /etc/profile
#NDK环境变量
export NDK_HOME=/root/android/ndk/android-ndk-r17c
export PATH=$PATH:$NDK_HOME

//2. 保存
source  /etc/profile

//3. 测试
ndk-build -v
```
如果出现如下字样,就证明配置成功了。
![](https://upload-images.jianshu.io/upload_images/26377919-a5542573c8d70223.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* 交叉编译在 Linux 上的环境变量配置(做一个参考,采坑之后的环境配置):
```export NDK_GCC_x86="/root/android/ndk/android-ndk-r17c/toolchains/x86-4.9/prebuilt/linux-x86_64/bin/i686-linux-android-gcc"
export NDK_GCC_x64="/root/android/ndk/android-ndk-r17c/toolchains/x86_64-4.9/prebuilt/linux-x86_64/bin/x86_64-linux-android-gcc"
export NDK_GCC_arm="/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc"
export NDK_GCC_arm_64="/root/android/ndk/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-gcc"

export NDK_CFIG_x86="--sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-x86 -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/i686-linux-android"
export NDK_CFIG_x64="--sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-x86_64 -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/x86_64-linux-android"
export NDK_CFIG_arm="--sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/arm-linux-androideabi"
export NDK_CFIG_arm_64="--isysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm64 -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/aarch64-linux-android"

export NDK_AR_x86="/root/android/ndk/android-ndk-r17c/toolchains/x86-4.9/prebuilt/linux-x86_64/bin/i686-linux-android-ar"
export NDK_AR_x64="/root/android/ndk/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"
export NDK_AR_arm="/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-ar"
export NDK_AR_arm_64="/root/android/ndk/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"
```
你可以根据自己的 ndk 路径对应我的环境变量来进行配置。下面我们就用 ndk gcc 来对 test.c 进行交叉编译,步骤如下:

1. 首先找到 /root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc
![](https://upload-images.jianshu.io/upload_images/26377919-487be64a421c0110.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
执行如下命令:
```
/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc -o test test.c
```
![](https://upload-images.jianshu.io/upload_images/26377919-43357915b727e780.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
这种错误是说在我们编得时候编译器找不到我们引入的 stdio.h 头文件,那怎么告诉编译器 stdio.h 头文件在哪里呢? 下面知识点说明怎么指定这些报错的头文件
2. 指定头文件代码
```
/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -pie -o test test.c
```
上面出现了几个命令符号,不了解了可以看一下如下解释:

--sysroot=?: 使用 ?作为这一次编译的头文件与库文件的查找目录,查找下面的 usr/include 目录。

-isystem ?(主要中间有一个英文空格) : 使用头文件查找目录,覆盖 --sysroot, 查找 ?/usr/include 目录下面的头文件。

-isystem ?(主要中间有一个英文空格): ** 指定头文件的查找路径。
-I?: 头文件的查找目录,I 是大写。

这样编译之后还是会报一个 asm/types.h 文件找不到,我们还要继续修改一下路径,如下
```
/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/arm-linux-androideabi -pie -o test test.c
```
这样就能编译成一个 Android 平台可执行的文件了,这样看起来路径太多不易阅读,大家可以参考我提供的全局变量配置来进行设置,最后一行命令解决,如下:
```
$NDK_GCC_arm $NDK_CFIG_arm -pie -o test test.c
```
![](https://upload-images.jianshu.io/upload_images/26377919-bd08a0c6f02da923.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看到,我们使用 Android NDK 编译出来的可执行文件已经在 Linux 平台下不可执行了。下面我们将 test 文件导入到 手机 /data/local/tmp 目录。

3. 将 NDK 交叉编译出来的 test 可执行文件,导入 Android 手机中并执行 test 文件
![](https://upload-images.jianshu.io/upload_images/26377919-bd965e979b8043b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
根据上面的录屏,我们知道已经成功的在 Android 设备下执行了 NDK 交叉编译后的 test 文件了。

下面我们利用 NDK 工具交叉编译 test.c 输出静态动态库。

#动态库 & 静态库
编译静态库
1. 将 test.c 使用 NDK GCC 编译为 .o 文件 ,命令如下:
```
$NDK_GCC_arm $NDK_CFIG_arm -fpic -c test.c -o test.o
```
![](https://upload-images.jianshu.io/upload_images/26377919-3a4dc62257bf633b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
2 使用 NDK arm-linux-androideabi-ar 工具将 test.o 文件生成 test.a 静态库,命令如下:
```
$NDK_AR_arm r test.a test.o
```
![](https://upload-images.jianshu.io/upload_images/26377919-ee9e43ea1297058f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
之后我们把 test.a 文件导入到 AS 中,来对 .a 的使用。
##编译动态库
在编译动态库的时候我们需要指定 -fPIC -shared 额外参数给编译器,完整命令如下:
```
$NDK_GCC_arm $NDK_CFIG_arm -fpic -shared test.c -o libTest.so
```
##动态库与静态库的区别

在平时工作中我们经常把一些常用的函数或者功能封装为一个个库供给别人使用,java开发我们可以封装为 ja r包提供给别人用,安卓平台后来可以打包成 aar 包,同样的,C/C++ 中我们封装的功能或者函数可以通过静态库或者动态库的方式提供给别人使用。

Linux 平台静态库以 .a 结尾,而动态库以 .so 结尾。

那静态库与动态库有什么区别呢?
1. 静态库

与静态库连接时,静态库中所有被使用的函数的机器码在编译的时候都被拷贝到最终的可执行文件中,并且会被添加到和它连接的每个程序中:

优点:运行起来会快一些,不用查找其余文件的函数库了。

缺点:导致最终生成的可执行代码量相对变多,运行时, 都会被加载到内存中. 又多消耗了内存空间
2. 动态库

与动态库连接的可执行文件只包含需要的函数的引用表,而不是所有的函数代码,只有在程序执行时, 那些需要的函数代码才被拷贝到内存中。

优点:生成可执行文件比较小, 节省磁盘空间,一份动态库驻留在内存中被多个程序使用,也同时节约了内存。

缺点:由于运行时要去链接库会花费一定的时间,执行速度相对会慢一些。
静态库是时间换空间,动态库是空间换时间,二者均有好坏。

如果我们要修改函数库,使用动态库的程序只需要将动态库重新编译就可以了,而使用静态库的程序则需要将静态库重新编译好后,将程序再重新编译一遍。
#总结
该篇文章主要讲解了如何利用 NDK 对 C 程序进行交叉编译,以及交叉编译后的动态静态库在 Android 项目中的使用,本篇文章也是比较基础的,对于后续使用或者编译 FFmpeg 打下基础。

上一篇:字节跳动Android研发岗这些知识点内部泄露出来了,已开源


下一篇:ndk开发教程,Android-Binder机制及AIDL使用,实战篇