1. 异常处理:跨越语言的错误边界
1.1 Java 与 Native 异常机制对比
特性 | Java/Kotlin 异常机制 | C/C++ 错误处理方式 |
---|---|---|
错误传递 | 异常抛出-捕获 (throw-catch) | 错误码返回 (errno) 或 C++ 异常 |
处理强制 | 受检异常必须处理 | 完全依赖开发者自觉 |
中断流程 | 自动跳转到 catch 块 | 需手动检查错误码 |
内存安全 | 自动调用 finally 块释放资源 | 需手动释放资源 (RAII 是最佳实践) |
1.2 JNI 异常处理三原则
- 检查异常:在可能抛出异常的 JNI 调用后必须检查
- 清理资源:异常发生时确保释放已分配的资源
- 明确路径:决定是处理异常还是传播回 Java
1.3 异常检测 API
// 检查是否有异常发生 (不获取异常对象)
jboolean ExceptionCheck(JNIEnv* env);
// 获取当前异常对象 (返回 nullptr 表示无异常)
jthrowable ExceptionOccurred(JNIEnv* env);
// 清除当前异常
void ExceptionClear(JNIEnv* env);
// 抛出新异常
jint ThrowNew(JNIEnv* env, jclass clazz, const char* message);
1.4 完整异常处理流程示例
Java 类定义:
public class FileProcessor {
public native void processFile(String path);
private void onError(String message) {
Log.e("FileProcessor", message);
}
}
C++ 实现:
extern "C" JNIEXPORT void JNICALL
Java_com_example_FileProcessor_processFile(JNIEnv* env, jobject thiz, jstring path) {
const char* c_path = env->GetStringUTFChars(path, nullptr);
if (c_path == nullptr) return; // OutOfMemoryError 已抛出
FILE* file = fopen(c_path, "r");
if (file == nullptr) {
env->ReleaseStringUTFChars(path, c_path);
// 获取 Java 异常类
jclass ioexCls = env->FindClass("java/io/IOException");
if (ioexCls != nullptr) {
env->ThrowNew(ioexCls, "Failed to open file");
}
env->DeleteLocalRef(ioexCls);
return;
}
// 获取 onError 方法 ID
jclass cls = env->GetObjectClass(thiz);
jmethodID onError = env->GetMethodID(cls, "onError", "(Ljava/lang/String;)V");
env->DeleteLocalRef(cls);
char buffer[256];
while (fgets(buffer, sizeof(buffer), file) {
// 处理文件内容...
// 模拟处理错误
if (strstr(buffer, "error")) {
jstring jmsg = env->NewStringUTF("Invalid content detected");
env->CallVoidMethod(thiz, onError, jmsg);
env->DeleteLocalRef(jmsg);
// 检查 Java 回调是否抛出异常
if (env->ExceptionCheck()) {
fclose(file);
env->ReleaseStringUTFChars(path, c_path);
return; // 让异常传播回 Java
}
}
}
fclose(file);
env->ReleaseStringUTFChars(path, c_path);
}
关键点解析:
- 资源释放:在任何提前返回路径(包括异常)都必须释放已获取的资源
- 异常检查:在调用可能抛出异常的 JNI 函数后进行检查
- 异常传播:
ThrowNew
后立即返回,让异常传播到 Java 层 - 本地引用管理:及时删除不再需要的本地引用
2. 多线程挑战:JNIEnv 的线程束缚
2.1 JNIEnv 的线程规则
- 主线程:自动拥有有效的
JNIEnv*
- Java 创建的线程:自动附加到 JVM,拥有
JNIEnv*
- Native 创建的线程:必须手动附加/分离
2.2 线程生命周期管理
全局缓存 JavaVM:
// 在库加载时保存 JavaVM
static JavaVM* g_vm = nullptr;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
return JNI_VERSION_1_6;
}
线程附加/分离示例:
void* native_thread_func(void* arg) {
JNIEnv* env = nullptr;
// 附加当前线程到 JVM
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
// 处理附加失败
return nullptr;
}
// 现在可以安全使用 JNIEnv
jclass cls = env->FindClass("com/example/MyClass");
// ... 其他 JNI 操作
// 分离线程
g_vm->DetachCurrentThread();
return nullptr;
}
// 启动线程
pthread_t thread;
pthread_create(&thread, nullptr, native_thread_func, nullptr);
2.3 线程同步策略
2.3.1 Java 同步 (基于 jobject 监视器)
// 进入同步块
jint monitor_enter(JNIEnv* env, jobject obj);
// 退出同步块
jint monitor_exit(JNIEnv* env, jobject obj);
// 示例
if (env->MonitorEnter(obj) == JNI_OK) {
// 临界区代码
if (env->ExceptionCheck()) {
env->MonitorExit(obj); // 异常时也要确保退出
return;
}
env->MonitorExit(obj);
}
2.3.2 Native 同步 (更高效)
#include <mutex>
static std::mutex g_mutex;
void thread_safe_operation(JNIEnv* env) {
std::lock_guard<std::mutex> lock(g_mutex);
// 临界区代码
}
3. 实战:多线程异常处理综合示例
场景:实现一个从多线程回调 Java 的图片处理器
Java 接口:
public interface ImageProcessorCallback {
void onProgress(int percent);
void onComplete(Bitmap result);
void onError(String message);
}
Native 实现:
struct ProcessingContext {
JavaVM* vm;
jobject callback; // 全局引用
jmethodID onProgress;
jmethodID onComplete;
jmethodID onError;
};
void* process_image(void* ctx) {
ProcessingContext* context = static_cast<ProcessingContext*>(ctx);
JNIEnv* env = nullptr;
// 附加线程
if (context->vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
delete context;
return nullptr;
}
try {
for (int i = 0; i <= 100; ++i) {
// 报告进度
env->CallVoidMethod(context->callback, context->onProgress, i);
if (env->ExceptionCheck()) {
env->ExceptionClear();
break; // Java 回调要求取消
}
// 模拟处理
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// 模拟结果
jobject bitmap = create_bitmap(env); // 假设的创建位图函数
// 完成回调
env->CallVoidMethod(context->callback, context->onComplete, bitmap);
env->DeleteLocalRef(bitmap);
} catch (const std::exception& e) {
jstring msg = env->NewStringUTF(e.what());
env->CallVoidMethod(context->callback, context->onError, msg);
env->DeleteLocalRef(msg);
}
// 清理
env->DeleteGlobalRef(context->callback);
delete context;
context->vm->DetachCurrentThread();
return nullptr;
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_ImageProcessor_processAsync(JNIEnv* env, jobject thiz, jobject callback) {
// 初始化上下文
auto* ctx = new ProcessingContext();
env->GetJavaVM(&ctx->vm);
// 创建全局引用
ctx->callback = env->NewGlobalRef(callback);
// 缓存方法 ID
jclass cls = env->GetObjectClass(callback);
ctx->onProgress = env->GetMethodID(cls, "onProgress", "(I)V");
ctx->onComplete = env->GetMethodID(cls, "onComplete", "(Landroid/graphics/Bitmap;)V");
ctx->onError = env->GetMethodID(cls, "onError", "(Ljava/lang/String;)V");
env->DeleteLocalRef(cls);
// 启动线程
pthread_t thread;
pthread_create(&thread, nullptr, process_image, ctx);
pthread_detach(thread);
}
关键设计:
- 全局引用:跨线程使用的 jobject 必须转为全局引用
- 方法ID缓存:提前获取并缓存方法 ID(方法 ID 在类生命周期内有效)
- 线程安全:使用 C++ 异常捕获 Native 错误并转为 Java 回调
- 资源清理:确保在所有路径释放全局引用和分离线程
4. 最佳实践与陷阱规避
4.1 异常处理黄金法则
- 检查所有可疑调用:特别是创建对象、分配内存的 JNI 函数
- 先清理后返回:在异常返回路径先释放资源再传播异常
- 避免嵌套异常:处理异常时不要再触发新异常
- 日志辅助:在关键路径添加日志帮助诊断
4.2 多线程安全准则
-
JNIEnv 线程限制:绝不跨线程共享 JNIEnv
-
全局引用管理:全局引用是线程安全的,但要确保正确释放
-
同步策略:
- 短期锁定用 Java 同步
- 高性能场景用 Native 同步原语
-
线程生命周期:确保每个附加的线程最终都会分离
4.3 常见陷阱解决方案
陷阱 1:JNIEnv*
在线程间共享
症状:随机崩溃或错误
解决:每个线程独立获取自己的 JNIEnv*
陷阱 2:忘记释放全局引用
症状:内存泄漏,可能导致全局引用表溢出
解决:使用 RAII 封装器或严格配对的 New/DeleteGlobalRef
陷阱 3:Native 异常穿越 JNI 边界
症状:应用崩溃(SIGABRT
)
解决:所有 Native 入口点捕获 C++ 异常并转为 Java 异常
陷阱 4:回调期间抛出未处理异常
症状:后续 JNI 调用失败
解决:每次回调后检查并清除异常
5. 本章总结
异常处理核心:
- ✅ 理解 Java 与 Native 异常机制的差异
- ✅ 掌握
ExceptionCheck
/ThrowNew
等关键 API - ✅ 遵循"先清理后传播"的资源管理原则
- ✅ 将 C++ 异常转换为 Java 异常的策略
多线程要点:
- ✅
JNIEnv*
的线程关联性及获取方式 - ✅ 全局引用在多线程中的正确使用
- ✅ Java 与 Native 同步机制的选择
- ✅ 线程生命周期的严格管理
避坑指南:
- 🔸 忘记检查异常是崩溃的常见原因
- 🔸 跨线程共享
JNIEnv*
必然导致问题 - 🔸 未释放的全局引用会导致内存泄漏
- 🔸 让 C++ 异常穿越 JNI 边界会终止应用