深入浅出 Android JNI (四):异常处理与多线程

6 阅读6分钟

1. 异常处理:跨越语言的错误边界

1.1 Java 与 Native 异常机制对比

特性Java/Kotlin 异常机制C/C++ 错误处理方式
错误传递异常抛出-捕获 (throw-catch)错误码返回 (errno) 或 C++ 异常
处理强制受检异常必须处理完全依赖开发者自觉
中断流程自动跳转到 catch 块需手动检查错误码
内存安全自动调用 finally 块释放资源需手动释放资源 (RAII 是最佳实践)

1.2 JNI 异常处理三原则

  1. 检查异常:在可能抛出异常的 JNI 调用后必须检查
  2. 清理资源:异常发生时确保释放已分配的资源
  3. 明确路径:决定是处理异常还是传播回 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);
}

关键点解析:

  1. 资源释放:在任何提前返回路径(包括异常)都必须释放已获取的资源
  2. 异常检查:在调用可能抛出异常的 JNI 函数后进行检查
  3. 异常传播ThrowNew 后立即返回,让异常传播到 Java 层
  4. 本地引用管理:及时删除不再需要的本地引用

2. 多线程挑战:JNIEnv 的线程束缚

2.1 JNIEnv 的线程规则

  1. 主线程:自动拥有有效的 JNIEnv*
  2. Java 创建的线程:自动附加到 JVM,拥有 JNIEnv*
  3. 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);
}

关键设计:

  1. 全局引用:跨线程使用的 jobject 必须转为全局引用
  2. 方法ID缓存:提前获取并缓存方法 ID(方法 ID 在类生命周期内有效)
  3. 线程安全:使用 C++ 异常捕获 Native 错误并转为 Java 回调
  4. 资源清理:确保在所有路径释放全局引用和分离线程

4. 最佳实践与陷阱规避

4.1 异常处理黄金法则

  1. 检查所有可疑调用:特别是创建对象、分配内存的 JNI 函数
  2. 先清理后返回:在异常返回路径先释放资源再传播异常
  3. 避免嵌套异常:处理异常时不要再触发新异常
  4. 日志辅助:在关键路径添加日志帮助诊断

4.2 多线程安全准则

  1. JNIEnv 线程限制:绝不跨线程共享 JNIEnv

  2. 全局引用管理:全局引用是线程安全的,但要确保正确释放

  3. 同步策略

    • 短期锁定用 Java 同步
    • 高性能场景用 Native 同步原语
  4. 线程生命周期:确保每个附加的线程最终都会分离

4.3 常见陷阱解决方案

陷阱 1JNIEnv* 在线程间共享
症状:随机崩溃或错误
解决:每个线程独立获取自己的 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 边界会终止应用