☰
自定义编译OpenCV Android库:CMake与NDK实战指南
2026/10/16 3:04:18 网站建设 项目流程

直接去看官方 SDK 里的 OpenCV 包,你会发现 Android 版本的库早就帮你编好了。可一旦项目需要定制,比如去掉不需要的模块、缩小 so 体积、固定 ABi 列表、或者修改底层实现,官方预编译包就帮不上忙了。这时候老老实实从源码编译 OpenCV Android 库,反而是一次把构建机制摸透的机会。

我这里说的“自定义配置”,指的是通过修改 CMake 参数和模块列表,按自己的需求编译出一套 OpenCV Android 库。整个过程踩完一遍,会对 OpenCV 的模块依赖、NDK 工具链、JNI 接口封装都有更深的理解,后面出问题不会再两眼一抹黑。这篇文章完整记录编译的全过程,从环境准备到参数解释,再到多 ABI 产物整理和问题排查,你可以直接照着操作。

1. 编译前的整体设计思路

1.1 为什么不用官方预编译包

绝大多数 Android 项目集成 OpenCV,直接用官方 SDK 就够了。官方包里有完整的 Java 接口层、so 库和示例代码,导入模块后就可以调用。但如果你遇到过下面几个问题,就该考虑自己编译了:

体积太大。官方 OpenCV SDK 默认包含 core、imgproc、features2d、objdetect、calib3d、ml、video、dnn 等全部模块,每个 ABI 的 so 加起来可能超过几十 MB。如果只需要图像滤波和边缘检测,肯定不想把深度学习模块 dnn 的代码也塞进 APK。

多余的 ABI。官方包会为 armeabi-v7a、arm64-v8a、x86、x86_64 等平台输出全部 so。但国内 Android 应用基本只需要 arm64-v8a,最多加上 armeabi-v7a 做兼容,模拟器平台的 so 完全用不上。

不能改底层。OpenCV 的开源特性决定了你可以修改源码逻辑,比如优化某个算法、加入自定义算子、调整编译参数开启特定指令集。官方预编译包关闭了这些可能性。

需要跟项目原生代码统一编译配置。如果项目里还有自己写的 C++ 代码,用 NDK 的 CMake 工具链一起编译,能做到一个构建系统管理所有原生库,免得 OpenCV 一套、项目代码另一套,集成起来各种路径问题。

1.2 构建流程的核心逻辑

OpenCV 的 Android 编译,本质上是把 OpenCV 源码用 Android NDK 交叉编译成目标平台能用的动态库和静态库,同时生成 Java 包装层。

全流程分三段:

  1. 用 CMake 配置 OpenCV 源码,指定构建目标为 Android,传入 NDK 工具链文件。
  2. CMake 根据配置生成 Makefile 或 Ninja 构建脚本,进入源码目录下各模块完成编译,输出 .so/.a 文件。
  3. 编译 Java 包装层,生成 OpenCV 的 Java 类库模块(openCV-Library),集成到 Android Studio 项目中使用。

这里面最容易绕晕的就是模块之间的依赖关系。比如 jar 包里的 Java 类调用 native 方法,依赖的符号必须在一个 or 多个 so 文件里能查到;编译时模块之间的头文件引用关系,又决定了哪些模块不能随便裁剪。后续在自定义配置时,这两点都要照顾到。

我用一个简单类比帮你理解:OpenCV 源码是一棵菜地,CMake 参数就是挑选和种植方案,NDK 是收割工具,Java 包装层是打包好的精致包装盒。你自定义配置就是在调整“种什么、种多少、收完怎么打包”。

1.3 自定义配置的几个关键决策点

决策一:模块取舍。OpenCV 的模块分布在 opencv/modules 目录下,每个模块有独立的功能定位。裁剪前要先明确需求,比如很多人只要 core、imgproc,偶尔用到 photo 和 imgcodecs。建议先画出你自己的功能清单,再反向对照模块名。最保守的做法是只裁剪明显用不到的模块,比如 dnn、ml、stitching、superres、videostab,这些模块依赖链条长,裁剪收益大。

决策二:ABI 选择。我建议如果面向国内应用市场,只编 arm64-v8a 就够。如果还要兼顾旧设备,再编 armeabi-v7a。x86 和 x86_64 留给模拟器调试场景,实际上线不需要。

决策三:构建系统。OpenCV 官方支持 Ninja,相比默认 Makefile 速度提升明显,也方便并行构建。建议直接指定 Ninja 作为生成器。

决策四:全局控制参数。比如是否开启 NEON 指令优化、是否构建 OpenCL 支持(ANDROID_ABI 相关的 CPU 加速)、是否编译测试、是否生成静态库。这些参数会影响编译时间、产物大小和运行时的兼容性。

把这些先想明白,后面配置 CMake 参数时就不会被动接受默认值,而是根据目标去主动调整。

2. 环境准备与工具链选型

2.1 必须准备的软件清单

编译 OpenCV Android 库,本质是交叉编译,所以必须具备:

  • OpenCV 源码:注意要下完整源码包,不是只含 Java 的 SDK 包。源码包里才有 modules、platforms、CMakeLists.txt 等构建必需内容。
  • Android NDK:最好用 LTS 版本,版本与 OpenCV 源码的兼容性很重要。
  • CMake:建议 3.16 以上版本,NDK 自带的 CMake 或者本机安装都可以。
  • JDK:编译 Java 接口层时需要,版本 8 或 11(取决于你的 Android Gradle Plugin 版本)。
  • Python:OpenCV 构建脚本里有部分步骤需要 Python,提供一下环境即可,一般 2.7 或是 3.x 都行。
  • Android SDK:编译 openCV Library 模块生成 jar 和 Android 工程结构时需要。

我实测的版本组合是:OpenCV 4.5.x 源码 + NDK r23c + CMake 3.22.1 + JDK11 + Python 3.8。这套组合编译顺畅,没有因为版本匹配问题折腾太久。

2.2 NDK 版本和 OpenCV 的兼容性

NDK 版本直接影响编译能否通过。OpenCV 官方在源码的 platforms/android/build_sdk.py 和 CMake 脚本里维护了一套 NDK 兼容说明。我测试下来的经验:

  • NDK r21e/r22/r23 对 OpenCV 4.x 支持都不错,r23 开始默认使用 clang 工具链,不需要单独配置 GCC。
  • 不要用 r24 之后的太新版本编译老版本 OpenCV,会出现 GNUSTL 相关报错或者链接问题。OpenCV 4.5 及以下版本比较依赖 GNU 工具链的一些行为,太新 NDK 会改变默认工具链逻辑。
  • 不要用 ndk-r17 这种过老版本,编译某些模块时缺少__android_log_print等符号引用。

建议直接把 NDK 版本锁定在某一个稳定版,不要中途升级。如果你已经装了多个 NDK,在 CMake 参数里用ANDROID_NDK明确指定完整路径,避免系统选错。

2.3 目录规划与初始准备

我建议把源码和编译产物分开放,方便后续多次尝试不用重新解压源码。目录结构参考如下:

E:\work\opencv ├── opencv-4.5.5 # 源码根目录 ├── build_android # 存放编译中间文件和产物 │ ├── arm64-v8a │ ├── armeabi-v7a │ └── x86_64 └── output # 整理后的Android库工程

优点是:如果你想调参数重新编译,只需要删掉对应 ABI 的 build 目录,源码不受影响。不会出现源码目录下堆积一堆 .o 和 CMakeCache 的情况。

提前把 JDK 的JAVA_HOME配置好,并把 NDK 的路径记下来。后面 CMake 参数里的-DANDROID_NDK=...就要用到。

3. 核心配置:CMake 参数完全解析

3.1 构建命令模板

OpenCV Android 编译并不需要你手写一整套 CMakeLists.txt,官方已经写好了platforms/android/android.toolchain.cmake工具链文件。你只要通过参数把 NDK、ABI、API Level 告诉 CMake,剩下的交给 Opencv 的构建系统。

一个精简且完整的配置命令(以 Windows 为例):

cd E:\work\opencv\opencv-4.5.5 mkdir build_android\arm64-v8a cd build_android\arm64-v8a cmake -DCMAKE_TOOLCHAIN_FILE=E:/work/opencv/opencv-4.5.5/platforms/android/android.toolchain.cmake ^ -DANDROID_NDK=E:/Android/Sdk/ndk/23.2.8568313 ^ -DANDROID_ABI=arm64-v8a ^ -DANDROID_NATIVE_API_LEVEL=21 ^ -DANDROID_ARM_NEON=ON ^ -DCMAKE_BUILD_TYPE=Release ^ -DCMAKE_INSTALL_PREFIX=E:/work/opencv/output/arm64-v8a ^ -DOPENCV_PROVIDE_OLD_IMPL=OFF ^ -DOPENCV_EXTRA_MODULES_PATH= ^ -DBUILD_ANDROID_EXAMPLES=OFF ^ -DBUILD_TESTS=OFF ^ -DBUILD_PERF_TESTS=OFF ^ -DBUILD_DOCS=OFF ^ -DBUILD_opencv_world=OFF ^ -DBUILD_LIST=core,imgproc,imgcodecs,photo,features2d ^ -G Ninja ..

Linux/Mac 下把^换成\,路径风格调整一下。这套参数的综合思路是:只编指定的核心模块,关闭编译测试和文档,关闭 world 动态库,输出每个模块的独立 so。

3.2 每个关键参数的作用与坑点

CMAKE_TOOLCHAIN_FILE:指定交叉编译工具链。OpenCV 源码自带的 android.toolchain.cmake 能自动适配 NDK 中的 clang 工具链,这是最省事的方式。网上有人让你用 NDK 自带的 toolchain.cmake 替换,其实不行,因为 OpenCV 构建脚本里有很多针对自己工具链的变量设定,混用会出各种诡异错误。

ANDROID_ABI:目标平台架构。可选项有armeabi-v7a、arm64-v8a、x86、x86_64。注意如果你传了arm64-v8a,最终产物目录里只会生成对应的 so;要为多 ABI 打包,需要分别建目录调用多次 cmake。

ANDROID_NATIVE_API_LEVEL:指定 Android 平台的最低 API 级别。我建议设置为 21,覆盖了绝大多数 Android 设备。如果设置得太低,比如 16,意味着你要兼容 Android 4.1,编译时会引入很多旧平台的条件分支,某些代码路径反而容易出问题;设置得太高,比如 30,旧设备运行就会报找不到系统函数的错误。

ANDROID_ARM_NEON:针对 armeabi-v7a 的 NEON 指令集优化开关。arm64-v8a 默认强制开启 NEON,不需要配置。如果你最终目标包含 armeabi-v7a,建议开启,图像算法性能会有明显提升。开启后,OpenCV 的某些 kernel 会走 NEON 优化路径,毕竟 ARMv7 时代的浮点性能主要靠它。

BUILD_LIST:这是自定义配置的核心参数。用逗号分隔模块名,只构建列表里的模块。OpenCV 构建系统会自动解析这些模块对 core、imgproc 等基础模块的依赖,所以你不必手动把依赖模块全都写上,比如指定 imgproc 后 core 会自动带出来。但你要清楚地意识到:裁剪后部分同时依赖多个被裁剪模块的高级功能会不可用,比如video模块如果被裁掉,基于它的videoio(摄像头读取)相关接口就会失效。

BUILD_opencv_world:OpenCV 官方默认会在 Android 下生成一个libopencv_world.so,把所有模块合并进一个大 so。合并的好处是代码间直接引用,不用处理模块间依赖的运行时顺序;坏处是体积大,改动一个模块要整体重编。我配置为 OFF,让每个模块生成独立 so,比如libopencv_core.so、libopencv_imgproc.so,方便裁剪和单独升级。代价是 APK 里会多几个 so 文件,加载时要依次System.loadLibrary,下文会讲。

BUILD_TESTS、BUILD_PERF_TESTS、BUILD_DOCS:这些默认会拉取大量测试框架、文档生成工具和额外依赖,编译时间能拖长一两个小时。全部关掉,只保留核心库。

BUILD_ANDROID_EXAMPLES:官方会把 samples 目录里的 Android 示例也加入构建,非常耗时且没必要,强制 OFF。

OPENCV_EXTRA_MODULES_PATH:如果你想加入 OpenCV contrib 模块(如 dnn 的某些增强、ximgproc 等),通过这个参数指定路径。这里留空表示不添加贡献模块。

除了这些,还有两个隐藏参数值得关注:

CMAKE_BUILD_TYPE:必须为Release。用 Debug 类型编译出来的 so 体积巨大,且包含大量调试符号,放到 Android 项目中运行也会有性能损失。如果需要调试,可以在 NDK 层面设-DCMAKE_CXX_FLAGS_DEBUG="-g",但平时直接用 Release。

ANDROID_TOOLCHAIN_NAME:老版本 OpenCV 需要显式指定,OpenCV 4.x 基本根据 ANDROID_ABI 自动选择 clang 工具链,不需要手动传,传了反而可能因为名称不匹配导致 cmake 报错。

3.3 模块裁剪清单与依赖关系

很多人纠结怎么选模块,我梳理了一份常用功能的模块映射表。你可以对照自己的需求来勾选:

需要的功能对应的OpenCV模块
基础矩阵运算、图像数据结构core
图像滤波、几何变换、直方图、边缘检测imgproc
图像编解码(imread/imwrite)imgcodecs
特征检测与描述子(ORB/SIFT等)features2d
相机标定、立体视觉calib3d
视频分析、背景分割、光流video
深度学习推理(DNN)dnn
图像修复、去噪、HDRphoto
机器学习传统算法(SVM/KMeans等)ml
图像拼接stitching
超分辨率superres
视频稳定videostab
目标检测级联分类器(人脸检测)objdetect

模块依赖关系里有两个非常容易忽略的点:

第一,features2d依赖calib3d的头文件,即使你只用它的 ORB 特征,也会把 calib3d 的部分代码拉进来。所以裁剪时不用打破砂锅问到底去手写依赖列表,让 CMake 解析即可,编译产物里会自动包含所需依赖模块的 so。

第二,dnn模块对第三方库(Protobuf、flatbuffers)有很强的依赖,如果你的 BUILD_LIST 里包含 dnn,构建时会自动下载或编译这些依赖。你如果不想要这些额外负担,干脆从一开始就不要把 dnn 放进列表。

从我实际编译的产物大小来看:只编 core、imgproc、imgcodecs、photo、features2d 五个模块,arm64-v8a 平台下每个 so 加起来大约 8-12MB(Release,不含调试符号); 而全量模块编译后单个平台可能要 60MB 以上。差距非常明显,这也是自定义编译最大的收益点。

4. 多 ABI 编译与产物整理

4.1 多 ABI 的编译策略

实际上线时,你可能需要同时输出 arm64-v8a 和 armeabi-v7a 两个平台的 so。我的做法是一个 ABI 一个构建目录,用脚本串行或并行执行。

Linux/Mac 下可以用简单的 for 循环:

ABIS=("arm64-v8a" "armeabi-v7a") for abi in ${ABIS[@]} do rm -rf build_android/$abi mkdir -p build_android/$abi cd build_android/$abi cmake \ -DCMAKE_TOOLCHAIN_FILE=../../opencv-4.5.5/platforms/android/android.toolchain.cmake \ -DANDROID_NDK=/opt/android-ndk-r23c \ -DANDROID_ABI=$abi \ -DANDROID_NATIVE_API_LEVEL=21 \ -DANDROID_ARM_NEON=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=../../output/$abi \ -DBUILD_LIST=core,imgproc,imgcodecs,photo,features2d \ -DBUILD_TESTS=OFF \ -DBUILD_PERF_TESTS=OFF \ -DBUILD_EXAMPLES=OFF \ -DBUILD_opencv_world=OFF \ -G Ninja \ ../../opencv-4.5.5 ninja ninja install cd ../.. done

Windows 下没有现成的 for 循环直接用,你可以写一个简单的 batch 脚本,或者分两次调用。因为两次编译互不影响,脚本足够简单。

注意 CMake 配置阶段会输出一个OpenCVModules.cmake之类的辅助文件,你需要留意CMAKE_INSTALL_PREFIX设置是否正确。ninja install会在指定目录下生成包括头文件、库文件、cmake 配置文件的完整包,等下整合 Android 工程时就直接从这个目录复制。

并行编译提效:Ninja 本身会根据 CPU 核数并行,如果你想限制核数避免内存不足,可以追加-j N参数:

ninja -j 8

如果编译中途因为内存不足被杀进程,缩小-j数值重试即可。

4.2 Java 包装层是如何生成的

OpenCV Android Java API 并不是手写的,而是在构建过程中自动生成的。构建完成后,在build_android/abi目录下会出现一个OpenCV或类似命名的模块目录,里面包含src/java、jni、libs等关键内容。

Java 层的生成逻辑是:OpenCV 源码的modules/java/generator里维护了一套描述文件(.list),列出了哪些 C++ 函数需要暴露到 Java,然后构建时通过 java generator 工具生成对应的.java文件。这些 Java 代码调用 JNI 层,JNI 层的 C++ 代码在modules/java/jni目录里预先写好了骨架,编译时生成对应平台的.so。

所以当你自定义裁剪模块后,Java API 里对应模块的类也会减少。比如你没有编videoio模块,那么 Java 里的VideoCapture类就不会生成。

有些开发者会直接跳过 Java 层,只用 C++ 接口,通过自己的 JNI 层封装。这样做的好处是减少一层间接调用,性能理论上更好;坏处是需要自己维护 JNI 代码。我的建议:如果你业务逻辑主要是算法,建议直接用 Java API,开发效率高很多;如果你要在 native 层做大量图像处理,可以只用 C++ 接口,完全抛开 OpenCV 的 Java 包装。

4.3 整合到 Android Studio 的完整步骤

源码编译完不是终点,把产物放进一个 Android 工程并正常调用,才算真正成功。以下是我常用的整合方式。

第一步:新建或准备一个 Android 工程

如果你的项目里已有 so 集成方案,直接替换即可。没有的话,新建一个普通 Android 项目,然后在app/src/main下新建jniLibs目录,按 ABI 建子目录:

app/src/main/jniLibs/ ├── arm64-v8a/ │ ├── libopencv_core.so │ ├── libopencv_imgproc.so │ ├── libopencv_imgcodecs.so │ ├── libopencv_photo.so │ └── libopencv_features2d.so └── armeabi-v7a/ └── (同so列表)

第二步:放入 Java API

把编译后生成的 OpenCV Java 模块里的src/java/org整个目录复制到你的项目app/src/main/java下。这个org.opencv包就是所有 Java API 的入口,包括Mat、Imgproc这些核心类。注意如果之前你已经引入过 OpenCV SDK,需要先删掉老包,避免类冲突。

第三步:加载 so

在 Application 或用到 OpenCV 的 Activity 中,按顺序加载所有模块:

static { System.loadLibrary("opencv_core"); System.loadLibrary("opencv_imgproc"); System.loadLibrary("opencv_imgcodecs"); System.loadLibrary("opencv_photo"); System.loadLibrary("opencv_features2d"); }

加载顺序不是随意的,模块之间有依赖关系。imgproc 依赖 core 的符号,所以 core 必须最先加载。有时你只加载 imgproc 也能通过,因为 System.loadLibrary 在加载时会进行符号解析,如果找不到依赖的 so 会直接抛UnsatisfiedLinkError,所以保险起见按依赖顺序全部加载。

如果你编译时开了BUILD_opencv_world=ON,则只需要加载一个:

System.loadLibrary("opencv_world");

第四步:build.gradle 配置

在 app 模块的android节点下,允许打包指定 ABI:

android { defaultConfig { ndk { abiFilters "arm64-v8a", "armeabi-v7a" } } }

这一步也可以不写,默认会把 jniLibs 下所有 so 都打进去,但写上 abiFilters 能明确控制安装包支持的平台,避免 x86 so 混入导致体积增大。

第五步:调用验证

用一个最简单的测试,读入图、做高斯模糊、再输出:

Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); Mat src = new Mat(); Utils.bitmapToMat(srcBitmap, src); Mat dst = new Mat(); Imgproc.GaussianBlur(src, dst, new Size(15, 15), 0); Utils.matToBitmap(dst, srcBitmap); imageView.setImageBitmap(srcBitmap);

如果这一条链路正常工作,说明 so 加载、Java API 代码生成、JNI 符号解析都成功了,整套自定义编译流程闭环。

5. 编译加速与脚本技巧

5.1 善用 ccache 缓存

OpenCV 全量编译非常耗时,即使裁剪模块,反复调整参数重新编译也会浪费大量时间。在这个场景下,ccache 是不可或缺的工具。

ccache 是一个编译缓存,它缓存每次编译的预处理结果和汇编产物。当你重新 cmake 后,没有修改的源文件会直接命中缓存,跳过编译。实测下来,第二次编译速度能提升 3-5 倍。

安装并启用的方法:

# 安装(ubuntu为例) sudo apt install ccache # 在CMake配置时开启 cmake -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ...

也可以在环境变量层面统一生效:

export CCACHE_DIR=/path/to/ccache_cache

需要注意 ccache 对编译器路径非常敏感,如果 NDK 换了版本,之前的缓存可能全部 miss,这是正常的。另一个坑是 ccache 必须在路径里能被找到,Windows 下如果你下载了 ccache.exe,要把它的路径加入 PATH。

5.2 环境变量与脚本封装

编译命令很长,而且多 ABI 要重复执行,我建议把整套流程写成脚本。以 Linux 为例,保存为build_opencv_android.sh:

#!/bin/bash set -e OPENCV_SRC=/path/to/opencv-4.5.5 NDK_ROOT=/path/to/android-ndk-r23c CMAKE_TOOLCHAIN=$OPENCV_SRC/platforms/android/android.toolchain.cmake MODULE_LIST="core,imgproc,imgcodecs,photo,features2d" ABIS=("arm64-v8a" "armeabi-v7a") for abi in "${ABIS[@]}" do BUILD_DIR=build_android/$abi INSTALL_DIR=output/$abi rm -rf $BUILD_DIR $INSTALL_DIR mkdir -p $BUILD_DIR cmake -S $OPENCV_SRC -B $BUILD_DIR \ -DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN \ -DANDROID_NDK=$NDK_ROOT \ -DANDROID_ABI=$abi \ -DANDROID_NATIVE_API_LEVEL=21 \ -DANDROID_ARM_NEON=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR \ -DBUILD_LIST=$MODULE_LIST \ -DBUILD_TESTS=OFF \ -DBUILD_PERF_TESTS=OFF \ -DBUILD_EXAMPLES=OFF \ -DBUILD_opencv_world=OFF \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -G Ninja cmake --build $BUILD_DIR --config Release -j $(nproc) cmake --install $BUILD_DIR done

脚本最后加一句echo "build done: output/",方便确认执行结果。

Windows 用户可以用 PowerShell 或 batch 脚本。我在 Windows 下测试时发现一个坑:-S和-B路径如果带有空格,必须用引号包起来。路径里最好也不要带非英文字符,因为 NDK 的 clang 和 CMake 对路径编码比较敏感,中文路径会引发各种找不到文件的问题。

5.3 调整模块列表后的重编策略

你一定会遇到这种情况:第一次编译只编了 core 和 imgproc,后面需求变化又要加入 features2d。这时候不需要从头全部重编,只需把新模块加进BUILD_LIST,CMake 会检测源码目录时间戳,只重新构建新增模块及其依赖链。

但有个前提:不要随意修改BUILD_LIST里的旧模块。比如你从core,imgproc改成core,imgproc,features2d,core 和 imgproc 的源码没有变化,它们的二进制会保留。如果你中途把 imgproc 从列表里拿掉,CMake 会重新评估整个依赖图,之前编译的 imgproc so 会被视为不需要构建,但它在输出目录里仍然存在——手动清理CMAKE_INSTALL_PREFIX下的旧 so 即可,免得集成时不小心打包了一个不再需要的库。

6. 常见问题与排查技巧实录

6.1 找不到编译器或工具链

现象:CMake 配置时报错CMAKE_C_COMPILER not set或Unable to find the CMAKE_C_COMPILER。

原因:绝大多数是CMAKE_TOOLCHAIN_FILE路径不对,或 NDK 目录里没有对应 ABI 的工具链。

排查方法:

  1. 检查-DCMAKE_TOOLCHAIN_FILE是否指向 OpenCV 源码的platforms/android/android.toolchain.cmake,不要误用 NDK 自带的那个。
  2. 检查-DANDROID_NDK路径下是否存在toolchains/llvm/prebuilt/linux-x86_64/bin(不同系统目录名不同)。
  3. 检查环境变量ANDROID_NDK_HOME是否被设置成旧版本,影响 CMake 内部的逻辑。

注意:如果同时设置了多个 NDK 环境变量,CMake 解析 toolchain.cmake 时可能会产生优先级冲突。建议只保留一个全局 NDK 变量,其余全部删掉。

6.2 编译时报undefined reference to cv::xxx

现象:编译某个模块时,链接阶段报一堆undefined reference to,比如cv::Mat的符号找不到。

原因:模块之间的依赖没被正确解析,或者裁剪模块时把基础模块给去掉了。比如你的BUILD_LIST只写了imgproc,没写core,但 imgproc 的源码里引用了 core 的符号。虽然 CMake 理论上会自动把依赖模块带上,但如果你直接手改了模块目录下的 CMakeLists,或者用BUILD_LIST写了非官方模块名,MATLAB 模式下就可能出错。

解决方法:不要手动修改各个模块的 CMakeLists 增减依赖,用BUILD_LIST顶层配置。确认BUILD_LIST里至少包含core。如果你确实需要自定义新模块,需要额外检查它自己声明的依赖。

6.3 编译过程中内存不足或编译速度极慢

现象:Ninja 进入多任务编译后,系统内存占用飙升,甚至 OOM,或者编译卡了半小时还在某几个源文件上。

原因:OpenCV 的源码编译非常吃内存,尤其 NEON 优化代码和一些模板库。并行任务数超过了机器物理内存的承受能力。

解决方案:

ninja -j 2

小核数慢一点没关系,稳定优先。另外,检查是不是把BUILD_TESTS和BUILD_PERF_TESTS忘了关,这些测试代码里的模板展开和 OpenCV 的 Test Framework 会大幅增加编译工作量。

6.4 生成产物缺少头文件或 jar 包

现象:ninja install完成后,CMAKE_INSTALL_PREFIX目录里只有 sdk 的 native 部分,没有 Java 层代码和 jar。

原因:Java 包装层的构建不是ninja默认目标的一部分。OpenCV 在 Android 构建里,会额外生成 OpenCV Library 模块,但是否生成受BUILD_ANDROID_PROJECTS或类似参数控制。

解决方法:去build_android/<abi>/目录下找OpenCV模块目录,里面通常会有build.gradle、src/main等。直接把这个模块作为库模块导入你的 Android 工程,或者手动拷贝它的src/main/java、libs、jniLibs。另外要注意,不同 ABI 的构建目录应该分别保存,不要互相覆盖。

6.5 运行时报java.lang.UnsatisfiedLinkError

现象:App 运行到加载 OpenCV 时崩溃,日志显示某个 native 方法找不到,比如cv::Mat::n_1Mat()。

原因:Java 代码存在,但 JNI 层对应的 so 没加载成功,通常是三种情况:

  1. 没有按依赖顺序加载所有模块,只 loadLibrary 了某个模块。
  2. so 没有完整打包进 APK,检查jniLibs目录里的 ABI 目录名是否和构建 .so 时用的 ABI 匹配。
  3. Java 包装层代码版本和 so 版本不一致,比如你用官方 SDK 的 org.opencv 包,配的是自己编译的 so,JNI 函数签名对不上。

排查思路:

# 查看APK里是否有对应ABI的so文件 unzip -l app-debug.apk | grep "opencv"

如果 APK 里有 so 但运行时仍报错,再用nm -D libopencv_core.so | grep Java_看 JNI 导出符号,确认 so 里确实有 Java 层需要的 native 方法。

6.6 自定义模块后编译成功但功能缺失

现象:编译和集成都正常,但运行某个 API 发现并没有生效,比如调VideoCapture打不开摄像头。

原因:videoio模块没有编进去,或者 Java 层VideoCapture类存在但底层 so 没有对应实现。这也解释了为什么我说要根据功能选模块,而不是从网上随便抄一两个“用不到的模块”就不管了。

验证方法:在编译前,先查看modules/java/generator里的模块映射,确认你的功能对应的模块到底叫什么名字。比如摄像头采集这一功能,官方 Java API 把它放在org.opencv.videoio.VideoCapture类中,对应 native 实现位于 videoio 模块。你的BUILD_LIST没包含这个模块,就会编译出类,但运行时找不到 native 方法。

7. 我踩过的几个坑和最终建议

基于完整的实操,把最容易踩坑的地方再提一遍。

第一,NDK 版本不能乱换。网上很多人的编译失败,导火索是新版 NDK 改了默认链接器和头文件路径。我个人的建议是固定一个 NDK 版本,比如 r23c,能用到 OpenCV 4.10 发布都没问题。不要图新升级 NDK 大版本,除非你确认 OpenCV 官方支持。

第二,不要把编译目录建在源码目录里。如果你执行了cmake ..并把 build 目录放在源码根目录下,再想重新配置时会发现 CMakeCache.txt 里残留了旧 ABI 信息,切换 ABI 会报错。一定用build_android/<abi>这种中间目录,保证每个 ABI 的构建环境完全隔离。

第三,模块裁剪不要拍脑袋。建议先核对功能映射表再调整。不过裁剪的边界不用太死,如果你不需要考虑 APK 体积,直接用全量编译或保留默认模块列表也行。自定义编译的主要收获是理解了构建过程,而不是一味追求极致体积。

第四,Java 包装层和 so 要同步更新。如果你改了 OpenCV 的 native 源码但没重新生成 Java 包装层,最终只会得到一份和 JNI 签名对不上的“歪库”。每次重新编译前清空旧的org.opencv包,重新拷贝新生成的 Java 文件。

第五,善用脚本和缓存。手动敲这几百字 CMake 命令的次数越少越好。把脚本保存到 git 仓库,后续换电脑或者交给同事,直接一个脚本搞定,节省的时间非常可观。

另外想提一个容易被忽略的点:自定义 OpenCV 库之后,再配合 NDK 的abiFilters,APK 里原生库的体积能压缩到非常理想的状态。我做过一次对比,全量官方包四个 ABI 打包大约 300MB 的应用,裁剪后只留 arm64-v8a 加必要的 Java 层,体积直接降到 30MB 以内,加载速度也有所提升。

如果你在产品研发中遇到了 OpenCV 定制需求,希望这篇记录能帮你少走一些弯路。最后建议你第一次编译时不要开太多自定义项,先用官方默认配置跑通一遍,确认环境无误后再调整模块和参数,这样能避免“一改全错”的排查地狱。

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

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

立即咨询