简介面向 iOS 音视频开发者的 FFmpeg 多架构交叉编译资料包围绕 armv7、armv7s、arm64 与 i386 四种目标环境整理适合需要在真机与模拟器上统一集成音视频处理能力的中高级开发者参考。压缩包共 2000 个文件约 50.66MB以 1782 个 C 源文件、1167 个头文件及 90 个汇编文件为主体并包含 configure、Makefile、pc 配置、测试样本和少量脚本能够覆盖从源码配置、交叉编译到多架构产物合并的学习链路。目录中保留了 libavcodec、libavformat 等模块及 test02ffmpeg 验证文件便于对照架构参数、编译选项和 lipo 合并结果排查真机与模拟器适配问题。已有 380 人学习下载可作为 iOS 端 FFmpeg 编译实践与工程集成的参考资料。1. 为什么要自己编 FFmpeg四个架构不是复制粘贴是四份不同的产物第一次接到ffmpeg 编译 armv7/armv7s/arm64/i386这种需求很多人的第一反应是编一次把.a复制四份改个名字不就完了。真动手走三步就会断——lipo -info一看四个库里躺着的全是同一种机器码链到 32 位模拟器上直接报符号缺失。这四个架构对应的是三套完全不同的工具链配置armv7 和 armv7s 共用 32 位 ARM 后端但 CPU 特性表不一样arm64 是另一套寄存器模型和汇编文件i386 干脆换到了 x86 后端还得用模拟器 SDK 里的 sysroot。配置错一位编译可能照样过但运行时崩在第一条 NEON 指令上排查成本比重新编一次高得多。这篇笔记讲的是怎么用一套脚本把四条分支跑顺、怎么裁剪体积、以及哪几个坑几乎人人都会踩一遍。2. 四套指令集不是四份配置先把 armv7、armv7s、arm64、i386 的边界画清楚2.1 一张表看清四个 target 的位宽、指令集与真实运行位置很多人把 armv7 和 armv7s 当成差不多把 i386 当成就是模拟器版这两种理解都会导致后面的参数乱填。先把差异摆到台面上。架构位宽关键指令集特性实际运行位置现阶段还要不要armv732 位ARMv7-ANEON 可选VFPv332 位真机早期 A 系列芯片只在老工程/老设备矩阵里还有意义armv7s32 位ARMv7-A VFPv4针对特定核心优化极少数 32 位机型基本可以砍掉arm6464 位ARMv8-AAArch64NEON 为标配现代真机 新 Mac 上的模拟器必须有i38632 位x86SSE2 起步老 Intel Mac 上的 32 位模拟器老工具链才编得出来armv7s 的特殊之处在于它是同一代指令集的一个超集变体给某个特定核心加了 VFPv4 和若干调度优化。它跑在 armv7 设备上没问题但反过来 armv7 的二进制跑不了它的专属指令——所以真正有意义的做法是 armv7 的库作为兜底armv7s 在lipo合并时作为独立 slice 放进去让系统按设备挑。合并时它不是更高级的替代品只是多一条分支。i386 的坑在于它跟真机 SDK 完全是两套 sysroot。iphoneos的 SDK 里根本没有 x86 目标iphonesimulator的 SDK 里也没有 ARM 目标。所以四个架构不是同一份脚本改一个变量而是三份参数 一个 SDK 切换。还有一个容易忽略的事实arm64 现在同时存在真机 arm64和模拟器 arm64新 Mac 上跑模拟器时编译出来的产物。两者架构名一样平台不一样链接器认的是平台而不是名字。这一点放到最后一章单独说因为它是 fat 库方案崩掉的地方。2.2 交叉编译的三件套toolchain、sysroot、target 各管一段交叉编译这件事本质上要同时告诉三拨人三件事缺一件就编不对告诉编译器clang输出什么机器码靠-arch armv7这类参数。告诉编译器和链接器去哪找系统头文件和系统库靠--sysrootSDK 路径。告诉 FFmpeg 自己的构建系统该启用哪些汇编文件、哪些内联汇编分支靠--arch和--cpu。这里最容易翻车的是第二组和第三组参数不一致。举个例子--archarm、--cpucortex-a9会让 FFmpeg 打开libavcodec/arm/下面一批 NEON 汇编但如果--extra-cflags里没带-arch armv7clang 会按宿主机的默认架构走编出来的是宿主机的目标文件等到lipo合并时才发现 slice 名字对不上。所以我的习惯是--arch/--cpu决定编哪些源文件-arch决定编成什么两边必须逐条对照。对照关系大致是这样目标 sliceconfigure 的--archconfigure 的--cpuclang 的-archSDKarmv7armcortex-a9armv7iphoneosarmv7sarmswiftarmv7siphoneosarm64arm64arm64arm64iphoneosi386x86不指定i386iphonesimulator--cpuswift这个值听起来奇怪但它是 FFmpeg 里对那一代核心的代号用来打开 VFPv4 相关的分支。arm64 那条我一般显式写--cpuarm64不写也能过但写了之后生成的 config.h 更容易一眼看懂。模拟器那两条我刻意不写--cpu让它按 SDK 的默认目标走。原因是 x86 后端的--cpu一旦写成i386会顺带关掉 SSE 系列优化编出来的库能跑但慢一截而模拟器只在开发期用这条分支的收益本来就不在性能上。2.3 32 位这三条分支到底该留还是该扔新手最常见的浪费是把四个架构一直当必选项供着每次 CI 跑满四遍。判断标准其实很简单如果你的最低支持版本落在还需要 32 位设备的区间armv7 必须留armv7s 只在你有明确的老设备矩阵、且做过对比测试证明它有收益时才留否则它就是纯体积负担——合并进 fat 库后每个.a都会变大而绝大多数设备永远走不到那一条 slice。i386 更干脆新版本工具链已经把 32 位模拟器的目标从 SDK 里拿掉了你连编都编不出来。留着它的唯一场景是你被锁在老工具链上还得维护老工程。至于 arm64没有讨论余地。真机和模拟器两条 arm64 都要只是打包方式要换这一点第 6 章会展开。3. 把 FFmpeg 砍到能装进包里configure 参数、裁剪顺序与外部库取舍3.1 一条能跑通的最小 configure 命令先给一条能直接跑起来的命令以 arm64 为例。跑通它比读十页参数说明有用。#!/usr/bin/env bash # 单架构最小可跑示例arm64 真机 set -euo pipefail SDK$(xcrun --sdk iphoneos --show-sdk-path) # 取当前选中的 SDK 真实路径 MIN_IOS11.0 ./configure \ --prefix$(pwd)/build/arm64 \ --target-osdarwin \ --archarm64 \ --cpuarm64 \ --ccxcrun -sdk iphoneos clang \ --cxxxcrun -sdk iphoneos clang \ --sysroot$SDK \ --enable-cross-compile \ --enable-pic \ --disable-shared --enable-static \ --disable-programs --disable-doc \ --disable-avdevice --disable-postproc \ --disable-swscale-alpha \ --disable-network \ --disable-everything \ --enable-decoderh264,aac \ --enable-demuxermov \ --enable-parserh264,aac \ --enable-protocolfile \ --enable-swresample \ --enable-small \ --extra-cflags-arch arm64 -miphoneos-version-min$MIN_IOS \ --extra-ldflags-arch arm64 -miphoneos-version-min$MIN_IOS make -j$(sysctl -n hw.ncpu) make install逻辑上分四段看第一段--target-os/--arch/--cpu是给 FFmpeg 自己的构建系统看的决定启用哪些源码分支第二段--cc/--sysroot/--enable-cross-compile是告诉它我在交叉编译别拿宿主机试着玩第三段是一串--disable-*负责体积第四段--extra-cflags和--extra-ldflags里的-arch必须和第一段完全一致否则前面白配。那几个参数里最容易写错的是--cc。直接写clang也能过但它用的是宿主机的默认 SDK--sysroot一覆盖反而互相打架症状是头文件里出现一堆莫名其妙的类型冲突。写成xcrun -sdk iphoneos clang之后工具链和 sysroot 就锁在同一套 SDK 上了。3.2 参数逐条拆哪些决定架构哪些决定体积哪些决定 ABI参数一多就容易抄完不知道为什么。按作用分组记会稳得多。分组参数作用写错的后果架构--arch/--cpu选择源码里的汇编与优化分支编出来的库缺 NEON 路径跑得慢架构--extra-cflags里的-arch决定 clang 输出的机器码库能出但lipo认不出 slice架构--sysroot指定系统头文件与库的根头文件冲突、链接期找不到系统符号ABI-miphoneos-version-min标记最低系统版本部署版本不一致链接期告警甚至拒绝加载ABI--enable-pic生成位置无关代码静态库被链进动态库时可能重定位失败体积--disable-everything关掉全部编解码组件不配--enable-*就一个功能都没有体积--enable-small优先体积而非极限速度编解码速度小幅下降体积--disable-programs不产出可执行程序多出几 MB 无用产物功能--disable-network去掉网络协议栈需要拉流时功能缺失功能--disable-avdevice去掉设备采集层这个模块在移动端基本用不上--disable-everything是体积控制的核心杠杆但它只关组件decoder、demuxer、parser、protocol、filter 这类不会关掉 avformat、avcodec 这些库本身。所以正确姿势是先全关再按白名单一个个开回来。开的时候注意能解 mp4 里的 H.264这件事需要三样东西同时到位mov解复用器、h264解析器、h264解码器。少开一个avformat_open_input那步就返回失败。另外一个常被忽略的是--enable-swresample。做音频播放时音频格式几乎一定要重采样这个库如果不是默认开着就得显式打开否则链接期才发现缺符号。3.3 裁剪顺序从二十多兆到几兆的删除次序裁剪不要一次删到底按下面的顺序一层层砍每砍一层跑一次链接验证出问题能立刻定位到是哪一层。第一层砍外围--disable-programs --disable-doc --disable-avdevice --disable-postproc。这几个是纯收益移动端几乎不用砍完体积掉得最明显。第二层砍功能域--disable-network --disable-encoders --disable-muxers。如果你的场景只做本地文件解码播放网络协议栈和全部编码器、封装器都可以整块去掉。这一层通常是从十几兆掉到个位数兆的关键。第三层开白名单--disable-everything之后按业务逐个--enable-decoder/--enable-demuxer/--enable-parser/--enable-protocolfile。这一层要克制宁可先少开跑通主链路再补别一上来把常见格式全开回来等于白砍。第四层才是微调--enable-small、--disable-swscale-alpha、关掉不用的 filter。这一层单看每一项只省一点点但叠起来在几兆的基数上占比不小。砍的过程中有个反直觉的现象关掉汇编--disable-asm能让编译少一堆麻烦但体积不一定变小性能会明显下降。所以汇编相关的问题优先去修工具链别拿--disable-asm当万能药。3.4 config.log 是黑匣子configure 失败时先看哪三行configure挂在C compiler test failed是最常见也最没信息量的一句。真正的答案永远在源码目录下的ffbuild/config.log里翻到末尾往前找这三类内容编译测试命令的完整展开行。看-arch、--sysroot、-miphoneos-version-min是不是都带上了缺哪个一目了然。第一条 error 而不是第一条 warning。很多人被前面的警告带偏真正致命的是后面一行error:。链接阶段的符号缺失。如果编译测试过了、链接测试没过通常是 sysroot 指错了 SDK或者最低版本参数和 SDK 支持区间冲突。养成习惯每次改完参数先make distclean再重新configure。configure 会把上一次的探测结果缓存进config.mak不清干净的话新参数有一部分不生效症状是我明明改了产物却没变这类问题最耗时间。4. 一条脚本编四个架构循环、lipo 合并与头文件导出4.1 build.sh 骨架循环架构、清理、独立前缀四个架构逐个手敲命令太低效也容易漏。用一个数组把差异项集中管理其余部分共用。#!/usr/bin/env bash # 四架构批量编译骨架 set -euo pipefail ROOT$(cd $(dirname $0) pwd) SRC$ROOT/ffmpeg # 源码目录只读不在里面改文件 OUT$ROOT/build MIN_IOS11.0 # 格式名称|configure的arch|configure的cpu|clang的arch|SDK名|最低版本参数 CONFIGS( armv7|arm|cortex-a9|armv7|iphoneos|-miphoneos-version-min$MIN_IOS armv7s|arm|swift|armv7s|iphoneos|-miphoneos-version-min$MIN_IOS arm64|arm64|arm64|arm64|iphoneos|-miphoneos-version-min$MIN_IOS i386|x86||i386|iphonesimulator|-mios-simulator-version-min$MIN_IOS ) cd $SRC for row in ${CONFIGS[]}; do IFS| read -r NAME ARCH CPU CLANGARCH SDKNAME MINVER $row SDK$(xcrun --sdk $SDKNAME --show-sdk-path) PREFIX$OUT/$NAME echo building $NAME make distclean /dev/null 21 || true # 必须清否则上一个架构的缓存会污染 CPU_FLAG [ -n $CPU ] CPU_FLAG--cpu$CPU ./configure \ --prefix$PREFIX \ --target-osdarwin \ --arch$ARCH \ $CPU_FLAG \ --ccxcrun -sdk $SDKNAME clang \ --cxxxcrun -sdk $SDKNAME clang \ --sysroot$SDK \ --enable-cross-compile \ --enable-pic \ --disable-shared --enable-static \ --disable-programs --disable-doc \ --disable-everything \ --enable-decoderh264,aac \ --enable-demuxermov \ --enable-parserh264,aac \ --enable-protocolfile \ --enable-swresample \ --enable-small \ --extra-cflags-arch $CLANGARCH $MINVER \ --extra-ldflags-arch $CLANGARCH $MINVER make -j$(sysctl -n hw.ncpu) make install done这段脚本里有两个设计决定值得说清楚。一是make distclean放在 configure 之前而不是放在循环末尾。放在末尾的话如果某个架构编到一半失败退出下一个架构启动时面对的是一个半污染的状态报错会非常莫名其妙。放在开头无论上一轮怎么失败的这一轮都是干净起点。二是CPU_FLAG用一个空值判断来处理。i386 那条不需要--cpu如果硬塞一个空字符串进去configure 会把它当成非法参数直接退出。这种四个架构里三个长得像、一个不一样的情况用数据驱动加一个条件分支比写四份重复命令好维护得多。再有就是--prefix每个架构一个独立目录。有人图省事共用同一个前缀make install后一个架构覆盖前一个等到合并时发现四个目录里躺的是同一份头文件架构信息早丢了。4.2 lipo 合并fat 库的生成与同名 slice 冲突四个架构编完每个前缀下的lib/里都有一整套.a。合并就是把同一个库的四个版本捏成一个 fat 文件。LIBSlibavcodec libavformat libavutil libswresample libswscale mkdir -p $OUT/universal for lib in $LIBS; do lipo -create \ $OUT/armv7/lib/$lib.a \ $OUT/armv7s/lib/$lib.a \ $OUT/arm64/lib/$lib.a \ $OUT/i386/lib/$lib.a \ -output $OUT/universal/$lib.a done # 头文件取任意一个架构的即可四个架构的公开头文件内容一致 cp -R $OUT/arm64/include $OUT/universal/include逻辑上lipo -create做的是把不同架构的目标文件塞进同一个容器它不会做任何符号层面的合并。所以有两条硬约束其一参与合并的每个输入必须是不同的架构。如果两个输入都是 arm64lipo会直接报 same architectures 并拒绝输出。这条限制在后面遇到真机 arm64 和模拟器 arm64时会变成死路只能换打包方式。其二四个架构的 configure 参数里除了架构相关那几项其余必须完全一致。如果 armv7 关了swscale而 arm64 开着合并出来的 fat 库在不同 slice 上导出的符号集合不同链接期表现会随目标架构变化极难排查。我的做法是把公共参数抽成一个变量四个架构共用同一份只让差异项走数组。4.3 产物自检lipo -info、file、nm 三条命令该看到什么合并完不要急着往工程里拖先跑三条命令。# 1) 看 fat 容器里有哪几个 slice lipo -info build/universal/libavcodec.a # 期望Architectures in the fat file: ... are: i386 armv7 armv7s arm64 # 2) 看每个 slice 的真实文件类型与目标平台 file build/universal/libavcodec.a # 3) 看某个 slice 里有没有你要的符号 nm -g --arch arm64 build/universal/libavcodec.a | grep -c T _avcodec_lipo -info只能证明容器里有四条记录证明不了内容对。曾经遇到过的情况是lipo -info显示四个架构齐全但链接时 32 位那条报符号缺失——原因是 armv7 那一轮make其实失败了脚本因为某处吞掉了退出码继续往下走lipo拿一个空壳或者旧文件凑数。所以set -euo pipefail这行不是装饰lipo之后一定要配一条nm检查符号数量数量级明显偏低就是有问题。file那条的用处是看平台标记。同样的 arm64真机产物和模拟器产物在file的输出里平台描述不一样这也是判断有没有混入错误 slice 最快的方式。5. 编译期避坑五条血泪经验每条都能让你卡半天5.1 现象configure 报 C compiler test failed日志里却全是警告原因通常不是编译器坏了而是交叉编译的汇编预处理环节缺工具。32 位 ARM 那两条分支在打开汇编时需要额外的汇编预处理脚本参与缺了它编译测试在汇编阶段就挂日志里前面的 C 编译测试却是通过的很容易误判成编译器问题。解决先确认工具链路径没问题xcrun --sdk iphoneos --find clang能返回路径再把汇编预处理脚本放到PATH里或者干脆先用--disable-asm跑通全流程验证参数确认参数无误后再把汇编打开、单独修这一项。顺序不要反。5.2 现象链接期报symbol(s) not found for architecture armv7但库明明合进去了原因几乎总是--extra-ldflags里漏了-arch。configure 阶段的链接测试用的是--extra-ldflags只写--extra-cflags的话编译能过、链接测试会用宿主机默认架构去试结果要么测试失败要么侥幸通过但产物里混了宿主机目标文件。等到真正链接 App 时armv7 这条 slice 里缺符号。解决把-arch同时写进--extra-cflags和--extra-ldflags最低版本参数也一样。改完make distclean重来别指望增量编译能修好。5.3 现象新工具链上压根编不出 armv7 / armv7s / i386原因很直接新版工具链的 SDK 里已经不再包含这几个 32 位目标的支持文件clang -arch armv7直接报无效架构。这不是参数问题是环境不支持。解决先确认业务是否真的还需要 32 位。如果只是历史包袱直接把脚本里那三条删掉维护成本立刻降一半。如果确实需要就在构建环境里固定一套还支持 32 位的老工具链并且把版本号写进脚本注释——这种依赖最怕的就是某天环境被悄悄升级然后整条流水线集体失败却找不到原因。5.4 现象lipo报两个输入是相同架构无法合并原因真机 arm64 和模拟器 arm64 的架构名都是 arm64但平台不同。lipo只认架构名不认平台所以它认为你在重复塞同一个 slice。解决这两条分支不能用 fat 库的方式共存必须换成新的打包格式下一章展开。如果暂时不想改就把模拟器分支单独出一套库靠构建配置切换但这属于临时方案长期会越维护越乱。5.5 现象库能编能链跑起来解码第一帧就崩原因多半是 CPU 特性假设错了。--cpu写得太激进比如给 armv7 指了一个只有更新核心才有的特性编译期不会报错因为指令本身是合法的只是运行的那台设备不支持。这类问题的特点是换台设备就好了非常像玄学。解决armv7 这条分支保守一点用cortex-a9这类覆盖面广的目标想榨性能就在运行时做能力检测而不是在编译期一刀切。真机上定位这类问题的办法是抓崩溃栈看崩在哪条汇编指令再去对config.h里打开的特性宏。6. 从“编出来”到“敢上线”符号验收与 XCFramework 收尾编出四个.a只是中间态能不能上线取决于两件事符号是否完整、打包格式是否被工具链接受。先做符号验收。合并后每个库都跑一遍检查项命令判据slice 数量lipo -info x.a与脚本里的架构数一致平台标记file x.a真机与模拟器产物可区分导出符号nm -g --arch arm64 x.a | grep -c T _四个架构数量级接近不出现某条明显偏低依赖外部库nm -u --arch arm64 x.a只剩系统库符号不该有第三方库残留第三行是关键。四个架构的符号数量应该在同一量级如果 armv7 那条只有别人的零头说明那一轮编译其实没成功只是脚本没拦住。然后是打包。前面说过真机 arm64 和模拟器 arm64 不能进同一个 fat 文件现在的标准做法是打成一个多平台的库包# 先各自合出两个平台的库真机一套模拟器一套模拟器内部再 lipo 合并 i386 与 x86_64 xcodebuild -create-xcframework \ -library build/device/libavcodec.a -headers build/device/include \ -library build/simulator/libavcodec.a -headers build/simulator/include \ -output FFmpeg.xcframework-create-xcframework接受多个-library每个条目对应一个平台切片平台信息由输入产物自带的标记决定不再靠架构名猜。工程里直接引这个包链接器会按当前构建目标挑对应的那一份模拟器和真机同时可用。我自己踩得最深的一次是死磕lipo合并真机与模拟器的 arm64反复报 same architectures试了改名、试了拆目录折腾半天才想明白问题不在参数——架构名一样就是不能合并是格式层面的限制。后来定了个习惯只要架构列表里同时出现真机和模拟器就别用 fat 库直接上库包格式省下的时间够把裁剪参数调三轮。希望帮到你。本文还有配套的精品资源点击获取