文章目錄
- Android高級開發第二篇 - JNI 參數傳遞與 Java → C → Java 雙向調用
- 引言
- JNI基礎回顧
- JNI中的參數傳遞
- 基本數據類型傳遞
- 字符串傳遞
- 數組傳遞
- 對象傳遞
- Java → C → Java 雙向調用
- 從C/C++調用Java方法
- 實現一個完整的回調機制
- 內存管理與注意事項
- 性能優化提示
- 結論
- 參考資源
Android高級開發第二篇 - JNI 參數傳遞與 Java → C → Java 雙向調用
引言
在Android開發中,JNI (Java Native Interface) 是連接Java代碼和本地C/C++代碼的橋梁。通過JNI,我們可以利用C/C++的高性能特性來處理計算密集型任務,同時保持Java的跨平臺優勢。本文將深入探討JNI參數傳遞機制以及Java和C之間的雙向調用實現。
JNI基礎回顧
在深入參數傳遞之前,讓我們先回顧JNI的基本概念:
- JNI: Java本地接口,允許Java代碼調用C/C++等本地語言編寫的函數
- JNIEnv: 提供大多數JNI函數的接口指針
- jobject: 表示Java對象的引用
- jclass: 表示Java類的引用
JNI中的參數傳遞
基本數據類型傳遞
JNI提供了一系列與Java基本數據類型對應的C數據類型:
Java類型 | JNI類型 | C/C++類型 |
---|---|---|
boolean | jboolean | unsigned char |
byte | jbyte | signed char |
char | jchar | unsigned short |
short | jshort | short |
int | jint | int |
long | jlong | long long |
float | jfloat | float |
double | jdouble | double |
示例代碼:
// Java代碼
public native int calculateSum(int a, int b);
// C代碼
JNIEXPORT jint JNICALL
Java_com_example_MyClass_calculateSum(JNIEnv *env, jobject thiz, jint a, jint b) {return a + b;
}
字符串傳遞
字符串是最常見的復雜參數類型之一。在JNI中,我們需要在Java的String和C的字符數組之間進行轉換:
// Java代碼
public native String reverseString(String input);
// C代碼
JNIEXPORT jstring JNICALL
Java_com_example_MyClass_reverseString(JNIEnv *env, jobject thiz, jstring input) {// 將Java字符串轉換為C字符串const char* str = (*env)->GetStringUTFChars(env, input, NULL);// 處理字符串(例如反轉)int len = strlen(str);char* reversed = malloc(len + 1);for (int i = 0; i < len; i++) {reversed[i] = str[len - i - 1];}reversed[len] = '\0';// 釋放資源(*env)->ReleaseStringUTFChars(env, input, str);// 將C字符串轉換回Java字符串jstring result = (*env)->NewStringUTF(env, reversed);free(reversed);return result;
}
數組傳遞
JNI提供了訪問和修改Java數組的方法:
// Java代碼
public native void processIntArray(int[] array);
// C代碼
JNIEXPORT void JNICALL
Java_com_example_MyClass_processIntArray(JNIEnv *env, jobject thiz, jintArray array) {// 獲取數組長度jsize length = (*env)->GetArrayLength(env, array);// 獲取數組元素jint* elements = (*env)->GetIntArrayElements(env, array, NULL);// 處理數組for (int i = 0; i < length; i++) {elements[i] *= 2; // 每個元素乘以2}// 更新Java數組并釋放資源(*env)->ReleaseIntArrayElements(env, array, elements, 0);
}
對象傳遞
在JNI中傳遞Java對象需要使用反射機制:
// Java類
public class Person {private String name;private int age;// getter和setter方法public String getName() { return name; }public void setName(String name) { this.name = name; }public int getAge() { return age; }public void setAge(int age) { this.age = age; }
}// Java接口
public native void updatePerson(Person person);
// C代碼
JNIEXPORT void JNICALL
Java_com_example_MyClass_updatePerson(JNIEnv *env, jobject thiz, jobject person) {// 獲取Person類jclass personClass = (*env)->GetObjectClass(env, person);// 獲取setName方法IDjmethodID setNameMethod = (*env)->GetMethodID(env, personClass, "setName", "(Ljava/lang/String;)V");// 調用setName方法jstring newName = (*env)->NewStringUTF(env, "Updated from JNI");(*env)->CallVoidMethod(env, person, setNameMethod, newName);// 獲取setAge方法IDjmethodID setAgeMethod = (*env)->GetMethodID(env, personClass, "setAge", "(I)V");// 調用setAge方法(*env)->CallVoidMethod(env, person, setAgeMethod, 30);
}
Java → C → Java 雙向調用
JNI最強大的特性之一是支持雙向調用:不僅可以從Java調用C/C++代碼,還可以從C/C++回調Java方法。
從C/C++調用Java方法
// Java類
public class Callback {// 這個方法將被C代碼調用public void onProgress(int progress) {System.out.println("Progress: " + progress + "%");}// JNI方法public native void startLongTask();
}
// C代碼
JNIEXPORT void JNICALL
Java_com_example_Callback_startLongTask(JNIEnv *env, jobject thiz) {// 獲取Callback類jclass callbackClass = (*env)->GetObjectClass(env, thiz);// 獲取onProgress方法IDjmethodID onProgressMethod = (*env)->GetMethodID(env, callbackClass, "onProgress", "(I)V");// 模擬一個長時間運行的任務for (int i = 0; i <= 100; i += 10) {// 執行一些工作...// 調用Java的回調方法(*env)->CallVoidMethod(env, thiz, onProgressMethod, i);// 模擬延遲usleep(500000); // 500毫秒}
}
實現一個完整的回調機制
下面是一個更完整的例子,展示了如何實現一個回調接口:
// Java回調接口
public interface TaskCallback {void onStart();void onProgress(int progress);void onComplete(String result);
}// Java類
public class NativeTask {private TaskCallback callback;public NativeTask(TaskCallback callback) {this.callback = callback;}// JNI方法public native void executeTask();// 靜態代碼塊加載本地庫static {System.loadLibrary("nativetask");}
}
// C代碼
JNIEXPORT void JNICALL
Java_com_example_NativeTask_executeTask(JNIEnv *env, jobject thiz) {// 獲取NativeTask類jclass taskClass = (*env)->GetObjectClass(env, thiz);// 獲取callback字段IDjfieldID callbackField = (*env)->GetFieldID(env, taskClass, "callback", "Lcom/example/TaskCallback;");// 獲取callback對象jobject callback = (*env)->GetObjectField(env, thiz, callbackField);// 獲取TaskCallback接口的類引用jclass callbackClass = (*env)->FindClass(env, "com/example/TaskCallback");// 獲取接口方法IDjmethodID onStartMethod = (*env)->GetMethodID(env, callbackClass, "onStart", "()V");jmethodID onProgressMethod = (*env)->GetMethodID(env, callbackClass, "onProgress", "(I)V");jmethodID onCompleteMethod = (*env)->GetMethodID(env, callbackClass, "onComplete", "(Ljava/lang/String;)V");// 調用onStart(*env)->CallVoidMethod(env, callback, onStartMethod);// 模擬任務進度for (int i = 0; i <= 100; i += 10) {// 執行一些工作...// 調用onProgress(*env)->CallVoidMethod(env, callback, onProgressMethod, i);// 模擬延遲usleep(200000); // 200毫秒}// 調用onCompletejstring result = (*env)->NewStringUTF(env, "Task completed successfully!");(*env)->CallVoidMethod(env, callback, onCompleteMethod, result);// 釋放局部引用(*env)->DeleteLocalRef(env, result);
}
在實際應用中的使用示例:
// 使用示例
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TaskCallback callback = new TaskCallback() {@Overridepublic void onStart() {Log.d("NativeTask", "Task started");}@Overridepublic void onProgress(int progress) {Log.d("NativeTask", "Progress: " + progress + "%");// 更新UI進度條}@Overridepublic void onComplete(String result) {Log.d("NativeTask", "Task completed: " + result);// 顯示結果}};NativeTask task = new NativeTask(callback);new Thread(() -> task.executeTask()).start();}
}
內存管理與注意事項
在JNI編程中,內存管理是一個關鍵問題:
- 局部引用: 每次JNI調用返回后自動釋放,但在復雜函數中應使用
DeleteLocalRef
手動釋放 - 全局引用: 必須手動創建和釋放,使用
NewGlobalRef
和DeleteGlobalRef
- 弱全局引用: 不會阻止垃圾回收,使用
NewWeakGlobalRef
和DeleteWeakGlobalRef
// 創建全局引用示例
jobject globalCallback;JNIEXPORT void JNICALL
Java_com_example_NativeTask_initialize(JNIEnv *env, jobject thiz, jobject callback) {// 創建全局引用globalCallback = (*env)->NewGlobalRef(env, callback);
}JNIEXPORT void JNICALL
Java_com_example_NativeTask_cleanup(JNIEnv *env, jobject thiz) {// 釋放全局引用if (globalCallback != NULL) {(*env)->DeleteGlobalRef(env, globalCallback);globalCallback = NULL;}
}
性能優化提示
- 最小化JNI調用次數: 每次跨越JNI邊界都有開銷
- 批量處理數據: 一次傳遞大量數據比多次傳遞少量數據更高效
- 直接緩沖區: 使用
ByteBuffer.allocateDirect()
創建直接緩沖區,減少復制 - 保持引用: 重復使用的類和方法ID應該緩存起來
- 合理釋放資源: 及時釋放不再需要的引用和本地資源
結論
JNI參數傳遞和雙向調用是Android高級開發中的關鍵技能。掌握這些技術可以讓你充分利用Java和C/C++的各自優勢,構建高性能的Android應用。然而,JNI編程也帶來了額外的復雜性和潛在的內存管理問題,因此需要謹慎使用并遵循最佳實踐。
在實際開發中,可以考慮使用一些現代化的工具如Djinni
或SWIG
來簡化JNI開發過程,減少樣板代碼并提高開發效率。另外,Android NDK還提供了許多有用的庫和工具,幫助開發者更輕松地進行本地開發。
參考資源
- Android NDK 官方文檔
- JNI 規范
- Android JNI 提示
希望本文對你理解和應用JNI參數傳遞與雙向調用有所幫助。在下一篇文章中,我們將探討如何在JNI中處理異常和線程安全問題。