49、Android NDK:使用CMake编译Android原生库
说实话,Android开发这么多年,我一直觉得纯Java/Kotlin写应用挺舒服的。但一旦涉及到性能敏感的场景——比如图像处理、音视频编解码、或者游戏引擎——你就不得不请出C++这位老大哥了。
我记得第一次在Android项目里集成C++代码时,用的是老旧的Android.mk + ndk-build那套。说实话,那体验真不怎么样。后来Google官方开始推荐CMake,我试了一下,嗯,这才对味嘛。
今天我们就聊聊,怎么用CMake在Android NDK环境下编译原生库。
49.1 环境准备:你需要的工具
在动手之前,先把家伙事儿备齐。你需要:
- Android Studio(3.0以上版本,越新越好)
- NDK(Native Development Kit)
- CMake(Android Studio自带,也可以单独装)
安装NDK很简单:打开Android Studio → SDK Manager → SDK Tools → 勾选NDK和CMake → Apply。等它下载完就行了。
ndk.dir=/path/to/ndk。
49.2 项目结构:CMakeLists.txt放哪儿?
一个典型的Android NDK项目结构是这样的:
app/
├── src/
│ └── main/
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ ├── native-lib.cpp
│ │ └── include/
│ └── java/
└── build.gradle
CMakeLists.txt放在src/main/cpp/目录下,这是Android Studio的默认约定。当然你也可以放别处,但需要在build.gradle里指定路径。
49.3 编写CMakeLists.txt:从零开始
好,我们直接上手写一个。假设我们要编译一个叫native-lib的库,它依赖一个第三方库libpng。
cmake_minimum_required(VERSION 3.10)
# 设置项目名
project("my_native_lib")
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加原生库
add_library(
native-lib
SHARED
native-lib.cpp
utils.cpp
)
# 查找系统库(比如log)
find_library(
log-lib
log
)
# 链接库
target_link_libraries(
native-lib
${log-lib}
)
这段代码干了三件事:
- 声明了一个叫
native-lib的共享库 - 指定了源文件
- 链接了Android的log库(方便打印日志)
嗯,这里要注意:SHARED表示编译成动态库(.so),如果你想要静态库就用STATIC。我在项目中一般用动态库,因为可以减少APK体积,而且多个库之间可以共享代码。
49.4 在build.gradle中集成CMake
光有CMakeLists.txt还不够,你得告诉Gradle去调用CMake。在app/build.gradle的android块里加上:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-O2 -frtti -fexceptions"
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
}
这里有几个关键点:
- cppFlags:传给C++编译器的参数。
-O2是优化级别,-frtti和-fexceptions分别启用RTTI和异常支持。我个人建议除非必要,否则别开异常,因为会增加二进制体积。 - abiFilters:指定要编译的CPU架构。现在主流是arm64-v8a,但为了兼容老设备,我一般会加上armeabi-v7a。x86_64主要是给模拟器用的。
49.5 多库依赖:链接第三方库
实际项目中,你很少只编译一个孤零零的库。比如你要用OpenCV或者FFmpeg,就得处理依赖关系。
假设我们有一个预编译的libpng.so放在app/src/main/jniLibs/目录下,CMakeLists.txt里这样写:
# 导入预编译库
add_library(
png
SHARED
IMPORTED
)
set_target_properties(
png
PROPERTIES
IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libpng.so
)
# 链接到我们的库
target_link_libraries(
native-lib
png
${log-lib}
)
这里用IMPORTED关键字告诉CMake:这个库不是我编译的,是外面拿来的。然后通过IMPORTED_LOCATION指定它的路径。
你想想看,如果每个架构的libpng.so都放在不同目录,用${ANDROID_ABI}这个变量就能自动匹配,是不是很方便?
49.6 知识体系:一张图看懂
说了这么多,我画张图帮你理清整个流程:
49.7 避坑指南:我踩过的那些坑
做NDK开发,坑是真的不少。我挑几个常见的说说:
- STL链接问题:如果你用了
std::string或std::vector,记得在CMakeLists.txt里加上set(CMAKE_SHARED_LINKER_FLAGS "-static-libstdc++"),否则运行时可能崩溃。 - JNI函数命名:Java层的native方法名必须和C++函数名严格对应。比如
com.example.MyClass.nativeMethod对应Java_com_example_MyClass_nativeMethod。少一个下划线都不行。 - ABI不匹配:我曾经把arm64的.so放到了armeabi-v7a的目录下,结果在旧设备上直接闪退。所以一定要检查
jniLibs目录结构是否正确。
49.8 调试与验证
编译完成后,怎么确认.so文件真的打进了APK?
用Android Studio的APK Analyzer:Build → Analyze APK → 选择你的apk文件。然后展开lib/目录,就能看到各个架构下的.so文件了。
或者用命令行:
unzip -l app-debug.apk | grep .so
如果看到了libnative-lib.so,恭喜你,成功了。
嗯,其实NDK开发没有想象中那么复杂。你只要把CMakeLists.txt写对,Gradle配置好,剩下的就是写C++代码的事了。我个人觉得,CMake比Android.mk直观太多了,至少你不用记那些奇怪的语法。
好了,这一章就到这里。去试试吧,把第一个.so编译出来,你会很有成就感的。