简介:android-ndk-r25b-linux.zip 是 Google 面向 Linux 平台发布的 Android 原生开发工具包(NDK)r25b 稳定版,核心服务对象是需要在 Android 工程中使用 C/C++ 完成高性能计算、图形处理、物理模拟,或复用现有 C/C++ 代码库的开发者。通过 JNI 桥接,开发者可在 Java/Kotlin 层与原生代码之间传递数据、调用方法,从而兼顾开发效率与运行性能。整个压缩包约 506.38MB,包含 2000 个文件,其中以 1917 个 .h 头文件为主,覆盖 ARM、x86 等不同 CPU 架构的系统 API 接口;同时有 57 个 Python 脚本、10 个 Markdown 文档、9 个文本文件以及若干构建脚本,可辅助完成环境配置、交叉编译、源码阅读与问题排查,目录结构清晰,便于按需查阅。该版本为官方稳定版,自带 GCC/Clang 编译工具链与构建系统,资源已有 992 人学习浏览,适合正在入门 JNI、希望理解原生内存管理、集成音视频与图像处理库,或计划优化应用性能的中高级 Android 开发者。借助这份资源,读者可以一次性获得完整的头文件体系、自动化脚本和说明文档,省去琐碎的下载配置步骤,在 Linux 环境下顺畅地生成 .so 动态库并接入工程。
1. 拿到 android-ndk-r25b-linux.zip,先别急着解压
很多人在 Linux 上做 Android 构建时,拿到 android-ndk-r25b-linux.zip 的第一个动作就是 unzip 到 /opt 里然后改 PATH。这个包确实是绿色版,解压即用,但真正让构建失败的往往不是解压动作本身,而是解压之后的路径关系:Gradle 靠什么找到 NDK、CMake 工具链文件在哪、ANDROID_NDK_HOME和PATH分别被谁读取。r25b 是 NDK 的 LTS 修订版,自带 Clang 14,对老工程的 CMake 脚本兼容性比后续几年频繁改动的版本要稳定,所以现在还有很多 CI 流水线和五年以上的 Android 工程指定使用这个 zip 包。下面按解压、环境变量、Gradle 接入、交叉编译、验证这条线来一步步说明,同时也是给初次维护 Linux 构建镜像的人一份可复现的检查单。
2. 在 Linux 上解压 android-ndk-r25b-linux.zip 并配置环境
解压前要先明确一件事:这个 zip 里的顶层目录是android-ndk-r25b,不是散装文件。也就是说,unzip -d /opt/android-sdk/ndk之后,实际路径是/opt/android-sdk/ndk/android-ndk-r25b,很多人后面配环境变量少写一层,导致编译时直接报Unable to locate aapt2之类的非标准错误。另外,zip 包体积不小,解压后会占用数 GB 磁盘空间,所以先用df -h确认分区剩余空间,再用df -i看 inode 是否充足,这在 Docker 镜像构建场景里尤其重要。
2.1 先校验 SHA-256,避免解压到一半报错
在 Linux 上下载这类 zip 包,最常见的问题是下载被代理或限速中断,文件少了尾部字节。zip 格式的文件如果缺少 End Of Central Directory 记录,unzip 会直接报invalid zip archive: could not find eocd。这个报错不是系统缺少解压工具,而是文件没下全。因此在解压之前先做一步校验:
sha256sum android-ndk-r25b-linux.zipsha256sum会输出一个 64 位十六进制哈希,把它和 NDK 官方下载页给出的 SHA-256 做比对。如果比对一致再继续,不一致就直接重新下载,不要抱着“也许能用”的心态继续操作,因为 NDK 工具链里任何一个小文件损坏,都会在后续链接时表现出完全无关的符号错误。下载大文件时建议用支持断点续传的wget -c,即wget -c https://dl.google.com/android/repository/android-ndk-r25b-linux.zip -O android-ndk-r25b-linux.zip,避免中途丢包后从头再来。
2.2 规划安装目录和顶层目录结构
校验通过后,解压前可以先用unzip -l预览包内文件列表:
unzip -l android-ndk-r25b-linux.zip | head -20输出第一行通常就是android-ndk-r25b/,后面跟着source.properties、build/、toolchains/。这一步不只是看目录名,还能确认这个 zip 包没有用-j参数压平,避免解压后一堆文件直接撒在目标目录里。确认后再正式解压:
mkdir -p /opt/android-sdk/ndk unzip -q android-ndk-r25b-linux.zip -d /opt/android-sdk/ndk-q让解压静默输出,只保留错误信息;-d指定根目录。安装路径里不要有空格和中文,NDK 的 CMake 工具链文件对路径中的空格处理并不完善,Gradle 的 externalNativeBuild 遇到带空格路径时会生成奇怪的转义参数。解压完成后检查一下权限:
ls -ld /opt/android-sdk/ndk/android-ndk-r25b find /opt/android-sdk/ndk/android-ndk-r25b -type f -perm -111 | wc -l第二行统计可执行文件数量,如果数量为 0,说明压缩包在传输过程中丢失了执行权限或文件模式,常见于从 Windows 共享目录复制出来的文件;这时用chmod -R +x补上全局执行位,但更建议返回到原始 zip 包重新解压,因为 zip 格式本身是会保存 Unix 权限位的。
2.3 配置 ANDROID_NDK_HOME 与 PATH 的先后顺序
解压完成后,配置环境变量。ANDROID_NDK_HOME是给 NDK 自带的ndk-build脚本和很多第三方构建系统读取的,PATH则是让终端直接能调用 clang 工具链。两者指向的位置不同,不要混写:
export ANDROID_NDK_HOME=/opt/android-sdk/ndk/android-ndk-r25b export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATHPATH没有直接指向$ANDROID_NDK_HOME,而是指向toolchains/llvm/prebuilt/linux-x86_64/bin,因为 NDK 在 Linux 下的编译器、链接器都打包在这个 prebuilt 目录里。后面手动交叉编译时,使用的 clang 包装器也全部在这里。把这两行写入~/.bashrc后执行source ~/.bashrc,再用一条命令确认:
| 环境变量 | 推荐值 | 被谁读取 |
|---|---|---|
| ANDROID_NDK_HOME | /opt/android-sdk/ndk/android-ndk-r25b | ndk-build、CMake 脚本 |
| PATH | /opt/android-sdk/ndk/android-ndk-r25b/toolchains/llvm/prebuilt/linux-x86_64/bin | shell、Gradle 外部进程 |
| ANDROID_HOME | /opt/android-sdk | AGP、sdkmanager |
接着执行$ANDROID_NDK_HOME/ndk-build --version。如果能看到GNU Make 3.81或类似输出,说明 NDK 主脚本没有缺少依赖;如果主要报错是/bin/bash: ...: Permission denied,回到上一节看权限。这里的常见误用是把变量名写成NDK_HOME,很多老脚本确实识别NDK_HOME,但 Android Gradle Plugin 认识的是ANDROID_NDK_HOME和ANDROID_HOME,所以两个都设置也不冲突,只是不要依赖其中一个而不设置另一个。
3. 让 Gradle 和 AGP 识别 NDK r25b:local.properties 与符号链接
把环境变量配置好只是第一步,实际项目里通常不会手动跑 NDK 命令,而是通过 Android Gradle Plugin(AGP)在构建时自动调用 NDK。AGP 识别 NDK 有两套线索:一是local.properties里的sdk.dir,二是 AGP 自己记录的 SDK 目录下ndk子文件夹。如果只设置了ANDROID_NDK_HOME,没有把 NDK 放到 AGP 预期的位置,Gradle 会报NDK not configured或者提示下载某个特定版本。下面用 r25b 这个 zip 包来演示最稳妥的接入方式。
3.1 local.properties 里的 sdk.dir 决定查找起点
local.properties是 Gradle 工程根目录下的键值文件,不需要提交到 Git。CI 环境通常由脚本在构建前生成。第一行这样写:
sdk.dir=/opt/android-sdkAGP 会在$sdk.dir/ndk目录下寻找 NDK,并且要求子目录名等于source.properties里的Pkg.Revision。比如 r25b 的Pkg.Revision是25.1.8937393,那么 AGP 期望的目录就是/opt/android-sdk/ndk/25.1.8937393。这里和老版本习惯不一样,老项目里常见的ndk.dir=/opt/android-ndk-r25b写法在 AGP 7.0 之后逐渐失效,Android Studio 新版也不再生成ndk.dir字段,所以不建议继续依赖它。
除了local.properties,还可以在gradle.properties里用android.ndkDirectory指定绝对路径。这个属性对 AGP 仍然有效,适合不想动符号链接的场景。但缺点是它影响的是单个工程配置,多个工程共用时容易遗忘。下面推荐的方式是目录伪装,一次配置,所有工程都能用。
3.2 用符号链接把 NDK 伪装进 SDK 的 ndk 目录
在 Linux 上,最简单的做法是把解压出来的android-ndk-r25b目录软链成 AGP 期望的版本号目录名:
ln -s /opt/android-sdk/ndk/android-ndk-r25b /opt/android-sdk/ndk/25.1.8937393执行前先查看source.properties确认Pkg.Revision:
cat /opt/android-sdk/ndk/android-ndk-r25b/source.properties输出里有Pkg.Revision=25.1.8937393,这个值必须和符号链接名严格一致。AGP 在构建时读取source.properties和build.gradle里的ndkVersion,两个值不一致时不会去尝试模糊匹配,而是直接报版本不存在。所以 build.gradle 里这样写:
android { ndkVersion "25.1.8937393" compileSdk 33 }如果项目里没有显式写ndkVersion,AGP 会使用它自己默认绑定的 NDK 版本,那可能就不是 r25b 了。因此,只要项目依赖的是这个 zip 包,就一定要显式声明ndkVersion。符号链接名和ndkVersion都对齐后,Gradle 的配置阶段就会认为 NDK 已经安装在 SDK 内部,不再走网络下载流程。
3.3 AGP 版本与 NDK r25b 的兼容边界
NDK r25b 对应 Clang 14,AGP 8.x 和 AGP 7.4 都支持它,但要注意 AGP 8.0 之后对ndk.dir和android.ndkDirectory的兼容级别。下面这张表是实际维护中容易踩到的对应关系:
| 配置项 | AGP 7.4 | AGP 8.0+ | 建议 |
|---|---|---|---|
| ndkVersion | 支持 | 支持 | 必须写 |
| local.properties 的 sdk.dir | 支持 | 支持 | 必须写 |
| local.properties 的 ndk.dir | 支持 | 忽略 | 不要写 |
| ANDROID_NDK_HOME | 部分场景 | 部分场景 | 当作补充 |
| 符号链接到 ndk/版本号 | 支持 | 支持 | 推荐 |
这说明在 AGP 8.0+ 的环境里,ndk.dir会被静默忽略。如果你的 CI 脚本还在往local.properties里写ndk.dir=/opt/ndk/r25b,构建日志里看不到报错,但最终编译时会突然出现找不到libc++_shared.so或直接使用错误 NDK 的诡异现象。正确姿势是只维护sdk.dir和ndkVersion两个值,再配合符号链接,这样无论 Jenkins 还是 GitLab Runner,镜像重建后都能立刻跑起来。
4. NDK r25b 交叉编译实战:CMake 还是直接 clang
Gradle 能接入 NDK,最终执行交叉编译的还是 Clang 和 LLVM 工具链。NDK r25b 不再提供单独的arm-linux-androideabi-gcc,所有编译都走toolchains/llvm/prebuilt/linux-x86_64/bin下的 clang 包装器。这些包装器的命名规则是目标三元组 + API level + 编译器类型,其中一个常见误解是“NDK 里没有 gcc 所以不能做 C++ 编译”,实际上 clang 完全兼容 GNU 命令行参数,下面的命令在 r25b 上都可以直接跑。
4.1 用 CMake 工具链文件搭建交叉编译
CMake 是 NDK 官方推荐的构建方式,r25b 自带的工具链文件位于build/cmake/android.toolchain.cmake。创建一个小型 C++ 工程,源文件如下:
extern "C" int native_add(int a, int b) { return a + b; }CMakeLists.txt 这样写:
cmake_minimum_required(VERSION 3.18.1) project(native_demo LANGUAGES C CXX) add_library(native SHARED native.cpp) target_link_libraries(native log android)CMakeLists.txt 里LANGUAGES C CXX必须写,否则 NDK 工具链文件在project()阶段看不到 C++ 编译器,会退回用宿主的 g++。然后执行:
cmake -S . -B build \ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ -DANDROID_STL=c++_shared cmake --build build这里没有手动设置CMAKE_C_COMPILER,因为工具链文件会自己填好。ANDROID_ABI=arm64-v8a编译 64 位 ARM 指令集,ANDROID_PLATFORM=android-21通知编译器使用 API 21 的 sysroot。ANDROID_STL=c++_shared会链接libc++_shared.so,APK 里需要单独打入这个 so;改成c++_static则直接把 C++ 运行时静态编进产物,APK 体积会更大,但省去带 so 的麻烦。在 Android Studio 工程中,这段 CMake 由 Gradle 的externalNativeBuild触发,最简配置是:
android { defaultConfig { externalNativeBuild { cmake { cppFlags "-std=c++17" arguments "-DANDROID_STL=c++_shared" } } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } } }这样 Gradle 会使用 NDK 目录下的 CMake 工具链,不需要在build.gradle里重复声明ANDROID_NDK_HOME。
4.2 直接调用 clang 编译一个 .so 的命令
不想引入 CMake 时,也可以直接使用 NDK 提供的 clang 包装器。比如编译上面那个函数,最简命令是:
ARM64_CLANG=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++ $ARM64_CLANG -shared -fPIC -std=c++17 \ native.cpp -o libnative.so -landroid -llogaarch64-linux-android21-clang++这个包装器的名字里已经隐含了--target=aarch64-linux-android和--sysroot,不需要再额外跟一长串参数。-shared告诉链接器产出动态库,-fPIC是 Android 平台上的硬性要求,不写的话后面链接到 app 进程时会报relocation R_AARCH64_ADR_PREL_PG_HI21一类错误。-landroid链接 Android 脚手架库,-llog提供__android_log_print,如果代码里没有用到日志可以不加。
4.3 ABI 与 API level 参数对应表
NDK r25b 支持四种 ABI,包装器前缀差别只在目标三元组。在 Linux 命令行里,API level 要写在三元组后面,并且 arm64 和 x86_64 必须使用android-21或更高,因为 64 位 Android 系统本身就是从 API 21 开始的。
| ABI | clang 包装器前缀 | 最低 API | 常见 Gradle ABI 过滤 |
|---|---|---|---|
| armeabi-v7a | armv7a-linux-androideabi21-clang++ | android-19 | armeabi-v7a |
| arm64-v8a | aarch64-linux-android21-clang++ | android-21 | arm64-v8a |
| x86 | i686-linux-android21-clang++ | android-19 | x86 |
| x86_64 | x86_64-linux-android21-clang++ | android-21 | x86_64 |
这里的21是示例 API level,如果工程 minSdkVersion 是 23,就把前缀里的21换成23。目录下确实存在多个 API 版本的包装器,直接用aarch64-linux-android23-clang++也可以。需要小心的是,armeabi-v7a的包装器不是arm-linux-androideabi,而是armv7a-linux-androideabi,写错的话终端会提示命令不存在。另外,libc++_shared.so 在sysroot/usr/lib/下的 ABI 子目录里,适配时不要搞混 arm64-v8a 和 armeabi-v7a 的库文件。
5. 验证与排错:从 zip 解压到 so 链接的自检清单
拿到 r25b 这个 zip 包,最后要确认的不只是编译通过,而是编译产物和 NDK 安装本身没被旧的缓存污染。这一套自检在 CI 里尤其值得写成一个脚本,每次换镜像后跑一遍。先检查 NDK 是否完整,再检查产物动态依赖,最后看 ELF 头。
5.1 解压层验证与 eocd 报错处理
如果解压时遇到invalid zip archive: could not find eocd,先别急着换 unzip 版本。这个报错的核心是 zip 包末尾没有End Of Central Directory记录,原因多数是文件没下载完。处理方法是重新下载,同时建议下载后补一个zip -T android-ndk-r25b-linux.zip测试完整性。zip -T会对包内每个文件做 CRC 检查,比 sha256 更快,适合解压前的二次确认。重新下载后如果仍然报错,再检查 zip 包所在分区是否被挂载为 noexec,虽然这不影响解压,但会让后续 clang 执行时直接回应Permission denied。
5.2 检查 sysroot 和 C++ 运行时是否匹配
r25b 的 sysroot 在toolchains/llvm/prebuilt/linux-x86_64/sysroot,检查是否能找到目标 ABI 的头文件和运行时:
test -d $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot && echo ok find $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot -name "libc++_shared.so" | head -5如果libc++_shared.so没有按 ABI 子目录出现,说明 zip 包解压不完整。这时候不要只重新解压一次,先确认磁盘空间是否足够,再删除旧目录。如果删除了旧目录后df -h显示空间没释放,用lsof +L1查看是否有进程仍占用旧 NDK 路径,常见于持续集成的 Gradle daemon 没有重启,杀掉 daemon 后空间才会回来。
5.3 用 readelf 判断编译产物是否真的跨平台
最后验证编译出来的 so:
readelf -d libnative.so | grep NEEDED readelf -h libnative.so | grep MachineNEEDED行会列出动态依赖,期望看到liblog.so和libc++_shared.so,如果出现libstdc++.so或 host 上的libgcc_s.so.1,说明 clang 包装器路径没设置对,编译器错误地用了宿主的 sysroot。Machine字段在 arm64-v8a 产物里应该是AArch64,在 armeabi-v7a 产物里应该是ARM,出现x86-64就说明 ABI 参数写错。这个检查比编译成功更有说服力,也是 NDK r25b 在 Linux 上从 zip 包到动态库最后一道容易忽略的关卡。
本文还有配套的精品资源,点击获取