☰
macOS下用FFmpeg编译.so:动态库格式、交叉编译与避坑指南
2026/10/19 13:41:13 网站建设 项目流程

如果你手头有一台 Mac,想用 FFmpeg 编译出一份 .so 给跨语言或跨平台模块用,很可能会在make install之后陷入自我怀疑:输出目录里躺着的全是 libavcodec.dylib、libavformat.dylib,唯独没有那个你想要的 .so。更折磨人的是,照着别人的帖子改了几个 configure 参数,终于编出几个 .so 塞进工程,运行时又报dlopen failed或者undefined symbol。这事我踩过不止一次,今天把这套流程彻底捋清楚:什么情况下应该拿到 .dylib,什么情况下应该拿到 .so,以及 macOS 环境下用 FFmpeg 源码产出可分发动态库的正确姿势。

1. 先把需求掰开:.so 和 .dylib 到底差在哪

1.1 后缀不同,二进制格式和加载机制也不同

macOS 和 Linux/Android 对动态库的处理并不是“换个文件名就行”。macOS 上的动态库通常是 Mach-O 格式,后缀是.dylib;Linux 和 Android 上的动态库是 ELF 格式,后缀是.so。

这两者的差异是结构级的。ELF 有 program header 和 dynamic section,Android linker 是靠这些信息去解析依赖、重定位符号的;Mach-O 有 load commands,dyld 靠这些信息去处理 rpath、install name。你把一个.dylib直接改名成.so,文件内容还是 Mach-O,Android 加载器根本不认。反过来,把一个 Linux 上编好的 .so 拷到 macOS,macOS dyld 同样不认。

我用一个生活化的类比来解释:.doc 和 .pdf 都装的是文档,但内部编码规则不一样。你把 .doc 改成 .pdf 后缀,用 PDF 阅读器打开照样报错。动态库这种二进制文件更严格,任何格式冲突都会在加载阶段直接崩溃。

所以先记住一个结论:后缀不是装饰,它代表当前文件属于哪套二进制体系。macOS 生成的 .so如果没有经过交叉编译,大概率只是改名后的 .dylib,千万别把它当成 Android 或 Linux 能用的库。

1.2 三种常见需求对照

我在实际项目里见过三种标题类似的诉求,对应的做法完全不同:

项目场景目标运行平台正确产物macOS 上的可行方式
macOS 桌面插件、工具链组件macOS.dylib 或 .bundle本机 clang 直接编译
Android SDK、JNI 封装层Android.so(ELF 格式)用 Android NDK 交叉编译
Linux 服务端扩展Linux.so(ELF 格式)在 Linux 机器或容器里编译

标题里写“macos 环境下 FFmpeg 打包成 .so 文件”,我猜你大概率需要第二种:人在 Mac 上,目标是给 Android 工程产出 .so。因为如果目标只是 macOS 自身,macOS 开发者一般不会执着于 .so 后缀;如果目标是 Linux,直接在一台 Linux 机器上编译会省很多事。

当然,也有少数项目为了让脚本统一扫描,要求 macOS 产物也叫 .so,这个我也会在后面给一个可行的兼容做法,但它不改变二进制格式。

2. 编译前准备:工具链、源码与工程目录

2.1 本机构建所需的最小工具链

不管你要走哪条路线,macOS 本机环境都要准备好编译基础工具。大多数情况下你需要这几样:

  • Xcode Command Line Tools,提供 clang、make、sysroot 等基础工具。
  • nasm 或者 yasm,FFmpeg 在 x86 平台做汇编优化时要用。
  • pkg-config,如果你打算让 FFmpeg 依赖外部库,比如 libx264、libmp3lame,就需要它来找头文件和库文件。

先检查 Xcode Command Line Tools:

xcode-select --install clang --version make --version

如果你还没装过命令行工具,xcode-select --install会弹出安装窗口。装完以后clang --version能看到 clang 版本号,才算基础环境可用。

nasm 可以通过 mac 上常见的软件包管理器装:

brew install nasm nasm -v

如果你不想装汇编器,configure 的时候可以加--disable-x86asm,FFmpeg 也能编过,但性能优化会少一部分。能装还是建议装,尤其要拿去做 Android 解码渲染时,x86_64 模拟器的速度会差不少。

2.2 获取源码:不推荐直接装预编译包

很多人一开始会贪方便,直接拿brew install ffmpeg装好的库去找 .so。这步在 macOS 上基本行不通,因为 brew 的 ffmpeg 给的是一堆 .dylib,而且版本、编译选项都不受你控制。我们做动态库打包,最终目的是把这套库塞进另一个工程里,必须用 FFmpeg 源码自己编一遍。

建议从 FFmpeg 官方 release 页面下载稳定的 tar.xz 包,或者直接克隆官方仓库。这里要注意版本选择:

git clone https://github.com/FFmpeg/FFmpeg.git

如果你用 git 克隆默认分支,拿到的可能是开发版本,API 变动会比较快。我更推荐找一个明确的 release tag,比如 6.1、7.0 这种。交叉编译、集成第三方项目时,版本固定非常重要,否则头文件和库的匹配关系一乱,编译期全是莫名其妙的报错。

下载完以后解压到一个路径里没有空格的目录,比如:

mkdir -p ~/src cd ~/src tar xf ffmpeg-6.1.tar.xz mv ffmpeg-6.1 ffmpeg

后面所有命令都基于这个~/src/ffmpeg目录。

2.3 输出目录与 ABI 规划

这一步看似不起眼,但踩坑的人特别多。我在构建 FFmpeg 动态库之前会先把目录结构定死,避免后面搞混。

例如,我习惯在项目根目录附近建一个dist目录:

dist/ macos/ android/ arm64-v8a/ armeabi-v7a/ x86_64/

macOS 本机产物放dist/macos,Android 产物按 ABI 分目录。为什么要按 ABI 分?因为 Android 的 .so 不是一套库通吃所有芯片架构,arm64-v8a、armeabi-v7a、x86_64都必须单独编译。你如果偷懒把 arm64 的 .so 当成 common 库放到所有 ABI 目录里,运行时大概率直接闪退或加载失败。

configure 阶段我还会给每个 ABI 设置独立的--prefix,这样make install会把头文件和库自动装到对应目录,不会互相覆盖。

3. 在 macOS 上把 FFmpeg 打成共享库

3.1 路线 A:给 macOS 自己用,正确产物其实是 .dylib

如果你的目标平台是 macOS,就不要纠结 .so 后缀。直接编译一份 .dylib 是正确做法。

进入源码目录,先执行 configure:

cd ~/src/ffmpeg ./configure \ --enable-shared \ --disable-static \ --disable-programs \ --disable-doc \ --prefix="$HOME/ffmpeg-dist/macos"

这几个参数解释一下:

  • --enable-shared:生成动态库,这是核心。
  • --disable-static:不需要静态库,节省编译时间。
  • --disable-programs:不生成 ffmpeg、ffprobe 可执行文件,我们只要库。
  • --disable-doc:不生成文档。
  • --prefix:指定安装目录。

然后编译安装:

make -j"$(sysctl -n hw.ncpu)" make install

完成后看输出目录:

ls -l "$HOME/ffmpeg-dist/macos/lib"

你会看到类似下面的文件:

libavcodec.dylib libavcodec.61.dylib libavformat.dylib libavutil.dylib libswscale.dylib libswresample.dylib

注意这里没有 .so。FFmpeg 的构建系统识别出 macOS 后,默认就会按 Darwin 的 dylib 规则生成。

有人会问,能不能让 FFmpeg 的 configure 直接生成 .so?FFmpeg 的共享库后缀是根据目标操作系统定的。在 macOS 上强行让它生成 .so,不是简单改一个选项就能做到,后面我会专门讲这个。

如果你打算把这个 .dylib 拖到别的 macOS 应用里用,还要注意 install name 的问题。用otool -L看一个库的依赖信息:

otool -L "$HOME/ffmpeg-dist/macos/lib/libavcodec.dylib"

输出第一行是这个库自己的 install name,下面跟着它依赖的其他库路径。如果你移动了库的位置,或者要把它放到一个独立文件包里,就要用install_name_tool把路径改成@rpath形式。

例如:

for lib in "$HOME/ffmpeg-dist/macos/lib/"*.dylib; do install_name_tool -id "@rpath/$(basename "$lib")" "$lib" done

还有一点:如果你希望一份库同时支持 arm64 和 x86_64,在 Apple Silicon 和 Intel 上都能跑,不要试图在一次 configure 里解决。我试过在通用二进制下搞这种操作,汇编层很容易出问题。最稳的方式是分别编译 arm64 和 x86_64 两套库,再用 lipo 合并:

lipo -create \ ~/ffmpeg-dist/arm64/lib/libavcodec.dylib \ ~/ffmpeg-dist/x86_64/lib/libavcodec.dylib \ -output ~/ffmpeg-dist/universal/libavcodec.dylib

3.2 路线 B:交叉编译 Android 需要的 .so

现在说绝大多数人真正需要的场景:在 macOS 上给 Android 编 FFmpeg .so。

跨平台编译 Android 目标时,不能用 Mac 自带的 clang 直接编,要用 Android NDK 提供的 clang 交叉编译工具链。NDK 下载完解压后,路径大概是这样的:

android-ndk/ toolchains/ llvm/ prebuilt/ darwin-x86_64/ bin/ aarch64-linux-android24-clang armv7a-linux-androideabi24-clang x86_64-linux-android24-clang

有些同学的 Mac 是 Apple Silicon,看到目录名还是darwin-x86_64会怀疑是不是下错了。其实 NDK 在 macOS 上长期维持这个命名,arm64 的 Mac 也能通过系统兼容方式正常调用里面的编译器,不用手动改目录。

以一个具体 ABI 为例,比如 arm64-v8a:

export API=24 export NDK=/opt/android-ndk export TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/darwin-x86_64" export CC="$TOOLCHAIN/bin/aarch64-linux-android${API}-clang"

API 是你的最低支持 Android 版本,一般选 21 以上覆盖绝大多数机型。然后执行 configure:

cd ~/src/ffmpeg ./configure \ --target-os=android \ --arch=aarch64 \ --cc="$CC" \ --enable-cross-compile \ --enable-shared \ --disable-static \ --disable-programs \ --disable-doc \ --enable-pic \ --prefix="$HOME/ffmpeg-dist/android/arm64-v8a"

这里几个参数很关键:

  • --target-os=android:告诉 FFmpeg 构建系统,最终产物要按 Android 的 ELF 格式来组织。
  • --arch=aarch64:指定目标 CPU 架构。
  • --cc="$CC":指定交叉编译器,一定不要漏。
  • --enable-cross-compile:开启交叉编译模式。
  • --enable-pic:生成位置无关代码,动态库必须有。

然后编译安装:

make -j"$(sysctl -n hw.ncpu)" make install

这时候去安装目录看:

ls -l "$HOME/ffmpeg-dist/android/arm64-v8a/lib"

你会看到真正的 .so 文件了:

libavcodec.so libavformat.so libavutil.so libswscale.so libswresample.so libavfilter.so

用 file 命令验证一下二进制格式:

file "$HOME/ffmpeg-dist/android/arm64-v8a/lib/libavcodec.so"

输出应该包含ELF 64-bit和ARM aarch64,而不是Mach-O。看到这个就说明交叉编译生效了。

多 ABI 的完整做法,我会写一个脚本去批量操作。因为每次 configure 都会改变源码目录里的配置状态,所以每个 ABI 之间要make distclean清理一次,再重新 configure。脚本大致如下:

#!/bin/bash set -e API=24 NDK=/opt/android-ndk FFMPEG_DIR=~/src/ffmpeg DIST_DIR=~/ffmpeg-dist TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/darwin-x86_64" cd "$FFMPEG_DIR" build_one() { local ABI=$1 local ARCH=$2 local TRIPLE=$3 local CPU_OPTS="" if [ -n "$4" ]; then CPU_OPTS="--cpu=$4" fi local PREFIX="$DIST_DIR/android/$ABI" local CC="$TOOLCHAIN/bin/${TRIPLE}${API}-clang" make distclean >/dev/null 2>&1 || true local CONFIGURE_OPTS=( --target-os=android --arch="$ARCH" --cc="$CC" --enable-cross-compile --enable-shared --disable-static --disable-programs --disable-doc --enable-pic --prefix="$PREFIX" ) if [ -n "$CPU_OPTS" ]; then CONFIGURE_OPTS+=("$CPU_OPTS") fi ./configure "${CONFIGURE_OPTS[@]}" make -j"$(sysctl -n hw.ncpu)" make install } build_one arm64-v8a aarch64 aarch64-linux-android armv8-a build_one armeabi-v7a arm armv7a-linux-androideabi armv7-a build_one x86_64 x86_64 x86_64-linux-android ""

这个脚本会把三套常见 ABI 的 .so 都编出来。如果你不需要 32 位,只保留 arm64-v8a 和 x86_64 也行。

3.3 如果一定要文件名带 .so,怎么办

有一种情况是项目脚本写死了要扫描.so文件,你只是想在 macOS 产物上也看到 .so 后缀。

我前面强调过,macOS 本身并不关心后缀叫什么。dyld 加载动态库时看的是文件内容里的 Mach-O 头,不是文件名。所以你可以把 dylib 复制一份改成 .so,这个文件在 macOS 上依然能被 dlopen 加载。

例如:

cd "$HOME/ffmpeg-dist/macos/lib" for lib in *.dylib; do name="${lib%.dylib}.so" cp "$lib" "$name" done

如果你要让改名后的 .so 作为独立库被依赖,最好把 install name 也同步改掉:

install_name_tool -id @rpath/libavcodec.so libavcodec.so

但请注意,这个 .so 是 Mach-O 格式,只能给 macOS 用,不能拿到 Android、Linux 上。如果你要把库交给不同平台,请分别走各自的编译路线,不要在 macOS 本机库上做“后缀欺骗”。

4. 集成验证与避坑手册

4.1 把 .so 放进工程并正确加载

交叉编译得到的 .so 最终要放进目标工程。Android Studio 工程里,通常把不同 ABI 的 .so 放到app/src/main/jniLibs/对应目录下:

app/src/main/jniLibs/ arm64-v8a/ libavcodec.so libavformat.so libavutil.so libswscale.so libswresample.so libswresample.so x86_64/ ...

如果你是用 CMake 接 FFmpeg 的 C/C++ API,可以这样导入:

set(FFMPEG_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) add_library(avcodec SHARED IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavcodec.so) target_link_libraries(你的模块 avcodec avformat avutil swscale swresample)

如果你走 Java/Kotlin JNI 调用,通常需要手动加载这么多动态库时,要注意加载顺序:

System.loadLibrary("avutil") System.loadLibrary("swresample") System.loadLibrary("avcodec") System.loadLibrary("avformat")

因为 libavcodec.so 内部依赖 libavutil.so,编译期链接会写进 ELF 的 needed 段,但运行时如果你只 loadLibrary("avcodec"),系统在查找依赖时找不到的话,可能直接抛UnsatisfiedLinkError。养成按依赖顺序加载的习惯,能省很多排查时间。

4.2 高频问题排查速查表

我整理了一份直接能对着查的问题表,全是实际遇到的:

遇到的现象原因处理方式
configure 报C compiler test failed没有装命令行工具,或 CC 指错了执行xcode-select --install,检查$CC路径
configure 报nasm/yasm not found汇编器缺失安装 nasm,或加--disable-x86asm
交叉编译时Unknown option一类的奇葩报错configure 参数拼错或架构名不对检查--arch的值,aarch64、arm、x86_64 要和编译器匹配
找不到aarch64-linux-android24-clangNDK 版本旧,或路径写错确认 NDK 的 toolchains/llvm/prebuilt 下的实际目录,API 数字改成存在的版本
生成的库用file看是 Mach-O没有进入交叉编译模式强制加--target-os=android --enable-cross-compile重新 configure
运行时dlopen failed: library not found依赖库路径不对,或某个 .so 没打包进去用readelf -d看依赖,把依赖的 .so 全部放进同 ABI 目录
运行时报undefined symbol: av_xxx头文件版本和 .so 版本不一致,或用了老 API统一 FFmpeg 版本,按新版本 API 改代码
切换配置后 make 总报旧的库存在源码目录残留旧 config执行make distclean后重新 configure
某 ABI 的库在真机上闪退jniLibs 里混入了不同 ABI 的文件检查每个 ABI 目录,删掉多余架构的 .so
拷贝只拷了带版本号的 .so,没拷符号链接打包脚本漏文件make install 后保留未版本号入口文件,比如 libavcodec.so

排查的时候,第一时间用file和readelf -d确认二进制格式与依赖,这两个命令能解决一半问题。不要凭感觉改代码。

4.3 打包心法和后续优化

最后分享一点我自己的经验。

第一次做 FFmpeg 动态库,不要追求全功能。裸编译一份 FFmpeg 就够了,把--disable-programs、--disable-doc都加上,减少编译量。如果你还需要 libx264、libmp3lame 这类外部编码器,一定要先确认它们也做好了对应 ABI 的交叉编译,并且要把它们的 include 和 lib 路径通过--extra-cflags、--extra-ldflags传给 FFmpeg。这一步复杂度会成倍上涨,我建议先跑通裸 FFmpeg 再升级。

每次编完一个 ABI,养成一个习惯:

file ~/ffmpeg-dist/android/arm64-v8a/lib/libavcodec.so readelf -d ~/ffmpeg-dist/android/arm64-v8a/lib/libavcodec.so | grep SONAME

确认它是 ELF、Android 架构、SONAME 正确。如果这一步检查不过,后面集成一定会返工。

还有,共享库版本号确实容易制造混乱。FFmpeg 在源码里会有类似LIBAVCODEC_VERSION_MAJOR的宏,make install 之后会生成带完整版本号的 .so 和未版本号的入口链接。在 Android 工程里,通常只要带入口链接libavcodec.so,不要只拷贝libavcodec.so.61这种文件,否则加载器按文件名找不到。

如果你对 .so 体积敏感,可以在 configure 里加--extra-cflags="-Os",编译后用 NDK 的 llvm-strip 去掉符号表。但去掉符号之前,一定要确认自己不再需要 cmake 链接时的符号信息。

说到最后,真正跑通一次之后,你会发现这事不复杂。难的只是开头那个选择:你要的是给哪个系统用的 .so。先跑通一个 ABI,再上全套,别一上来就搞四五个架构的矩阵,这是我目前最想给你的一条建议。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询