什么是交叉编译?
引自百度百科的定义:
交叉编译
是在一个平台上生成另一个平台上的可执行代码。同一个体系结构可以运行不同的操作系统;同样,同一个操作系统也可以在不同的体系结构上运行,就是说使用其他平台来编译Android或其他系统的库,于是,交叉编译出现。
举例来说,我们常说的x86 Linux平台实际上是Intel x86体系结构和Linux for x86操作系统的统称;而x86 WinNT平台实际上是Intel x86体系结构和Windows NT for x86操作系统的简称。
如何实现交叉编译?
1、编译环境
我们知道PC上的环境和手机上的运行环境是绝然不同的,很显然交叉编译非常重要的,要配置好编译过程中使用到的相关的环境,而这个环境其实就是目标机器(比如Android手机)运行时的环境,比如在Android上开发C/C++,那么他就需要NDK环境去编译,这样在Android上才可以使用。
2、编译工具链
1、对于C/C++的编译,通常有两个工具 GCC 和 CLANG 。GCC 可能大家都有听说过,这是一个老牌的编译工具,不仅可以编译C/C++,也可以编译Java,Object-C,Go等语言。
2、CLANG 则是一个效率更高的C/C++编译工具,并且兼容GCC,Google在很早以前就开始建议使用clang进行编译,并且在 ndk 17 以后,把 GCC 移除了,全面推行使用 CLANG ,感兴趣可以去工具链了解一下。
3、 Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++、Objective-C/Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载(通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。
4、LLVM是构架编译(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
-----------------以上引用百度百科
编译原理
编译就是将高级语言编写的程序转换为二进制目标程序过程,在开始之前先来大概了解编译原理,至少知道每一步做了什么。
预处理
完成宏替换、文件引入,以及去除空行、注释等,为下一步做准备。
-
也就是对各种预处理命令,包括头文件的包含、宏定义、条件编译的选择等。
#include
//test hello int main(){ printf("hello world!\n"); return 0; } 预处理命令:clang -E test.c -o test.i
主要作用就是处理关于 #的指令:
- 删除
#define
,展开所有宏定义。例#define hello "helloworld"
。 - 处理条件预编译
#if, #ifdef, #if, #elif,#endif
。 - 处理
#include
预编译指令,将包含的.h
文件插入对应位置。这可是递归进行的,文件内可能包含其他.h
文件。 - 删除所有注释
/**/,//
。 - 添加行号和文件标识符。用于显示调试信息:错误或警告的位置。
- 保留
#pragma
编译器指令。(1)设定编译器状态,(2)指示编译器完成一些特定的动作
编译
主要作用:1.扫描(词法分析),2.语法分析,3.语义分析,4.源代码优化(中间语言生成),5.代码生成,目标代码优化。
编译命令:clang -S test.i -o test.s
汇编
主要作用:汇编器是将汇编代码转变成可以执行的指令,生成 目标文件。
汇编命令: clang -c test.s -o test.o
链接
主要作用:将多个目标文件以及所需的库文件链接成生成可执行目标文件的过程。
链接命令: clang test.o -o test
静态库(.a)和动态库(.so)
静态库实际就是一些目标文件(一般是.0结尾,即我们汇编产生的文件)的集合,静态库一般以.a结尾,就是经过打包产生.a文件,只用于生成可执行文件阶段。
也就是说当我们需要生成可执行文件时,需要将这个已经打包好的静态库,经过链接器将汇编产生的.o文件的集合即静态库中的代码直接复制到可执行文件。
那么在ndk的工具链中对应了
aarch64-linux-android-ar
,ndk中就是这个可执行文件将.o
打包成静态库:
linux下大打包成静态库命令:ar rcs libtest.a test.o
使用ndk的打包的命令:aarch64-linux-android-ar rcs libtest.a test.o.
aarch64-linux-android-as
这是汇编器动态库在链接阶段没有复制到程序中,而是在程序运行时有系统动态加载到内存中共程序使用。
系统只需载入一次动态库,不同的程序可以得到内存中相同的动态库的副本。
配置ubuntu系统编译环境
首先我使用的是腾讯服务器使用XShell+Xftp操作服务器,为什么使用服务器编译ffmpeg,我个人没有mac电脑,而window系统编译需要配置的环境太复杂,刚好碰到腾讯搞活动索性买了一年,当然使用服务器的好处有很多。
首先你需要先下载NDK。我这里使用的r17c.
1、 NDK 版本 r17c,ffmpeg 版本4.2.3
2、 在ubuntu系统中我们使用命令下载ndk:
wget https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip?hl=zh_cn
解压 : tar xvf ffmpeg-4.2.3.tar
3、下载ffmpeg:
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
4、配置环境变量
vim ~/.bashrc
PATH="$PATH:/usr/local/ffmepg/bin"
export JAVA_HOME=/home/ubuntu/jdk1.8.0_92
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
export NDKROOT=/home/ubuntu/android-ndk-r17c
export PATH=$PATH:$NDKROOT
source ~/.bashrc
实际上NDK的环境可以不用配置,因为我们在编译脚本中已经声明了NDK的路径,所以无需配置,当然你可以直接引用这个环境变量。
编写build for Android的Shell 脚本
在FFMpeg根目录新建sh文件,命名为:build_android.sh,复制脚本到build_android.sh, 内容如下:
#!/bin/bash
#构建脚本,没有输入参数,那么默认是64位,如:32 或 64 分别构建32位 或 64位
archbit=$1
if [ $archbit -eq 32 ];then
echo "Start build for 32bit for ABI"
#32bit
#ABI是Application Binary Interface的缩写。
CPU='arm'
ABI='armeabi-v7a'
#架构 32位
ARCH='arm'
ANDROID='androideabi'
NATIVE_CPU='armeabi-v7a'
else
#64bit
echo "Start build for 64bit for ABI"
#架构 64位
CPU='aarch64'
ABI='arm64-v8a'
ARCH='arm64'
ANDROID='android'
NATIVE_CPU='arm64-v8a'
fi
echo "Biuld param={CPU = $CPU,ABP=$ABI,ARCH=$ARCH,ANDROID=$ANDOIRD,NATIVE_CPU=$NATIVE_CPU}"
#ndk 主目录,如果已经配置了环境变量,可以直接引用环境变量即可
NDKROOT=/home/ubuntu/android-ndk-r17c
#TOOLCHAIN 变量指向ndk中的交叉编译gcc所在目录
TOOLCHAIN=$NDKROOT/toolchains/$CPU-linux-$ANDROID-4.9/prebuilt/linux-x86_64
ISYSROOT=$NDKROOT/sysroot
ASM=$ISYSROOT/usr/include/$CPU-linux-$ANDROID
ADDI_CFLAGS=" -marm"
PREFIX=./android/$NATIVE_CPU
echo "NDKROOT=$NDKROOT,TOOLCHAIN=$TOOLCHAIN"
if [ $archbit -eq 32 ];then
FLAGS=" -I$ASM -isysroot $ISYSROOT -D__ANDROID_API__=21 -U_FILE_OFFSET_BITS -Os -fPIC -DANDROID -Wno-deprecated -mfloat-abi=softfp -marm"
else
FLAGS=" -I$ASM -isysroot $ISYSROOT -D__ANDROID_API__=21 -U_FILE_OFFSET_BITS -Os -fPIC -DANDROID -Wno-deprecated"
fi
echo -e "\033[32m build start \033[0m"
./configure \
--prefix=$PREFIX \
--enable-small \
--disable-programs \
--disable-avdevice \
--disable-encoders \
--disable-muxers \
--disable-filters \
--disable-doc \
--enable-cross-compile \
--cc=$TOOLCHAIN/bin/$CPU-linux-$ANDROID-gcc \
--enable-neon \
--cross-prefix=$TOOLCHAIN/bin/$CPU-linux-$ANDROID- \
--enable-static \
--disable-shared \
--sysroot=$NDKROOT/platforms/android-21/arch-$ARCH \
--extra-cflags="$FLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
--arch=$CPU \
--target-os=android
#执行清理
sudo make clean
#执行脚本生成的 makefile
sudo make -j8
sudo make install
echo -e "\033[32m build successful\033[0m"
最后在中端输入:
sudo ./build_android.sh 32
如果想构建64位就把32改为64,执行脚本。
编译参数详解
-
ndk 主目录
NDKROOT=/home/ubuntu/android-ndk-r17c
实际上我已经配置好了NDK环境变量,可以直接在编译脚本$NDKROOT使用。
-
TOOLCHAIN 变量指向ndk中的交叉编译gcc所在目录,这些我们在编译脚本中通过条件逻辑需要编译那种类型的cup架构,而在工具链中32位和64位的工具链目录是不一样的,详情参照编译脚本当中的配置。
TOOLCHAIN=$NDKROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/
-
-extra-cflags 会传值给gcc的参数, FLAGS和INCLUDES变量
可以在 AS NDK工程中的.cxx\cmake\debug\armeabi-v7a\build.ninja拷贝,需要注意的修改地址, 替换ndk主目录,删除-IE:/AndroidStudioprojects/NDKExampl/app/src/main/cpp/inc和- LE:/AndroidStudioprojects/NDKExampl/app/src/main/cpp/../jniLibs/armeabi-v7a,需要注意的是如果你加入不被支持的选项,会出现C compiler test failed的错误,具体哪些不被识别,请报错之后查看../ffbuild/config.log文件,比如在编译arm64-v8a时.那就把不识别的删掉就可以:../prebuilt/linux-x86_64/bin/arm-linux-androideabi-clang is unable to create an executable file.C compiler test failed.
--disabble-static --enable-shared 分别用于禁止输出静态库,以及输出动态库;
./confiure 脚本,用于生成makefile
--prefix : 安装目录,编译后文件的输出目录,用于配置输出的so库的存放路径。
--enable--small 优化大小
--disable-programs 不编译ffmpeg程序(命令行工具),我们是需要获得静态(动态)库
--disable-avdevice 关闭avdevice模块,此模块在android中无用
--disable-encoders 关闭所有编码器(播放器不需要编码)
--disable-muxers 关闭所有复用器(封装器),不需要生成mp4这样的文件,所以关闭
--disable-filters 关闭视频滤镜(抖音那种开启)
--enable-cross-compile 开启交叉编译(ffmpeg比较 跨平台 并不是所有库都有这么happy的选项)
--cross-prefix 这个选项直译为 交叉编译前缀,指的是交叉编译工具的前缀,他这个是找到ndk 中的工具链中的可执行的编译工具来进行编译的。
设置编译器,不然会报错,系统默认会使用arm-linux-androideabi-clang,但是此编译器在NDK不存在,导致编译失败。--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \ 设置编译器,不然会报错,系统默认会使用arm-linux-androideabi-clang,但是此编译器在NDK不存在,导致编译失败,这个和上一点有关联的
--sysroot 用于配置交叉编译环境的 根路径 ,编译的时候会默认从这个路径下去寻找 usr/include usr/lib 这两个路径,进而找到相关的头文件和库文件。
--arch 设置编译so库的架构,当前设置为arm,可以根据实际需求修改
--target-os=android 设-target-os=android:在旧版本的 FFmpeg 中,对Android平台的支持并不是很完善,并没有 android 这个target,所以在一些比较老的文章中都会提到,编译Android平台的so库,需要对 configure 做以下修改,否则会按照 linux 标准的方式输出so库,其命名方式和Android的so不一样,Android是无法加载的。
--arch --cpu 用于配置输出的so库是什么架构的
需要注意的是:
-
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \ 这条指定我整了几天,这个参数就是指定编译器,不然会报错,系统默认会使用arm-linux-androideabi-clang,但是此编译器在NDK不存在,导致编译失败:
../prebuilt/linux-x86_64/bin/arm-linux-androideabi-clang is unable to create an executable file.C compiler test failed.
就是ndk中文件的权限一定是要有可执行文件的权限。
--extra-cflags里不要加不被支持的选项,否则会出现C compiler test failed的错误,具体哪些不被识别,请报错之后查看../ffbuild/config.log文件,比如在编译arm64-v8a时.那就把不识别的删掉就可以了:
aarch64-linux-android-gcc: error: unrecognized command line option '-mfloat-abi=softfp'
编译完成最后会在我们制定的prefix目录中生成编译的库:
使用CLANG编译FFmpeg
下载Android NDK
Android 的 NDK 已经迭代了很多版本,在 r17c 以后,Google正式移除 GCC ,不再支持 GCC ,新版本的 NDK 都是使用 CLANG 进行编译。
这里就使用目前最新的 NDK r21 版本来编译。
最主要的就是这两个路径:
编译工具链目录:
toolchains/llvm/prebuilt/linux-x86_64/bin
交叉编译环境目录:
toolchains/llvm/prebuilt/linux-x86_64/sysroot
在NDK r21这两个路径和NDKr17是不同的。
我们选择 CPU 架构 armv7a,Android版本 21,当然你可以选择最低支持版本比如19也是可以的。
这个两个是交叉编译的工具链一个是编译C的另一个是编译C++的:
armv7a-linux-androideabi21-clang
armv7a-linux-androideabi21-clang++
下面就是编译的Shell脚本:
#!/bin/bash
#构建日志输出文件
LOG_FILE=./android/config.log
#ndk 主目录,如果已经配置了环境变量,可以直接引用环境变量即可
NDKROOT=/home/ubuntu/android-ndk-r21
#TOOLCHAIN 变量指向ndk中的交叉编译gcc所在目录
TOOLCHAIN=$NDKROOT/toolchains/llvm/prebuilt/linux-x86_64
ISYSROOT=$NDKROOT/sysroot
ASM=$ISYSROOT/usr/include/arm-linux-androideabi
ADDI_CFLAGS=" -marm"
PREFIX=./android2/armeabi-v7a
echo "NDKROOT=$NDKROOT,TOOLCHAIN=$TOOLCHAIN"
FLAGS=" -I$ASM -isysroot $ISYSROOT -D__ANDROID_API__=21 - U_FILE_OFFSET_BITS -Os -fPIC -DANDROID -Wno-deprecated -mfloat-abi=softfp -marm"
echo -e "\033[32m build start \033[0m"
./configure \
--prefix=$PREFIX \
--enable-small \
--disable-programs \
--disable-avdevice \
--disable-encoders \
--disable-muxers \
--disable-filters \
--disable-doc \
--enable-cross-compile \
--cc=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-neon \
--enable-static \
--disable-shared \
--sysroot="$TOOLCHAIN/sysroot" \
--extra-cflags="$FLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
--arch=arm \
--target-os=android
#执行清理
sudo make clean
#执行脚本生成的 makefile
sudo make -j8
sudo make install
echo -e "\033[32m build successful\033[0m"
需要注意ndk-r21 已经把sysroot移动到 $TOOLCHAIN/sysroot
,而r17c的 sysroot还在 $NDKROOT/platforms/android-21/arch-arm
注意ndk-r21中 --cc也是要指定的,因为他的cross-prefix命名规则不一样了:
比如:--cc=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang和cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi-
明显不一样了。
ndk r21版本-----------------------默认的 cc ar nm 路径前缀是一样的,但是在r21之后NDK的ar/nm 和 cc的前缀是不一样了,如下:
- 工具链:/home/ubuntu/android-ndk-r21/toolchains/llvm/prebuilt/linux-x86_64/bin
1、构建armeabi64-v8a:
aarch64-linux-android-ar
aarch64-linux-android-nm
aarch64-linux-android21-clang
2、构建armeabi-v7a:
arm-linux-androideabi-ar
arm-linux-androideabi-nm
armv7a-linux-androideabi21-clang
这个问题怎么解决,网上有人说修改ffmpeg的configure文件增加corss-prefix-clang命令,是可以解决这个问题,但是我们有更简单的?
这个时候就需要:
指定C的编译工具--cc=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang
指定交叉编译工具前缀而这个前缀 会匹配到ar、nm等编译工具--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi-
那么我们看看ffmpeg中configure的脚本:
ar_default="ar"
cc_default="gcc"
cxx_default="g++"
host_cc_default="gcc"
doxygen_default="doxygen"
//如果是android 那么就是clang
set_default target_os
if test "$target_os" = android; then
cc_default="clang"
fi
//cross_prefix默认是[] 意思是"",所以一般我们都需要指定cross_prefix
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
nm_default="${cross_prefix}${nm_default}"
看到了吗?ar/nm 和 cc的前缀是不一样的,前者是 arm-linux-androideabi- , 后者是 armv7a-linux-androideabi16-。
所以我们直接在命令中直接修改覆盖了默认的值:
--cc=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang
覆盖默认值就可以了,那么ar和nm我们可以使用默认值。
最后我建议大家都换成Clang编译,Clang会对编译的代码做优化,感兴趣的可以连接Clang编译器。
然后要记住每次官方发布新版NDK时自行使用最新的版本重新编译
参考链接
在Android项目中调用FFmpeg命令
在Android项目中调用FFmpeg命令
FFmpegAndroid
EpMedia