ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android NDK r26b Windows环境配置与JNI交叉编译实践

Android NDK r26b Windows环境配置与JNI交叉编译实践 简介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 16clang 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.cmd、armv7a-linux-androideabi21-clang.cmd、x86_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-r26bsetx 不需要管理员权限但只对之后新开的终端生效。如果当前 CMD 窗口里马上要用先手动 set 一遍set ANDROID_NDK_HOMED:\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.dirD:/dev/android-ndk-r26b配置完成后重新 syncGradle 会在 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.gradleGroovy DSL下的配置android { defaultConfig { externalNativeBuild { cmake { cppFlags -stdc17 arguments -DANDROID_STLc_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 上如果没装官方 CMakeGradle 会尝试下载 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_ABIarm64-v8a ^ -DANDROID_PLATFORMandroid-21 ^ -DANDROID_STLc_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 内实际包含的 sounzip -l app-debug.apk | findstr libnativecore.so第二种场景是依赖了 C 标准库但没带 libc_shared.so。在 CMake 的 arguments 里加-DANDROID_STLc_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 是 arm64EM_ARM 是 32 位 ARMEM_X86_64 是 x86_64。看到 Machine 与设备不符基本是编译入口脚本选错。查动态链接依赖llvm-readelf.exe -d libnativecore.so | findstr NEEDEDNEEDED 列表里如果只有 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 调用把带架构前缀的入口脚本全路径写进构建脚本可以绕过这类路径污染。本文还有配套的精品资源点击获取
返回列表