一、为什么要用NDK和JNI
在Android开发中,大部分业务逻辑都可以用Java或Kotlin搞定,但有些场景不得不请出"原生代码"这个外援:
- 性能敏感场景:比如音视频编解码、图像处理等算法,C++的执行效率比Java高出一个数量级
- 复用现有库:很多成熟的开源库(如OpenCV、FFmpeg)都是用C/C++写的
- 硬件底层操作:需要直接操作传感器或特定硬件时
不过跨语言调用就像两个说不同语言的人交流,需要个翻译——这就是JNI(Java Native Interface)的作用。
二、搭建NDK开发环境
1. 工具准备(以Android Studio为例)
// build.gradle配置示例
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17" // 使用C++17标准
arguments "-DANDROID_STL=c++_shared" // 使用动态STL库
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt" // CMake配置文件路径
version "3.22.1"
}
}
}
2. 创建第一个JNI调用
Java层声明native方法:
public class NativeLib {
// 加载动态库
static {
System.loadLibrary("native-lib");
}
// 声明native方法
public native String stringFromJNI();
}
C++层实现(注意方法名规范):
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
三、JNI数据类型与高级用法
1. 类型映射表
| Java类型 | JNI类型 | C/C++类型 |
|---|---|---|
| boolean | jboolean | unsigned char |
| int | jint | int |
| String | jstring | const char* |
| int[] | jintArray | jint* |
2. 复杂数据传递示例
Java层传递对象到Native:
public class UserData {
public int id;
public String name;
public native void processInNative(UserData user);
}
C++层处理:
extern "C" JNIEXPORT void JNICALL
Java_com_example_UserData_processInNative(
JNIEnv* env,
jobject thiz,
jobject userObj) {
// 获取Java类引用
jclass userClass = env->GetObjectClass(userObj);
// 获取字段ID
jfieldID idField = env->GetFieldID(userClass, "id", "I");
jfieldID nameField = env->GetFieldID(userClass, "name", "Ljava/lang/String;");
// 读取字段值
jint id = env->GetIntField(userObj, idField);
jstring name = (jstring)env->GetObjectField(userObj, nameField);
// 转换成C字符串
const char* nameStr = env->GetStringUTFChars(name, nullptr);
// 业务处理(示例:打印日志)
__android_log_print(ANDROID_LOG_INFO, "JNI",
"UserID: %d, Name: %s", id, nameStr);
// 释放资源
env->ReleaseStringUTFChars(name, nameStr);
}
四、原生代码调试技巧
1. LLDB调试配置
在Android Studio的Run -> Edit Configurations中添加:
{
"type": "lldb",
"request": "attach",
"name": "Attach to Native",
"processId": "${android_pid}"
}
2. 实用调试命令
# 查看线程状态
thread list
# 添加断点
breakpoint set --file native-lib.cpp --line 42
# 查看变量值
frame variable
# 查看内存内容
memory read --size 4 --format x --count 16 0x16fd3a20
3. 日志输出技巧
#include <android/log.h>
#define LOG_TAG "JNI_DEBUG"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
void complexCalculation() {
LOGD("Starting calculation at %lld", getCurrentTime());
// ...计算逻辑
if (error) {
LOGE("Calculation failed with code %d", errorCode);
}
}
五、实战:性能敏感场景优化
图像处理示例
Java层:
public class ImageProcessor {
public native void convertToGray(Bitmap bitmap);
static {
System.loadLibrary("image-processing");
}
}
C++层实现:
extern "C" JNIEXPORT void JNICALL
Java_com_example_ImageProcessor_convertToGray(
JNIEnv* env,
jobject thiz,
jobject bitmap) {
AndroidBitmapInfo info;
void* pixels;
// 获取Bitmap信息
AndroidBitmap_getInfo(env, bitmap, &info);
// 锁定像素缓冲区
AndroidBitmap_lockPixels(env, bitmap, &pixels);
// 灰度化处理
uint8_t* p = (uint8_t*)pixels;
for(int y = 0; y < info.height; y++) {
for(int x = 0; x < info.width; x++) {
uint8_t* pixel = p + y * info.stride + x * 4;
uint8_t gray = (uint8_t)(0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2]);
pixel[0] = pixel[1] = pixel[2] = gray;
}
}
// 解锁像素缓冲区
AndroidBitmap_unlockPixels(env, bitmap);
}
六、常见问题解决方案
1. JNI引用管理
// 局部引用会自动释放,但大量创建时需要手动管理
jobject createGlobalRef(JNIEnv* env, jobject obj) {
jobject globalRef = env->NewGlobalRef(obj);
// ...使用globalRef
env->DeleteGlobalRef(globalRef); // 必须手动释放
return globalRef;
}
// 弱引用示例
jweak createWeakRef(JNIEnv* env, jobject obj) {
jweak weakRef = env->NewWeakGlobalRef(obj);
// 使用前需要检查是否被GC
if (env->IsSameObject(weakRef, NULL)) {
// 对象已被回收
}
return weakRef;
}
2. 异常处理模式
extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeLib_riskyOperation(
JNIEnv* env,
jobject thiz) {
// 清空之前的异常
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
// 执行可能抛出异常的操作
jclass cls = env->FindClass("java/lang/NullPointerException");
if (cls == nullptr) {
// 处理查找失败
return;
}
// 检查是否发生异常
if (env->ExceptionCheck()) {
// 转换异常类型
jthrowable ex = env->ExceptionOccurred();
env->ExceptionClear();
env->Throw(ex);
return;
}
}
七、技术选型建议
适用场景
- 计算密集型任务(如3D渲染)
- 实时性要求高的处理(如音频降噪)
- 需要复用现有C/C++库的场景
优缺点分析
优点:
- 极致性能优化空间
- 直接访问底层API
- 代码安全性更高(反编译难度大)
缺点:
- 开发调试复杂度高
- 跨平台适配成本大
- 内存管理风险增加
注意事项
- 处理好JNI引用避免内存泄漏
- 注意线程安全(AttachCurrentThread调用)
- 做好异常边界处理
- 考虑ABI兼容性问题
八、总结
掌握NDK开发就像获得了一把瑞士军刀,虽然日常开发可能用不到,但在处理性能瓶颈或复杂计算时能发挥关键作用。建议从简单的JNI调用开始,逐步深入到内存管理和多线程处理,最终实现Java与原生代码的无缝协作。记住:能力越大责任越大,原生代码的错误往往会导致更严重的崩溃,所以务必做好充分的测试和异常处理。
Comments