Android 主流开发语言是 Kotlin,但很多场景你绕不开 C/C++:
图像处理、音视频编解码。
加密算法、安全模块。
复用已有的 C++ 库(如 OpenCV、FFmpeg、游戏引擎)。
对性能极度敏感的计算。
这篇会讲 Android Native 开发的核心:JNI、NDK、CMake,以及 Kotlin 和 C++ 怎么协作。
一、什么时候该用 Native?
不是性能瓶颈就一定要上 Native。Native 代码会带来:
包体积增大。
构建复杂度增加。
崩溃更难定位(栈信息、符号表)。
平台差异(ARM64 / x86_64 / ABI)。
适合用 Native 的场景:
已有 C/C++ 资产需要复用。
计算密集且 Kotlin 无法优化到目标性能。
需要绕过 JVM 的内存管理。
安全敏感逻辑需要隐藏实现。
不适合的场景:
普通 UI 逻辑。
简单的网络请求。
能用 Kotlin 协程 + 算法优化解决的问题。
二、JNI 基础:Kotlin 调用 C++
2.1 声明 Native 方法
class NativeLib {
external fun stringFromJNI(): String
external fun add(a: Int, b: Int): Int
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
2.2 生成头文件
用 javac -h 生成 JNI 头文件:
javac -h jni src/main/java/com/example/NativeLib.kt
2.3 C++ 实现
// native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeLib_stringFromJNI(JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_NativeLib_add(JNIEnv* env, jobject /* this */, jint a, jint b) {
return a + b;
}
2.4 方法签名规则
JNI 方法名格式:
Java_包名_类名_方法名
包名里的点换成下划线,下划线换成 _1。
例如 com.example.NativeLib.add → Java_com_example_NativeLib_add。
三、NDK 与 CMake 构建
3.1 build.gradle 配置
android {
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
defaultConfig {
externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
abiFilters += listOf("arm64-v8a", "x86_64")
}
}
}
}
3.2 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}
)
3.3 ABI 选择
建议:只保留 arm64-v8a 和 x86_64,包体积更小。如果必须支持 32 位,再加 armeabi-v7a。
四、JNI 引用类型与内存管理
4.1 局部引用 vs 全局引用
jstring local = env->NewStringUTF("local");
// 局部引用在函数返回后自动释放
// 如果需要长期保存,转成全局引用
jstring global = (jstring) env->NewGlobalRef(local);
// 用完必须手动释放
env->DeleteGlobalRef(global);
4.2 不要阻塞 JNI 调用
// 错误:长时间运行会阻塞 JVM 线程
JNIEXPORT void JNICALL
Java_com_example_NativeLib_heavyWork(JNIEnv* env, jobject thiz) {
for (int i = 0; i < 1e9; i++) { ... }
}
如果 Native 任务耗时,应该在 C++ 层自己开线程,或者用 Kotlin 协程 + Dispatchers.Default 调用。
4.3 数组传递
JNIEXPORT void JNICALL
Java_com_example_NativeLib_processArray(JNIEnv* env, jobject thiz, jintArray arr) {
jint* elements = env->GetIntArrayElements(arr, nullptr);
jsize len = env->GetArrayLength(arr);
for (int i = 0; i < len; i++) {
elements[i] *= 2;
}
env->ReleaseIntArrayElements(arr, elements, 0);
}
注意 Get 和 Release 必须配对,否则内存泄漏。
五、Kotlin 与 C++ 协作模式
5.1 数据模型下沉
不要频繁跨 JNI 边界传复杂对象。正确做法是:
Kotlin 把输入序列化成 ByteArray 或 FloatArray。
一次性传给 Native。
Native 处理完,返回数组或句柄。
val input = FloatArray(1000)
val output = nativeLib.processAudio(input)
5.2 对象生命周期管理
如果 Native 层持有资源,建议封装成 Closeable:
class NativeProcessor : AutoCloseable {
private var handle: Long = nativeCreate()
fun process(data: ByteArray): ByteArray = nativeProcess(handle, data)
override fun close() {
nativeDestroy(handle)
handle = 0
}
}
配合 use { } 自动释放。
六、Native 调试与崩溃定位
6.1 符号表
发布 Native 库时保留符号表:
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g")
或者用 ndk-stack 还原崩溃栈:
adb logcat | ndk-stack -sym obj/arm64-v8a/
6.2 AddressSanitizer
./gradlew app:assembleDebug -Pandroid.enableAsan=true
AddressSanitizer 能检测 Native 层的内存越界、Use-after-free,但会让 App 变慢。
七、性能与包体积权衡
小结
Native 开发是 Android 专家的能力边界之一:
只在必要场景引入 C/C++。
JNI 方法签名、局部/全局引用、数组释放要配对。
用 CMake 管理构建,合理选择 ABI。
跨 JNI 边界传简单数据,避免频繁对象交互。
保留符号表,用 ndk-stack / ASan 定位 Native 崩溃。
下篇预告
下一篇《第 13 篇:性能监控框架:从埋点到线上 APM 的闭环》会讲如何搭建线上性能监控:
埋点设计:性能指标怎么选。
本地采集:Choreographer、FrameMetrics、Memory。
上报策略:采样、聚合、离线缓存。
参考阅读
Android NDK 官方文档
JNI 官方规范
《Android C++ 高级编程》
上一篇文章:第 11 篇《Gradle 插件化与构建优化:把编译时间砍半》