Android NDK r26b Windows环境配置与JNI交叉编译实践
2026/9/16 18:34:40 网站建设 项目流程

简介:Android NDK r26b Windows版是Google官方提供的原生开发工具集,面向需要在Android应用中嵌入C/C++高性能模块的开发者,适用于图形渲染、音视频处理、物理模拟及复用现有原生库等场景,同时兼顾JNI桥接与多架构交叉编译需求。压缩包内含2000个文件,主要以1969个C/C++头文件为核心,辅以少量Python脚本、Markdown与文本说明,能够为NDK API调用、JNI接口声明和底层编译配置提供直接参考,整体大小约631MB。已有730人下载学习,说明其版本适用性与资料完整性受到社区认可。通过解压并配置该工具包,开发者可快速获得NDK r26b的完整头文件体系与配套文档,配合Android Studio即可着手原生模块开发、交叉编译环境调优以及.so库集成验证,有效降低搭建NDK开发环境的门槛。

1. 拆解 android-ndk-r26b-windows.zip,为什么这份压缩包值得单独收着

不少开发者第一次意识到自己需要 NDK,不是主动去学,而是 Android Studio sync 时弹出一行红色错误:NDK not configured。真正去官网手工下载 zip 的人不多,因为 SDK Manager 里勾一下就能自动装。那这份 android-ndk-r26b-windows.zip 的意义在哪?它是 Google 在 r26 系列里的第二版补丁包,对应 Android 14 生命周期内的 LTS 工具链,默认编译器是 LLVM 16(clang 16.0.5)。从 r26 开始,NDK 彻底移除了 GCC 交叉编译路径,所有架构统一走 clang。对写音视频、OpenGL ES、C++ 游戏底层,或接第三方预编译 .so 的开发者来说,r26b 的兼容面比 r27 长,对老模块的包容度比 r25 高,适合作为团队统一的基线版本。

2. 解压即用:r26b 目录结构与 Windows 环境变量配置

2.1 解压后先认清这几个目录

zip 解压出来是 android-ndk-r26b 单个目录,不要移动内部文件,因为工具链里大量路径基于相对位置推导。整个目录里,实际编译时接触最多的是三个子目录。

目录作用编译或运行时谁在用
platforms/android-/arch-/usr/include各 API level 的系统头文件与链接库clang 的 -I 和 -L 参数
toolchains/llvm/prebuilt/windows-x86_64编译器、链接器、llvm-readelf、strip 等二进制所有编译命令的入口
sources/cxx-stllibc++ 头文件与预编译库CMake 的 STL 相关配置

先看 platforms 目录,Android 14 对应的 API level 34 也在列表里,但 r26b 默认的链接目标并不是它。ndk-build 和 CMake 会选择 android-21 到 android-34 之间由项目指定的 API level。如果同一个头文件在两个目录里都存在,比如 NdkCameraMetadataTags.h,以 platforms 下对应 API level 的版本为准,sysroot 下的属于公共头文件。

再看 toolchains/llvm/prebuilt/windows-x86_64/bin,里面有一批带前缀的编译入口,比如aarch64-linux-android21-clang.cmdarmv7a-linux-androideabi21-clang.cmdx86_64-linux-android21-clang.cmd。命名规则是「架构-系统-最低 API level-clang」,调用哪个就编译出哪个平台的 .so,不需要额外指定 --target。bin 目录里还有 llvm-ar、llvm-strip、llvm-readelf 这些配套工具,后续排错环节会反复用到。

2.2 环境变量配置的两种写法

Windows 下配置 NDK 环境变量,核心变量是 ANDROID_NDK_HOME,指向 android-ndk-r26b 根目录。配置方式有用户级与系统级两种,用户级变量对当前账号生效,系统级变量对所有账号生效,一般开发机配用户级就够了。

setx ANDROID_NDK_HOME "D:\dev\android-ndk-r26b"

setx 不需要管理员权限,但只对之后新开的终端生效。如果当前 CMD 窗口里马上要用,先手动 set 一遍:

set ANDROID_NDK_HOME=D:\dev\android-ndk-r26b set PATH=%ANDROID_NDK_HOME%;%PATH%

PATH 里加 NDK 根目录不是必须的,因为 ndk-build.cmd 支持从 ANDROID_NDK_HOME 定位自己,但加上之后可以直接敲 ndk-build 而不写全路径。验证配置是否生效:

echo %ANDROID_NDK_HOME% "%ANDROID_NDK_HOME%\toolchains\llvm\prebuilt\windows-x86_64\bin\clang.exe" --version

第二条命令输出 clang version 16.0.5 说明工具链可执行。如果提示不是内部或外部命令,优先检查路径里是否有空格导致引号缺失,这是 Windows 上最常见的失败原因。另外,项目完全不进 Android Studio、只靠命令行做持续集成时,ANDROID_NDK_HOME 是唯一可信路径;Gradle 的 ndk.dir 只对具体项目生效,而环境变量对整台机器生效,Jenkins 或 GitHub Actions 的 Windows Runner 上一般解压到固定目录后在流水线里注入,避免每台机器路径不一致。

2.3 Android Studio 项目里指定 NDK 版本

项目根目录的 local.properties 中,一行 ndk.dir 会直接覆盖 Android Studio 的自动发现:

ndk.dir=D:/dev/android-ndk-r26b

配置完成后重新 sync,Gradle 会在 External Dependencies 里显示 NDK 路径。Android Studio 自带的 SDK Manager 下载的 NDK 和手工解压的 NDK 可以共存,但同一个项目只能指向一个版本。如果项目之前用的是 r25,切到 r26b 后编译通过、运行时崩溃,优先检查 C++ STL 相关配置,这个坑放到第五章细说。

3. JNI 头文件与最小 .so 编译链路

3.1 项目里那批头文件分别属于哪个模块

正文里出现的 NdkCameraMetadataTags.h、NeuralNetworks.h、gl2ext.h、OpenMAXAL.h,分别对应 Camera2 NDK API、NNAPI 神经网络接口、OpenGL ES 扩展头、OpenMAX AL 音频标准。r26b 把它们统一安装在两个位置:sysroot/usr/include 下是公共头文件,platforms/android-/arch-/usr/include 下是对应 API level 的版本。编译时只要把 sysroot 和对应 ABI 的 include 路径交出去,这些头文件都会被 clang 找到。

以 NdkCameraMetadataTags.h 为例,它定义的是 Camera2 的 metadata 标签枚举,比如 ACAMERA_LENS_FOCAL_LENGTH。Java 层用 CameraCharacteristics 读取的属性,在 Native 层通过 ACameraMetadata 接口访问,这个头文件就是访问入口。r26b 对该头文件的修订集中在 Android 14 新增的 RAW 能力标签上,因此升级到 r26b 后 Camera2 相关代码通常不需要改动。

3.2 javac 生成 JNI 头文件的完整流程

Java Native Interface 的调用链是:Java 代码声明 native 方法,编译期间由 javac 生成 C 头文件,再由 C/C++ 实现函数,最后链接成 .so。用 JDK 8 之后的 javac 即可完成头文件生成,不需要额外工具。

先准备 Java 文件:

package com.example.nativebridge; public class NativeBridge { static { System.loadLibrary("nativecore"); } public static native int add(int a, int b); public static native String getVersion(); }

编译并生成头文件:

javac -h jni -d build/classes com/example/nativebridge/NativeBridge.java

-h jni表示把生成的 .h 文件输出到 jni 目录,-d指定 class 文件输出位置。生成的 NativeBridge.h 里关键声明如下:

JNIEXPORT jint JNICALL Java_com_example_nativebridge_NativeBridge_add (JNIEnv *, jclass, jint, jint); JNIEXPORT jstring JNICALL Java_com_example_nativebridge_NativeBridge_getVersion (JNIEnv *, jclass);

函数名就是把 Java 的包名、类名、方法名用下划线拼起来。JNIEnv * 是当前线程的 JNI 环境指针,jclass 对静态 native 方法是 Class 对象的引用;如果方法是非静态的,第二个参数会变成 jobject。这里有一个容易被新手忽略的细节:头文件里的参数列表只写类型不写名字,实现文件里必须补全变量名,否则编译器报错。

3.3 用 r26b 的 clang 编译并链接

实现文件 NativeBridge.c 内容:

#include <jni.h> #include "com_example_nativebridge_NativeBridge.h" #include <string.h> JNIEXPORT jint JNICALL Java_com_example_nativebridge_NativeBridge_add (JNIEnv *env, jclass clazz, jint a, jint b) { return a + b; } JNIEXPORT jstring JNICALL Java_com_example_nativebridge_NativeBridge_getVersion (JNIEnv *env, jclass clazz) { const char *ver = "ndk-r26b"; return (*env)->NewStringUTF(env, ver); }

编译命令直接在项目根目录执行,利用前面配好的环境变量:

set NDK_BIN=%ANDROID_NDK_HOME%\toolchains\llvm\prebuilt\windows-x86_64\bin %NDK_BIN%\aarch64-linux-android21-clang.cmd ^ -I jni -I %ANDROID_NDK_HOME%\sysroot\usr\include ^ -c NativeBridge.c -o NativeBridge.o %NDK_BIN%\aarch64-linux-android21-clang.cmd ^ -shared NativeBridge.o -o libnativecore.so

第一段把源码编译成目标文件,第二段链接成动态库。这里用aarch64-linux-android21-clang.cmd直接指定了 arm64 架构和 Android 5.0 以上的 API level,而不是用通用 clang 加 --target,因为 NDK 为每个架构提供了带前缀的入口脚本,内部已经封装好 sysroot 和默认参数。如果目标设备是 32 位 ARM,换成armv7a-linux-androideabi21-clang.cmd即可。

注意-I jni写在-I sysroot之前,作用是让同名的本地头文件优先于系统头文件被找到。链接阶段不要加-c,否则不会产出 .so,只会有 .o。如果 .so 需要做初始化,比如注册 native 方法或预加载全局资源,可以在 C 侧实现 JNI_OnLoad,它会在 System.loadLibrary 返回前被调用。这里有一个资深开发者也会踩的细节:JNI_OnLoad 里调用 FindClass 时,必须用从 JNIEnv* 拿到的 ClassLoader,否则在某些 classloader 机制下会拿到错误的 Class 对象。

4. CMake 与多 ABI 构建:从手写命令到可维护工程

4.1 CMakeLists.txt 的标准写法

手写 clang 命令适合验证链路,工程化还是要交给 CMake。r26b 对 CMake 的版本要求是 3.22.1 起,低于这个版本的 CMake 会在配置阶段直接报 POLICY 错误。Android 官方模板生成的 CMakeLists.txt 一般是这样的:

cmake_minimum_required(VERSION 3.22.1) project(nativecore) add_library(nativecore SHARED NativeBridge.c ) target_include_directories(nativecore PRIVATE ${ANDROID_NDK}/sysroot/usr/include ${ANDROID_NDK}/sysroot/usr/include/${ANDROID_ABI} ) target_link_libraries(nativecore android log )

有三点值得展开。第一,${ANDROID_NDK} 不是手写的路径,而是 CMake 工具链文件注入的变量,由 Gradle 调用时自动带入。第二,${ANDROID_ABI} 在 Gradle 侧指定,取值是 armeabi-v7a、arm64-v8a、x86、x86_64 之一,工具链文件会根据它选择对应的 sysroot 路径。第三,target_link_libraries 里链了 android 和 log,前者是 Camera2、NNAPI 等 NDK 接口的宿主库,后者是 __android_log_print 等日志函数所在的库,缺了 log 会导致链接失败。

r26b 默认的 STL 策略是 c++_static,即静态链接 libc++,避免 APK 里多带一个 libc++_shared.so。但静态链接有一个隐蔽问题:如果工程里同时存在多个 .so 都静态链了 libc++,同一进程内会有两份 STL 全局状态,跨 so 传递 std::string 会崩溃。遇到跨模块传 C++ 对象的场景,需要统一改成 c++_shared。

4.2 Gradle 里控制 ABI 与 NDK 版本

app/build.gradle(Groovy DSL)下的配置:

android { defaultConfig { externalNativeBuild { cmake { cppFlags "-std=c++17" arguments "-DANDROID_STL=c++_static" } } ndk { abiFilters "arm64-v8a", "x86_64" } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.22.1" } } }

abiFilters 决定编译哪些 ABI,这里只留 arm64-v8a 和 x86_64,对应真机和模拟器的主流架构。armeabi-v7a 在近年的新设备上已经很少见,但如果产品覆盖低端 32 位设备,需要加回来。每次修改 abiFilters 后 Gradle 会重新跑一次 CMake 配置,这个阶段最容易暴露头文件路径问题。

Android Studio 在 sync 阶段会去找 CMake 可执行文件。Windows 上如果没装官方 CMake,Gradle 会尝试下载 3.22.1 版本,默认从 dl.google.com 拉取,公司内网环境经常失败。解决方式是把 CMake 换成本机安装的版本,在 local.properties 里加一行 cmake.dir 指向安装目录即可。

4.3 android.toolchain.cmake 的关键参数

不在 Android Studio 里构建,而是走命令行 CMake 时,核心入口是 NDK 自带的工具链文件:

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++_static

参数对应关系如下表:

参数取值作用
ANDROID_ABIarmeabi-v7a / arm64-v8a / x86 / x86_64选择目标架构,决定 sysroot 与编译器入口
ANDROID_PLATFORMandroid-21 等指定链接时的 API level,影响可用符号
ANDROID_STLc++_static / c++_shared / none选择 C++ 标准库链接方式
ANDROID_USELEGACY_TOOLCHAIN_FILEfalse是否回退到旧版工具链脚本,r26b 默认关闭

注意:ANDROID_PLATFORM 必须带 android- 前缀,写成android-21是合法值,写成21会在配置阶段报 "Invalid ANDROID_PLATFORM"。这个参数决定链接器从 platforms/android-21/arch-arm64 下寻找 libc.so、libm.so 等系统库。如果项目里用了只有在 API level 26 才引入的函数,而这里写 android-21,系统库里找不到符号,链接直接失败。

命令行构建完成后,产物在 build/ 目录下,不同 ABI 分开放置。r26b 同时支持 ndk-build 和 CMake 两条路径,Android.mk 的存量工程在 r26b 下依旧可以编译,但新代码不建议再用 ndk-build,因为 Android Studio 的调试体验和变量查看都围绕 CMake 定制,ndk-build 的产出也可以交给 CMake 的 imported library 机制来引用。

5. r26b 排错清单与三个高效验证技巧

5.1 运行时 UnsatisfiedLinkError 的两种典型场景

应用装到设备上调用 native 方法崩溃,logcat 显示dlopen failed: library "libnativecore.so" not found,第一个检查点是对不对得上 ABI。64 位设备会优先加载 arm64-v8a 目录下的 so,如果只编了 armeabi-v7a,加载就会失败。用 abiFilters 限制编译架构后,确认 APK 内实际包含的 so:

unzip -l app-debug.apk | findstr "libnativecore.so"

第二种场景是依赖了 C++ 标准库但没带 libc++_shared.so。在 CMake 的 arguments 里加-DANDROID_STL=c++_static能直接消除这种依赖,代价是 APK 体积增加约一到两兆。

5.2 用 llvm-readelf 核对架构与依赖

r26b 自带 llvm-readelf,比第三方 PE 查看器更贴合 NDK 语境。检查机器类型:

%ANDROID_NDK_HOME%\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-readelf.exe -h libnativecore.so | findstr Machine

输出 EM_AARCH64 是 arm64,EM_ARM 是 32 位 ARM,EM_X86_64 是 x86_64。看到 Machine 与设备不符,基本是编译入口脚本选错。查动态链接依赖:

llvm-readelf.exe -d libnativecore.so | findstr NEEDED

NEEDED 列表里如果只有 libc.so、libm.so、libdl.so 和 libandroid.so,说明没有多余的外部依赖,可以放心打进 APK;如果出现 libc++_shared.so,需要把该 so 一并放进 jniLibs,否则安装后必然崩溃。

5.3 用 source.properties 核对构建实际使用的 NDK 版本

本机装了多个 NDK 时,构建用的是哪个版本,靠 ANDROID_NDK_HOME 和 local.properties 双保险。但这两个配置都不生效时会走 Android Studio 自动安装的默认版本。r26b 的版本标识在根目录 source.properties 的 Pkg.Revision 一行:

findstr "Pkg.Revision" %ANDROID_NDK_HOME%\source.properties

输出26.1.12209101即为 r26b。构建日志里显示的 NDK 版本与预期不符时,优先查 local.properties 的 ndk.dir 是否被删。还有一种隐蔽情况:maven 依赖里的第三方模块自带 abiFilters,与你的 abiFilters 冲突时 Gradle 会合并,编译出计划外的架构,从 lint 输出里能看到实际 ABI 清单。另外,r25 与 r26b 混装的环境下,PATH 靠前的 NDK 会拦截所有 clang 调用,把带架构前缀的入口脚本全路径写进构建脚本,可以绕过这类路径污染。

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

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

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

立即咨询