当前位置: 首页 > news >正文

NDK r28c 在 Linux 上的安装、编译与踩坑指南

简介: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 系列里已经看不到gccg++这类可执行文件了。你在toolchains/llvm/prebuilt/linux-x86_64/bin目录下能看到的全是clangclang++以及带目标架构前缀的编译驱动,比如aarch64-linux-android21-clangarmv7a-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 位平台。下载命令可以用wgetcurl,但我更建议顺手做一次 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。
  • makepatchunzip这类基础命令要存在,建议用sudo apt install make python3 unzip zlib1g-dev之类的命令补齐。

很多容器镜像或精简版 Linux 发行版会把patchmake这类工具裁掉,结果 NDK 跑起来就报make: command not foundpatch: 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-r28c

3. 目录结构拆解: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/llvmplatformssysroot

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 的区别

有的新手会把platformssysroot弄混,其实两者分工不同。

  • 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.propertiessdk.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.mkApplication.mk一组 Make 文件。项目结构大致是:

jni/ Android.mk Application.mk native-lib.cpp

Android.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-v7aarm64-v8ax86x86_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.mkAndroid.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 librariescannot 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 高效得多。

本文还有配套的精品资源,点击获取

http://www.cnnetsun.cn/news/4318239.html

相关文章:

  • 格力2020秋招网络运维岗笔试题深度解析:考点与备考指南
  • STM32+ESP8266物联网智能家居监测控制系统设计详解
  • AI公司盈利之路:从成本优化到商业闭环的深度拆解
  • Grok Bot 成本优化:用 durable state 持久化状态降低 Token 消耗
  • 基于SpringBoot的仁爱”医院信息管理系统的实现
  • 基于SpringBoot的社区团购管理系统的设计与实现
  • 高频模拟电路设计:从核心模块到流片测试的完整工程路径
  • 轻量桌面机器人开发实战:从ROS 2导航到运动学与路径规划
  • 米家小美洗碗机S10评测:16套嵌入式,双效智洗与母婴级消毒实测
  • 自建智能体框架到底值不值?从最小闭环到落地实践
  • Grok Bot安卓预注册:从预约到上线的完整避坑指南
  • Reaction视频制作全流程:OBS录制、FFmpeg剪辑与字幕同步
  • 内容类型识别:为什么不能将影视剧集解析包装成CSDN技术博客
  • WBS工作分解结构实战:从目标到可执行任务清单
  • iOS网络授权验证系统实战:从Swift到Node.js全面防破解
  • 海特洛市第一代磁悬浮列车技术拆解:悬浮、驱动、安全控制
  • 垃圾分类收运路径优化全解析:从VRP建模到遗传算法求解实战
  • Java后端面试高频考点清单:集合/并发/MySQL/Redis全覆盖
  • 一条 Trajectory,如何解释 Agent Benchmark 的成败?
  • 差一个字就能仿冒?账号防伪从字符相似度到可验证流程
  • 上下水扫拖机器人怎么选?T90 Pro安装调试全指南
  • 全价位密码锁选购清单:场景化选锁与安装测试指南
  • 深信服校招C/C++F卷考点全解析:从指针到epoll的备考指南
  • C# vs Java:上位机与Web后端的真实技术拆解与选型建议
  • 从零开始学Maya 2027:建模、材质、动画到渲染的全流程入门指南
  • AI手书创作全流程:关键帧、图生视频与TTS配音实战
  • 用分立元件搭建带锁存功能的过压保护电路
  • AI Agent安全代登录:不泄露密码的自动化登录架构与实践
  • 嵌入式参数管理:用状态机设计实现调参异常一键恢复
  • 嵌入式调参改坏不用怕:空对象模式实现一键恢复出厂参数