简介:Android NDK r28c 是面向Linux 64位平台(x86架构)的官方原生开发工具集,专为Android平台C/C++开发者设计,用于构建高性能图形渲染、音视频处理、AI推理等对底层控制有强需求的应用模块。本资源完整包含NDK核心头文件(如NdkCameraMetadataTags.h、NeuralNetworks.h、gl32.h等)、Python构建脚本、Markdown文档说明及少量PDF/文本格式的官方指南,支撑JNI开发、HAL对接与硬件加速能力调用。压缩包共2000个文件,其中1977个头文件(.h)构成API主体,11个Python脚本用于自动化构建与测试,10个Markdown文档提供版本说明与使用指引,整体体积达688.8MB,结构规范、即下即用。已有230人下载学习,适用于中高级Android原生开发者快速搭建编译环境、查阅最新NDK接口定义、调试Native层逻辑或集成Camera/NPU/OpenGL等系统级能力。 上周一个同事在 Ubuntu 22.04 上遇到一个典型问题:他从官网下载了 android-ndk-r28c-linux.zip,按说明解压完,运行ndk-build --version直接报 GLIBC_2.34 找不到。他第一反应是文件损坏,重新下两遍还是一样,最后才定位到是系统 glibc 版本太旧,而 r28c 的预编译工具链已经悄悄提高了对运行库的要求。这件事让我想系统写一篇 NDK r28c 在 Linux 上从安装到编译的实操记录。它不复杂,但细节很容易踩,尤其是从旧版 NDK 直接跳到 r28 系列的人,很多差异官方文档根本没说全。这篇内容适合刚接触 NDK 的初学者,也适合正在升级 NDK 版本、需要在 Linux 服务器或本机交叉编译.so的开发者。
1. 先搞清楚 r28c 和旧版差在哪
1.1 工具链彻底转向 Clang
NDK 从 r23 开始就正式移除了 GCC 相关的独立工具链脚本,r28 系列里已经看不到gcc、g++这类可执行文件了。你在toolchains/llvm/prebuilt/linux-x86_64/bin目录下能看到的全是clang、clang++以及带目标架构前缀的编译驱动,比如aarch64-linux-android21-clang、armv7a-linux-androideabi21-clang。
这对老项目影响很大。不少网上教程还停留在“先跑 make-standalone-toolchain.py 生成独立工具链”的阶段,那套流程在 r28c 里根本没有对应入口,照搬旧脚本直接报错。实际上 NDK 官方现在的态度就是:要么用 ndk-build,要么用 CMake,要么直接用 clang 交叉编译,不再提供中间层包装。
1.2 API 级别策略收紧
r28 系列在构建时对minSdkVersion的关注度更高了。以前 NDK 默认的APP_PLATFORM可能比较宽松,新版工具链则倾向于按较高的最低 API 级别来链接。如果你沿用旧的android-16这类低版本目标,大概率会碰到平台头文件缺失,或者__ANDROID_API__宏触发的条件编译分支不生效。
我个人的建议是:新项目直接按系统当前能接受的最低版本定,比如android-21;老项目如果要升级到 r28c,优先把minSdkVersion一起提上来,别一边更新 NDK 一边守着远古 target。
1.3 r28c 这类小版本更新为何值得关注
NDK 的发布策略和大版本同步,r28b、r28c 都是针对构建工具链的补丁版。小版本修复的东西不会出现在大新闻里,但往往很实际,比如修复特定架构下编译某些第三方库时的内联汇编问题、修正链接器对 LLD 的默认行为等。
生产环境里我会优先选一个已经发布一段时间的稳定小版本,而不是直接追最新 RC 或 Canary。r28c 就属于比较稳的位置,既能享受到 r28 的新工具链特性,又不至于像 Dev Preview 那样频繁变动。
2. 下载解压前,把 Linux 环境检查一轮
2.1 网络下载、校验、解压的完整命令链
先说下载,一般从 Android 开发者官网的 NDK 下载页拿压缩包。文件名是android-ndk-r28c-linux.zip,对应的是 Linux 64 位平台。下载命令可以用wget或curl,但我更建议顺手做一次 SHA-256 校验,因为二进制文件太大,下载中途断掉又没报错的情况也不少:
wget https://dl.google.com/android/repository/android-ndk-r28c-linux.zip sha256sum android-ndk-r28c-linux.zip把输出结果跟下载页展示的官方哈希值比对,一致再解压。这一步在公网下载大文件时尤其值得做,省得后面编译报出一堆莫名其妙的链接错误。
2.2 系统依赖库核对清单
r28c 解压后的工具链大部分是动态链接的,对系统底座有一定要求。我建议在解压前先跑一轮检查,省得解压完才报环境问题:
- 64 位 Linux 系统,32 位系统跑不了 r28c。
glibc版本不能太旧,r28 系列的预编译工具链通常要求 GLIBC_2.17 以上,部分较新组件会要到 GLIBC_2.34。- 系统要有
python3,NDK 的构建脚本内部会调用 Python。 make、patch、unzip这类基础命令要存在,建议用sudo apt install make python3 unzip zlib1g-dev之类的命令补齐。
很多容器镜像或精简版 Linux 发行版会把patch、make这类工具裁掉,结果 NDK 跑起来就报make: command not found或patch: command not found。看着是 NDK 的问题,其实就是宿主环境缺依赖。
2.3 解压目录和权限选择
解压我习惯统一放到/opt或用户目录下,关键是路径里不能有空格,不要有中文,比如别解到/mnt/windows 下载/NDK这种路径。NDK 的构建脚本对路径很敏感,带空格的路径会让 CMake 和 make 在传参时直接断掉。
sudo unzip android-ndk-r28c-linux.zip -d /opt/解压完成后还要确认权限没问题。有时候通过某些图形界面工具解压,文件权限会丢,后续执行ndk-build就报 permission denied。稳妥起见可以补一次递归加执行权限:
sudo chmod -R +x /opt/android-ndk-r28c3. 目录结构拆解:r28c 解压后哪些目录在干活
3.1 顶层目录速览
NDK 解压完大概有 6 到 8 个顶层目录,第一次打开容易看花眼。我把主要目录的作用列出来:
| 目录 | 作用 |
|---|---|
build/ | 构建系统支持文件,比如 CMake 工具链文件android.toolchain.cmake就在这里 |
meta/ | NDK 构建系统的元数据,一般不用管 |
platforms/ | 各 API 级别对应的 Java 层 Android.jar,主要用于 SDK 层面构建 |
prebuilt/ | NDK 自带的预编译工具,比如 make、awk 等 |
sources/ | C++ STL 的源码、第三方库源码等 |
sysroot/ | 交叉编译要用的头文件和库文件集合,最终链接时非常重要 |
toolchains/ | 核心编译器位置,LLVM/Clang 就在这里 |
对大多数人来说,真正要接触的只有toolchains/llvm、platforms和sysroot。
3.2 toolchains/llvm 里藏着的编译器
toolchains/llvm/prebuilt/linux-x86_64/bin是核心中的核心。这个目录下的可执行文件数量很多,但命名有规律:
aarch64-linux-android21-clang:目标架构为 arm64-v8a,目标 API 级别为 21 的 C 编译器aarch64-linux-android21-clang++:目标架构为 arm64-v8a,目标 API 级别为 21 的 C++ 编译器armv7a-linux-androideabi21-clang:目标架构为 armeabi-v7a 的 C 编译器x86_64-linux-android21-clang:目标架构为 x86_64 的 C 编译器
不带 API 数字的aarch64-linux-android-clang也存在,它会自动选择 NDK 支持的默认 API 级别。在 r28c 里,直接使用带 API 级别的编译器更可控,因为默认值不一定是你项目的minSdkVersion。
3.3 sysroot 和 platforms 的区别
有的新手会把platforms和sysroot弄混,其实两者分工不同。
sysroot是 NDK 交叉编译的“根”,里面装着 Android 系统 API 对应的头文件(长期演进版本)和用于链接的.so/.a库。当你用clang交叉编译时,-sysroot参数会指向这里。platforms里装的是android.jar等 Java 层接口,主要是给 Android SDK 构建 APK 时用的。如果你只是单独编译 C/C++ 代码成.so,基本上不需要碰platforms。
4. 环境变量与 Android Studio 接入的两种姿势
4.1 命令行环境变量配置
如果只想在终端里用ndk-build或 CMake,比较简单的方式是设置两个环境变量:
export ANDROID_NDK_HOME=/opt/android-ndk-r28c export PATH=$ANDROID_NDK_HOME:$PATH我建议把这两行写进~/.bashrc或~/.zshrc,重新登录后自动生效。要注意的是ANDROID_NDK_HOME这个变量名在 Android Gradle Plugin 里也认,很多 CI 机器上配置 NDK 就是靠它。
验证是否配置成功:
$ANDROID_NDK_HOME/ndk-build --version如果能看到构建版本信息,说明 NDK 主程序能跑。再验证编译器:
$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang --version能输出 clang 版本号,说明工具链可执行文件具备运行条件。
4.2 Android Studio 中手动指定 NDK 路径
Android Studio 优先去 SDK 目录下的ndk/<版本号>里找 NDK。如果你不喜欢默认位置,最省事的方法是把压缩包解压到 SDK 的ndk目录下,并改名为 Gradle 能识别的版本号格式。
但我不建议这样做。因为 NDK 完整版本号是一长串,比如28.x.xxxxxx,手动改名很容易和ndkVersion配置对不上。更推荐的做法是在项目根目录的local.properties里显式指定:
sdk.dir=/home/youruser/Android/Sdk ndk.dir=/opt/android-ndk-r28c或者不写ndk.dir,直接设置环境变量ANDROID_NDK_HOME,新版 AGP 会优先读取这个变量。如果项目里的ndkVersion配置和实际安装版本不一致,Gradle 会尝试下载对应的版本,这也是很多人卡住的原因。一个比较稳的组合是:local.properties配sdk.dir,模块build.gradle里写:
android { ndkVersion "28.2.1356572" }版本号要以你解压出来的实际目录名或 NDK 包元信息为准,不要照抄网上任何例子。r28c 对应的具体 build 号会在解压目录里体现,你打开/opt/android-ndk-r28c/source.properties就能看到Pkg.Revision字段。
5. ndk-build 和 CMake 各有各的脾气:两套构建实战
5.1 ndk-build 方式
ndk-build 是 NDK 自带的传统构建方式,核心是Android.mk和Application.mk一组 Make 文件。项目结构大致是:
jni/ Android.mk Application.mk native-lib.cppAndroid.mk的写法:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := native-lib LOCAL_SRC_FILES := native-lib.cpp LOCAL_LDLIBS := -llog include $(BUILD_SHARED_LIBRARY)Application.mk里指定目标 ABI 和 C++ 运行时:
APP_ABI := arm64-v8a armeabi-v7a APP_PLATFORM := android-21 APP_STL := c++_shared然后在项目根目录执行:
$ANDROID_NDK_HOME/ndk-build默认输出会放在libs/<abi>/目录下。如果APP_ABI里写了多个架构,NDK 会逐个编译。这种方式的优点是简单直接,适合传统项目、纯 C/C++ 库、不依赖 Gradle 的构建场景。
5.2 CMake 方式
CMake 是 Android Studio 默认推荐的构建方式,本质上是 NDK 提供了一个工具链文件,让 CMake 知道怎么调用 clang、怎么设置 sysroot、怎么处理 ABI。
一个最小的CMakeLists.txt:
cmake_minimum_required(VERSION 3.22.1) project(native-lib) add_library(native-lib SHARED native-lib.cpp) find_library(log-lib log) target_link_libraries(native-lib ${log-lib})命令行编译时最关键的是指定工具链文件:
cmake -S . -B build \ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 cmake --build build这里ANDROID_ABI可以是armeabi-v7a、arm64-v8a、x86、x86_64中的任意一个。ANDROID_PLATFORM建议和项目minSdkVersion保持一致。
CMake 相比之下更灵活,适合大型项目和依赖很多第三方库的工程。Android Studio 里新建 Native C++ 项目默认就会生成 CMake 版本的项目骨架。
5.3 两种方式的输出差异
ndk-build 的输出通常集中在libs/<abi>/,CMake 的输出则取决于你指定的CMAKE_LIBRARY_OUTPUT_DIRECTORY,默认在build/下的子目录里。
要看清楚最终生成的是.so还是.a,取决于你在构建文件里写的是BUILD_SHARED_LIBRARY还是BUILD_STATIC_LIBRARY(或 CMake 里的SHARED/STATIC)。实际项目里,我习惯先把第三方库编成静态库.a,再链接进最终.so,这样交付产物单一,也避免在多个模块间重复暴露符号。
6. 从零编译一个 JNI 动态库并跑进 App
6.1 准备 JNI 源文件
先写一个最简单的 JNI 函数,验证整个工具链链路是否通畅:
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_app_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from NDK r28c"; return env->NewStringUTF(hello.c_str()); }注意函数名里的包名和类名:com_example_app_MainActivity对应 Java 层的com.example.app.MainActivity。如果包名或类名不一致,运行时会报UnsatisfiedLinkError。
6.2 用 ndk-build 编译验证
按第 5.1 节的Application.mk和Android.mk配置,在jni目录外执行:
$ANDROID_NDK_HOME/ndk-build看到类似输出:
[arm64-v8a] Compile++ : native-lib <= native-lib.cpp [arm64-v8a] SharedLibrary : libnative-lib.so [arm64-v8a] Install : libnative-lib.so => libs/arm64-v8a/libnative-lib.so说明 NDK 工具链已经正常工作了。这是在 Linux 上验证 r28c 最简单的测试,能跑通这一步就说明大部分环境问题已经排除。
6.3 用 CMake 编译验证
同样一份CMakeLists.txt,执行:
cmake -S . -B build \ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 cmake --build build最后在build/目录下找到libnative-lib.so。这一步跑通,说明 CMake 工具链文件没有被破坏,sysroot和链接器都工作正常。
6.4 把 .so 集成进 Android App
如果只是想在 App 里用这个库,把编译好的libnative-lib.so放到app/src/main/jniLibs/arm64-v8a/下。Java 层加载:
package com.example.app; public class MainActivity extends Activity { static { System.loadLibrary("native-lib"); } public native String stringFromJNI(); }这样绕开 Android Studio 的自动构建流程,适合快速验证“手动编译的库能不能被 App 加载”。实际开发中我更多用这个方式测试第三方预编译库,比如 OpenSSL、FFmpeg 之类。
7. 解压后第一波报错的完整排查记录
7.1 GLIBC 版本不兼容是最高频问题
GLIBC_2.34 not found这类错误在检查系统时其实很直观。r28 系列预编译的 clang 和 ndk-build 在运行时依赖的 glibc 符号已经高于 Ubuntu 18.04 这类老版本。
排查命令:
ldd --version看到版本低于要求后,两个方向:一是系统升级到 Ubuntu 20.04 以上,或换一个更新版本的系统;二是放弃 r28c,换用更老的 NDK 版本。旧系统补 glibc 的风险比较大,不建议手动替换系统库,我试过,容易把系统搞到崩溃,重装比修补更省时间。
7.2 zlib 缺失导致的链接异常
另一个常见问题是解压工具能跑,但 clang 在链接阶段报找不到libz.so.1。这种报错很容易误判为代码问题,实际是系统没有安装 32 位兼容库或压缩库。
处理方式就是装依赖:
sudo apt install zlib1g-dev装完重新跑一次编译,一般就能过。类似这种问题,我的排查习惯是:看到error while loading shared libraries或cannot open shared object file,第一反应查系统库,而不是翻代码。
7.3 Android Studio 报 NDK not configured
在 Android Studio 里改完 NDK 路径后,Gradle 仍可能报NDK not configured。这种情况通常不是路径写错,而是ndkVersion和实际 NDK 版本不匹配,Gradle 找不到对应目录,就当成没配置。
- 检查
local.properties里的ndk.dir是否指向正确目录。 - 检查
build.gradle里的ndkVersion是否和source.properties里的Pkg.Revision一致。 - 如果还不行,清理一下
.gradle缓存的配置。
7.4 解压后没有执行权限导致 permission denied
这个问题在服务器上经常遇到,特别是用 root 用户下载再解压给普通用户用。当 NDK 工具链在普通用户下执行时,部分二进制文件可能没有+x权限。
处理方式:
chmod -R +x /opt/android-ndk-r28c如果是多用户使用同一套 NDK,建议放到/opt下并由所有开发者统一使用。服务器上尽量不要解压到/tmp,因为重启可能被清理,而且权限管理也更混乱。
7.5 路径有空格导致 build 脚本炸裂
/home/user/My Downloads/android-ndk-r28c这类路径在命令行下容易出问题。CMake 和 ndk-build 内部会拼接大量的绝对路径,带空格时引号处理一旦出漏子,就报No such file or directory,而路径明明存在。
最省心的做法是解压到没有空格、没有中文、没有特殊符号的目录,例如/opt/android-ndk-r28c。这个看似小问题,实际能消掉一半的玄学报错。
8. 把这些经验固化成之后的 NDK 使用习惯
8.1 固定 NDK 版本,别混着用
同一个项目里 A 模块用 r28c、B 模块用旧版 NDK,短期内可能不炸,但遇到 C++ 标准库 ABI 不兼容时会非常痛苦。NDK 的版本升级经常伴随着 libc++ 实现调整,混用版本后通过System.loadLibrary加载多个.so,很容易在类加载阶段或运行时出现符号找不到。
我现在的固定做法是:所有模块统一一个 NDK 版本;升级时先看 release notes 里关于 ABI、最小 API 级别、libc++ 的变更说明,再决定是否整体升级。
8.2 交叉编译第三方库时的通用套路
很多人在 Linux 上拿 NDK 不是为了跑 Android Studio 项目,而是为了交叉编译 OpenSSL、FFmpeg、cURL 这类第三方库。r28c 下我建议直接写一个带--target的 clang 包装脚本,或者用 CMake 工具链文件统一管理。
以直接调用 clang 为例,编译 OpenSSL 时通常要设置:
export CC=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang export CXX=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++然后进到 OpenSSL 源码目录里按 Android 目标平台配置。这种玩法本质上就是把 NDK 当通用交叉编译工具链用,不局限于 Android 应用开发。
8.3 C++ 标准库的选择:c++_shared 与 c++_static
如果项目代码用了 STL,APP_STL或 CMake 的ANDROID_STL设置就很重要。c++_shared会链接系统里的 libc++_shared.so,生成的.so体积更小,但设备上需要同时提供 libc++_shared.so;c++_static把库静态编进目标.so,部署简单,但每个模块都静态链接会导致体积膨胀和符号冲突。
我的经验是:一个 App 有多个.so的时候统一用c++_shared,只在主模块里带一份 libc++_shared.so;单库项目随意,但c++_static在调试时符号重复的坑更少。
8.4 别把 NDK 当“绿色软件”随便拷
NDK 体积大、文件多、内部路径依赖强,我不建议直接把它放进 Git 仓库,也不建议在项目里用相对路径引用它。正确做法是在 CI 机器上安装固定版本,环境变量全局统一,项目里只存版本号配置。
另外,现在很多团队已经用 Docker 镜像做 Android 构建,里面预先装好 JDK、SDK 和 NDK。这种做法很值得推广,因为只要镜像升级了 NDK,所有引用的流水线都能保持一致,不会出现开发机器能编译、服务器上不能编译的尴尬。
我自己在实测 r28c 时最大的感受是:新版 NDK 对构建前置条件的要求更高了,但一旦环境就绪,编译效率和产物稳定性都明显提升。如果你正准备升级,别把第一波报错想得太可怕,多半是环境问题,按上面第 2 节和第 7 节的清单过一遍,比乱翻 Stack Overflow 高效得多。
本文还有配套的精品资源,点击获取