Linux 下 Android NDK r25b 解压配置与 Gradle 交叉编译实战

发布时间:2026/9/11 8:41:35
Linux 下 Android NDK r25b 解压配置与 Gradle 交叉编译实战 简介android-ndk-r25b-linux.zip 是 Google 面向 Linux 平台发布的 Android 原生开发工具包NDKr25b 稳定版核心服务对象是需要在 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-r25bndk-build、CMake 脚本PATH/opt/android-sdk/ndk/android-ndk-r25b/toolchains/llvm/prebuilt/linux-x86_64/binshell、Gradle 外部进程ANDROID_HOME/opt/android-sdkAGP、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 r25blocal.properties 与符号链接把环境变量配置好只是第一步实际项目里通常不会手动跑 NDK 命令而是通过 Android Gradle PluginAGP在构建时自动调用 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.Revisioncat /opt/android-sdk/ndk/android-ndk-r25b/source.properties输出里有Pkg.Revision25.1.8937393这个值必须和符号链接名严格一致。AGP 在构建时读取source.properties和build.gradle里的ndkVersion两个值不一致时不会去尝试模糊匹配而是直接报版本不存在。所以 build.gradle 里这样写android { ndkVersion 25.1.8937393 compileSdk 33 }如果项目里没有显式写ndkVersionAGP 会使用它自己默认绑定的 NDK 版本那可能就不是 r25b 了。因此只要项目依赖的是这个 zip 包就一定要显式声明ndkVersion。符号链接名和ndkVersion都对齐后Gradle 的配置阶段就会认为 NDK 已经安装在 SDK 内部不再走网络下载流程。3.3 AGP 版本与 NDK r25b 的兼容边界NDK r25b 对应 Clang 14AGP 8.x 和 AGP 7.4 都支持它但要注意 AGP 8.0 之后对ndk.dir和android.ndkDirectory的兼容级别。下面这张表是实际维护中容易踩到的对应关系配置项AGP 7.4AGP 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 还是直接 clangGradle 能接入 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_ABIarm64-v8a \ -DANDROID_PLATFORMandroid-21 \ -DANDROID_STLc_shared cmake --build build这里没有手动设置CMAKE_C_COMPILER因为工具链文件会自己填好。ANDROID_ABIarm64-v8a编译 64 位 ARM 指令集ANDROID_PLATFORMandroid-21通知编译器使用 API 21 的 sysroot。ANDROID_STLc_shared会链接libc_shared.soAPK 里需要单独打入这个 so改成c_static则直接把 C 运行时静态编进产物APK 体积会更大但省去带 so 的麻烦。在 Android Studio 工程中这段 CMake 由 Gradle 的externalNativeBuild触发最简配置是android { defaultConfig { externalNativeBuild { cmake { cppFlags -stdc17 arguments -DANDROID_STLc_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 -stdc17 \ native.cpp -o libnative.so -landroid -llogaarch64-linux-android21-clang这个包装器的名字里已经隐含了--targetaarch64-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 开始的。ABIclang 包装器前缀最低 API常见 Gradle ABI 过滤armeabi-v7aarmv7a-linux-androideabi21-clangandroid-19armeabi-v7aarm64-v8aaarch64-linux-android21-clangandroid-21arm64-v8ax86i686-linux-android21-clangandroid-19x86x86_64x86_64-linux-android21-clangandroid-21x86_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 判断编译产物是否真的跨平台最后验证编译出来的 soreadelf -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 包到动态库最后一道容易忽略的关卡。本文还有配套的精品资源点击获取