当前位置: 首页 > news >正文

Android高级开发第三篇 - JNI异常处理与线程安全编程

Android高级开发第三篇 - JNI异常处理与线程安全编程

  • Android高级开发第三篇 - JNI异常处理与线程安全编程
    • 引言
    • 为什么要关注异常处理和线程安全?
    • 第一部分:JNI异常处理基础
      • 什么是JNI异常?
      • 检查和处理Java异常
      • 从C代码抛出Java异常
      • 异常处理的最佳实践
    • 第二部分:线程安全基础
      • 什么是线程安全问题?
      • JNIEnv的线程安全性
      • 使用互斥锁保护共享资源
      • 线程安全的全局引用管理
    • 第三部分:实际应用示例
    • 调试技巧和常见错误
      • 常见错误及解决方案
      • 调试工具
    • 总结
    • 参考资源

Android高级开发第三篇 - JNI异常处理与线程安全编程

引言

在前两篇文章中,我们学习了JNI的基础知识和参数传递机制。然而,真正的生产环境中,我们必须面对两个关键挑战:异常处理和线程安全。这些看似复杂的概念其实是JNI开发中不可或缺的基础技能。本文将从新手的角度,逐步引导你理解和掌握这些重要概念。

为什么要关注异常处理和线程安全?

想象一下这样的场景:

  • 你的C代码访问了一个null指针,导致应用崩溃
  • 多个线程同时调用JNI方法,结果数据出现了不一致
  • Java代码抛出异常,但C代码没有正确处理,导致内存泄漏

这些都是JNI开发中的常见问题。掌握异常处理和线程安全,就是为你的应用程序构建一道安全防线。

第一部分:JNI异常处理基础

什么是JNI异常?

JNI异常可以分为两类:

  1. Java异常传播到C代码:Java方法抛出异常,需要在C代码中检查和处理
  2. C代码中的异常传播到Java:C代码发现错误,需要抛出Java异常

检查和处理Java异常

当你在C代码中调用Java方法时,这些方法可能会抛出异常。让我们看一个简单的例子:

// Java代码
public class Calculator {public int divide(int a, int b) {if (b == 0) {throw new ArithmeticException("Division by zero");}return a / b;}public native void testDivision(int a, int b);
}
// C代码 - 错误的处理方式
JNIEXPORT void JNICALL
Java_com_example_Calculator_testDivision(JNIEnv *env, jobject thiz, jint a, jint b) {// 获取divide方法jclass cls = (*env)->GetObjectClass(env, thiz);jmethodID methodID = (*env)->GetMethodID(env, cls, "divide", "(II)I");// 调用divide方法 - 这里可能抛出异常!jint result = (*env)->CallIntMethod(env, thiz, methodID, a, b);// 如果上面抛出异常,这里的代码可能不会正确执行printf("Result: %d\n", result);
}

正确的处理方式

// C代码 - 正确的异常处理
JNIEXPORT void JNICALL
Java_com_example_Calculator_testDivision(JNIEnv *env, jobject thiz, jint a, jint b) {jclass cls = (*env)->GetObjectClass(env, thiz);jmethodID methodID = (*env)->GetMethodID(env, cls, "divide", "(II)I");// 调用Java方法jint result = (*env)->CallIntMethod(env, thiz, methodID, a, b);// 检查是否有异常发生if ((*env)->ExceptionCheck(env)) {// 获取异常信息(可选)jthrowable exception = (*env)->ExceptionOccurred(env);// 打印异常堆栈(调试用)(*env)->ExceptionDescribe(env);// 清除异常(*env)->ExceptionClear(env);// 处理异常情况printf("An exception occurred in Java code\n");return;}// 只有在没有异常时才执行printf("Result: %d\n", result);
}

从C代码抛出Java异常

有时候,你需要在C代码中检测到错误并抛出Java异常:

// C代码 - 抛出Java异常
JNIEXPORT jstring JNICALL
Java_com_example_FileUtils_readFile(JNIEnv *env, jobject thiz, jstring filename) {// 获取文件名const char* file = (*env)->GetStringUTFChars(env, filename, NULL);// 尝试打开文件FILE* fp = fopen(file, "r");// 释放文件名字符串(*env)->ReleaseStringUTFChars(env, filename, file);if (fp == NULL) {// 文件打开失败,抛出Java异常jclass exceptionClass = (*env)->FindClass(env, "java/io/FileNotFoundException");(*env)->ThrowNew(env, exceptionClass, "Cannot open file");return NULL;}// 读取文件内容...char buffer[1024];fgets(buffer, sizeof(buffer), fp);fclose(fp);return (*env)->NewStringUTF(env, buffer);
}

异常处理的最佳实践

  1. 总是检查异常:调用Java方法后,使用ExceptionCheck()ExceptionOccurred()
  2. 及时清除异常:使用ExceptionClear()清除异常状态
  3. 资源清理:即使发生异常,也要确保资源得到正确释放
  4. 异常信息:提供有意义的异常信息,帮助调试
// 完整的异常处理示例
JNIEXPORT jbyteArray JNICALL
Java_com_example_DataProcessor_processData(JNIEnv *env, jobject thiz, jbyteArray input) {jbyte* inputBytes = NULL;jbyteArray result = NULL;// 获取输入数据inputBytes = (*env)->GetByteArrayElements(env, input, NULL);if (inputBytes == NULL) {// 内存分配失败jclass exceptionClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError");(*env)->ThrowNew(env, exceptionClass, "Failed to get array elements");goto cleanup;}jsize length = (*env)->GetArrayLength(env, input);if (length <= 0) {jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException");(*env)->ThrowNew(env, exceptionClass, "Input array is empty");goto cleanup;}// 处理数据...// 假设我们简单地复制数据result = (*env)->NewByteArray(env, length);if (result == NULL) {jclass exceptionClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError");(*env)->ThrowNew(env, exceptionClass, "Failed to create result array");goto cleanup;}(*env)->SetByteArrayRegion(env, result, 0, length, inputBytes);cleanup:// 清理资源if (inputBytes != NULL) {(*env)->ReleaseByteArrayElements(env, input, inputBytes, JNI_ABORT);}return result;
}

第二部分:线程安全基础

什么是线程安全问题?

在多线程环境中,多个线程可能同时访问和修改相同的数据,导致数据不一致或程序崩溃。JNI中的线程安全问题主要包括:

  1. JNIEnv不是线程安全的:每个线程都有自己的JNIEnv指针
  2. 全局引用的并发访问:多个线程访问同一个全局引用
  3. 静态变量的并发修改:C代码中的静态变量被多个线程修改

JNIEnv的线程安全性

错误的做法

// 全局变量 - 这是错误的!
JNIEnv* globalEnv = NULL;JNIEXPORT void JNICALL
Java_com_example_BadExample_initEnv(JNIEnv *env, jobject thiz) {// 错误:保存JNIEnv到全局变量globalEnv = env;
}void someFunction() {// 错误:在其他线程中使用全局的JNIEnvjclass cls = (*globalEnv)->FindClass(globalEnv, "java/lang/String");// 这可能导致崩溃!
}

正确的做法

// 全局JavaVM指针是线程安全的
JavaVM* g_jvm = NULL;JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {g_jvm = vm;return JNI_VERSION_1_6;
}// 在其他线程中获取JNIEnv
void someFunction() {JNIEnv* env;int result = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6);if (result == JNI_EDETACHED) {// 当前线程没有附加到JVM,需要附加result = (*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL);if (result != JNI_OK) {// 处理错误return;}// 使用env...jclass cls = (*env)->FindClass(env, "java/lang/String");// 分离线程(*g_jvm)->DetachCurrentThread(g_jvm);} else if (result == JNI_OK) {// 线程已经附加,直接使用jclass cls = (*env)->FindClass(env, "java/lang/String");}
}

使用互斥锁保护共享资源

当多个线程需要访问共享数据时,我们需要使用同步机制:

#include <pthread.h>// 共享数据
static int sharedCounter = 0;
static pthread_mutex_t counterMutex = PTHREAD_MUTEX_INITIALIZER;JNIEXPORT jint JNICALL
Java_com_example_ThreadSafe_incrementCounter(JNIEnv *env, jobject thiz) {int result;// 获取锁pthread_mutex_lock(&counterMutex);// 修改共享数据sharedCounter++;result = sharedCounter;// 释放锁pthread_mutex_unlock(&counterMutex);return result;
}JNIEXPORT jint JNICALL
Java_com_example_ThreadSafe_getCounter(JNIEnv *env, jobject thiz) {int result;pthread_mutex_lock(&counterMutex);result = sharedCounter;pthread_mutex_unlock(&counterMutex);return result;
}

线程安全的全局引用管理

#include <pthread.h>// 线程安全的全局引用管理
static jobject g_callback = NULL;
static pthread_mutex_t g_callback_mutex = PTHREAD_MUTEX_INITIALIZER;JNIEXPORT void JNICALL
Java_com_example_ThreadSafe_setCallback(JNIEnv *env, jobject thiz, jobject callback) {pthread_mutex_lock(&g_callback_mutex);// 删除旧的全局引用if (g_callback != NULL) {(*env)->DeleteGlobalRef(env, g_callback);}// 创建新的全局引用if (callback != NULL) {g_callback = (*env)->NewGlobalRef(env, callback);} else {g_callback = NULL;}pthread_mutex_unlock(&g_callback_mutex);
}void callbackFromNativeThread() {JNIEnv* env;jobject callback;// 获取当前线程的JNIEnvif ((*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {return;}// 安全地获取回调对象pthread_mutex_lock(&g_callback_mutex);callback = g_callback;if (callback != NULL) {// 创建局部引用以防止回调对象在使用过程中被删除callback = (*env)->NewLocalRef(env, callback);}pthread_mutex_unlock(&g_callback_mutex);if (callback != NULL) {// 调用回调方法jclass cls = (*env)->GetObjectClass(env, callback);jmethodID method = (*env)->GetMethodID(env, cls, "onCallback", "()V");(*env)->CallVoidMethod(env, callback, method);// 删除局部引用(*env)->DeleteLocalRef(env, callback);}
}

第三部分:实际应用示例

让我们创建一个完整的示例,展示如何在实际项目中应用异常处理和线程安全:

// Java代码
public class SecureFileProcessor {public interface ProgressCallback {void onProgress(int percentage);void onError(String error);void onComplete(String result);}static {System.loadLibrary("securefileprocessor");}public native void processFileAsync(String filename, ProgressCallback callback);public native void cancelProcessing();
}
// C代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>// 全局变量
static JavaVM* g_jvm = NULL;
static pthread_t g_processing_thread;
static volatile int g_should_cancel = 0;
static pthread_mutex_t g_cancel_mutex = PTHREAD_MUTEX_INITIALIZER;// 线程参数结构
typedef struct {char* filename;jobject callback;
} ProcessingParams;// 线程安全的取消检查
int shouldCancel() {int result;pthread_mutex_lock(&g_cancel_mutex);result = g_should_cancel;pthread_mutex_unlock(&g_cancel_mutex);return result;
}// 调用Java回调方法
void callJavaCallback(JNIEnv* env, jobject callback, const char* methodName, const char* signature, ...) {if (callback == NULL) return;jclass cls = (*env)->GetObjectClass(env, callback);jmethodID method = (*env)->GetMethodID(env, cls, methodName, signature);if (method == NULL) {// 方法不存在,抛出异常jclass exceptionClass = (*env)->FindClass(env, "java/lang/NoSuchMethodError");(*env)->ThrowNew(env, exceptionClass, "Callback method not found");return;}va_list args;va_start(args, signature);if (strcmp(signature, "(I)V") == 0) {int value = va_arg(args, int);(*env)->CallVoidMethod(env, callback, method, value);} else if (strcmp(signature, "(Ljava/lang/String;)V") == 0) {const char* str = va_arg(args, const char*);jstring jstr = (*env)->NewStringUTF(env, str);(*env)->CallVoidMethod(env, callback, method, jstr);(*env)->DeleteLocalRef(env, jstr);}va_end(args);// 检查回调是否抛出异常if ((*env)->ExceptionCheck(env)) {(*env)->ExceptionDescribe(env);(*env)->ExceptionClear(env);}
}// 处理线程函数
void* processingThread(void* params) {ProcessingParams* p = (ProcessingParams*)params;JNIEnv* env;// 附加到JVMint result = (*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL);if (result != JNI_OK) {free(p->filename);(*g_jvm)->DeleteGlobalRef(g_jvm, p->callback);free(p);return NULL;}// 检查文件是否存在FILE* file = fopen(p->filename, "r");if (file == NULL) {callJavaCallback(env, p->callback, "onError", "(Ljava/lang/String;)V", "File not found");goto cleanup;}// 模拟文件处理for (int i = 0; i <= 100; i += 10) {if (shouldCancel()) {callJavaCallback(env, p->callback, "onError", "(Ljava/lang/String;)V", "Processing cancelled");goto cleanup;}// 报告进度callJavaCallback(env, p->callback, "onProgress", "(I)V", i);// 模拟工作usleep(100000); // 100ms}// 处理完成callJavaCallback(env, p->callback, "onComplete", "(Ljava/lang/String;)V", "File processed successfully");cleanup:if (file) fclose(file);free(p->filename);(*env)->DeleteGlobalRef(env, p->callback);free(p);// 分离线程(*g_jvm)->DetachCurrentThread(g_jvm);return NULL;
}JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {g_jvm = vm;return JNI_VERSION_1_6;
}JNIEXPORT void JNICALL
Java_com_example_SecureFileProcessor_processFileAsync(JNIEnv *env, jobject thiz, jstring filename, jobject callback) {// 参数验证if (filename == NULL || callback == NULL) {jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException");(*env)->ThrowNew(env, exceptionClass, "Filename and callback cannot be null");return;}// 准备线程参数ProcessingParams* params = malloc(sizeof(ProcessingParams));if (params == NULL) {jclass exceptionClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError");(*env)->ThrowNew(env, exceptionClass, "Failed to allocate memory");return;}// 复制文件名const char* file = (*env)->GetStringUTFChars(env, filename, NULL);params->filename = malloc(strlen(file) + 1);if (params->filename == NULL) {(*env)->ReleaseStringUTFChars(env, filename, file);free(params);jclass exceptionClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError");(*env)->ThrowNew(env, exceptionClass, "Failed to allocate memory for filename");return;}strcpy(params->filename, file);(*env)->ReleaseStringUTFChars(env, filename, file);// 创建回调的全局引用params->callback = (*env)->NewGlobalRef(env, callback);// 重置取消标志pthread_mutex_lock(&g_cancel_mutex);g_should_cancel = 0;pthread_mutex_unlock(&g_cancel_mutex);// 创建处理线程int result = pthread_create(&g_processing_thread, NULL, processingThread, params);if (result != 0) {free(params->filename);(*env)->DeleteGlobalRef(env, params->callback);free(params);jclass exceptionClass = (*env)->FindClass(env, "java/lang/RuntimeException");(*env)->ThrowNew(env, exceptionClass, "Failed to create processing thread");}
}JNIEXPORT void JNICALL
Java_com_example_SecureFileProcessor_cancelProcessing(JNIEnv *env, jobject thiz) {pthread_mutex_lock(&g_cancel_mutex);g_should_cancel = 1;pthread_mutex_unlock(&g_cancel_mutex);
}

调试技巧和常见错误

常见错误及解决方案

  1. 忘记检查异常

    // 错误
    (*env)->CallVoidMethod(env, obj, method);
    // 继续执行...// 正确
    (*env)->CallVoidMethod(env, obj, method);
    if ((*env)->ExceptionCheck(env)) {(*env)->ExceptionClear(env);return;
    }
    
  2. 在错误的线程中使用JNIEnv

    // 错误:直接使用其他线程的JNIEnv// 正确:获取当前线程的JNIEnv
    JNIEnv* env;
    (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6);
    
  3. 没有正确管理全局引用

    // 错误:创建了全局引用但没有删除
    jobject globalRef = (*env)->NewGlobalRef(env, obj);// 正确:记得删除全局引用
    (*env)->DeleteGlobalRef(env, globalRef);
    

调试工具

  1. 使用CheckJNI:在开发阶段启用CheckJNI检查
  2. AddressSanitizer:检测内存错误
  3. 日志记录:在关键位置添加日志
  4. 异常堆栈:使用ExceptionDescribe()打印异常信息

总结

异常处理和线程安全是JNI开发中的核心技能。记住以下要点:

异常处理

  • 总是检查Java方法调用后的异常状态
  • 在C代码中适当地抛出Java异常
  • 确保异常情况下的资源清理

线程安全

  • JNIEnv不能跨线程使用
  • 使用JavaVM获取当前线程的JNIEnv
  • 保护共享资源访问
  • 正确管理全局引用的生命周期

虽然这些概念初看起来可能复杂,但通过实践和遵循最佳实践,你会发现它们是构建稳定JNI应用的基石。在下一篇文章中,我们将探讨JNI性能优化技巧和高级调试方法。

参考资源

  • JNI异常处理指南
  • Android NDK线程安全
  • pthread编程指南

相关文章:

  • 用 Whisper 打破沉默:AI 语音技术如何重塑无障碍沟通方式?
  • HTTP、WebSocket、SSE 对比
  • CNN卷积网络:让计算机拥有“火眼金睛“(superior哥AI系列第4期)
  • 打卡day43
  • 秋招Day12 - 计算机网络 - UDP
  • 05.MySQL表的约束
  • 如何区分虚拟货币诈骗与经营失败?
  • STM32G4 电机外设篇(四)DAC输出电流波形 + CAN通讯
  • Vue-3-前端框架Vue基础入门之VSCode开发环境配置和Tomcat部署Vue项目
  • paoxiaomo的XCPC算法竞赛训练经验
  • C++中实现随机数(超详细!​​​​​)
  • 黑马程序员C++核心编程笔记--4 类和对象--多态
  • 1.文件操作相关的库
  • Java Netty 中处理粘包和半包问题的解决方案 | TCP消息完整性校验(XOR )
  • 基于GPT-SoVITS-v4-TTS的音频文本推理,流式生成
  • SOC-ESP32S3部分:25-HTTP请求
  • 移动AI神器GPT Mobile:多模型自由切换
  • 基于SpringBoot运动会管理系统设计和实现(源码+文档+部署讲解)
  • 抛砖引玉:RadarDet4D,NuScenes数据集Radar模态目标检测第二名(即将开源)
  • 道路目标检测和分类数据集
  • 时时彩做网站/深圳最新消息
  • 厦门知名做企业网站设计的公司/免费建站的网站哪个好
  • 网站推广怎么做优化/谷歌seo是什么意思
  • 曲靖网站网站建设/刷粉网站推广
  • 电商网站系统/焊工培训内容有哪些
  • wordpress游戏站/网络推广赚钱项目