Android高级开发第三篇 - JNI异常处理与线程安全编程
Android高级开发第三篇 - JNI异常处理与线程安全编程
引言
在前两篇文章中,我们学习了JNI的基础知识和参数传递机制。然而,真正的生产环境中,我们必须面对两个关键挑战:异常处理和线程安全。这些看似复杂的概念其实是JNI开发中不可或缺的基础技能。本文将从新手的角度,逐步引导你理解和掌握这些重要概念。
为什么要关注异常处理和线程安全?
想象一下这样的场景:
- 你的C代码访问了一个null指针,导致应用崩溃
- 多个线程同时调用JNI方法,结果数据出现了不一致
- Java代码抛出异常,但C代码没有正确处理,导致内存泄漏
这些都是JNI开发中的常见问题。掌握异常处理和线程安全,就是为你的应用程序构建一道安全防线。
第一部分:JNI异常处理基础
什么是JNI异常?
JNI异常可以分为两类:
- Java异常传播到C代码:Java方法抛出异常,需要在C代码中检查和处理
- 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);
}
异常处理的最佳实践
- 总是检查异常:调用Java方法后,使用
ExceptionCheck()
或ExceptionOccurred()
- 及时清除异常:使用
ExceptionClear()
清除异常状态 - 资源清理:即使发生异常,也要确保资源得到正确释放
- 异常信息:提供有意义的异常信息,帮助调试
// 完整的异常处理示例
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中的线程安全问题主要包括:
- JNIEnv不是线程安全的:每个线程都有自己的JNIEnv指针
- 全局引用的并发访问:多个线程访问同一个全局引用
- 静态变量的并发修改:C代码中的静态变量被多个线程修改
JNIEnv的线程安全性
错误的做法:
// 全局变量 - 这是错误的!
JNIEnv* globalEnv = NULL;
JNIEXPORT void JNICALL
Java_com_example_BadExample_initEnv(JNIEnv *env, jobject thiz) {
// 错误:保存JNIEnv到全局变量
globalEnv = env;
}
void someFunction() {
// 错误:在其他线程中使用全局的JNIEnv
jclass 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;
// 获取当前线程的JNIEnv
if ((*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;
// 附加到JVM
int 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);
}
调试技巧和常见错误
常见错误及解决方案
忘记检查异常
// 错误 (*env)->CallVoidMethod(env, obj, method); // 继续执行... // 正确 (*env)->CallVoidMethod(env, obj, method); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); return; }
在错误的线程中使用JNIEnv
// 错误:直接使用其他线程的JNIEnv // 正确:获取当前线程的JNIEnv JNIEnv* env; (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6);
没有正确管理全局引用
// 错误:创建了全局引用但没有删除 jobject globalRef = (*env)->NewGlobalRef(env, obj); // 正确:记得删除全局引用 (*env)->DeleteGlobalRef(env, globalRef);
调试工具
- 使用CheckJNI:在开发阶段启用CheckJNI检查
- AddressSanitizer:检测内存错误
- 日志记录:在关键位置添加日志
- 异常堆栈:使用
ExceptionDescribe()
打印异常信息
总结
异常处理和线程安全是JNI开发中的核心技能。记住以下要点:
异常处理:
- 总是检查Java方法调用后的异常状态
- 在C代码中适当地抛出Java异常
- 确保异常情况下的资源清理
线程安全:
- JNIEnv不能跨线程使用
- 使用JavaVM获取当前线程的JNIEnv
- 保护共享资源访问
- 正确管理全局引用的生命周期
虽然这些概念初看起来可能复杂,但通过实践和遵循最佳实践,你会发现它们是构建稳定JNI应用的基石。在下一篇文章中,我们将探讨JNI性能优化技巧和高级调试方法。