標籤:jni java c android ndk javap
轉載請註明出處:http://blog.csdn.net/allen315410/article/details/41862479
在上篇部落格裡瞭解了Java層是怎樣傳遞資料到C層代碼,並且熟悉了大部分的實際開發知識,基本上掌握這些就可以做一個基本的NDK開發了,但是光是瞭解Java回調C層的資料是不是還不夠啊,考慮問題要考慮可逆性,Java能回調C,那麼C能否反過來回調Java呢?答案是肯定可以的,這篇部落格就介紹一個C語言如何調用Java層的代碼。以下是一些問題情境,我們帶著這個問題情境來分析一下實現的過程。
情境1:開發中C語言層完成了一系列操作後,需要通知Java層代碼此時需要做什麼操作。
情境2:大家知道程式員都是比較懶惰的,Java代碼中封裝了大量的方法,C程式員不想重複寫複雜的邏輯,這時想通過C語言回調使用Java層代碼中的方法。
好,帶著上面的情境,我們下面建立一個小的Demo來嘗試解決這些業務情境的問題。
建立工程,在工程裡面定義Java方法和Native方法
package com.example.ndkcallback;public class DataProvider {/** * C調用java空方法 */public void nullMethod() {System.out.println("hello from java");}/** * C調用java中的帶兩個int參數的方法 * * @param x * @param y * @return */public int Add(int x, int y) {int result = x + y;System.out.println("result in java " + result);return result;}/** * C調用java中參數為String的方法 * * @param s */public void printString(String s) {System.out.println("java " + s);}// 本地方法public native void callMethod1();public native void callMethod2();public native void callMethod3();}
編譯標頭檔
在DOS命令列下,切換到工程目錄所在的源碼存放的src目錄下,使用javah命令編譯C語言的函數簽名。而且得注意的是,由於我使用的JDK 是1.7版本的,所以必須得切換到工程目錄/src目錄下執行javah,如果大家使用的是JDK 1.6或者JDK 1.5,那就切換到工程目錄/classes目錄,執行javah命令。
注意:使用javah命令時,需要指定-encoding utf-8 參數,防止編譯報亂碼錯誤,下面是編譯好的標頭檔:
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_ndkcallback_DataProvider */#ifndef _Included_com_example_ndkcallback_DataProvider#define _Included_com_example_ndkcallback_DataProvider#ifdef __cplusplusextern "C" {#endif/* * Class: com_example_ndkcallback_DataProvider * Method: callMethod1 * Signature: ()V */JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod1(JNIEnv *, jobject);/* * Class: com_example_ndkcallback_DataProvider * Method: callMethod2 * Signature: ()V */JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod2(JNIEnv *, jobject);/* * Class: com_example_ndkcallback_DataProvider * Method: callMethod3 * Signature: ()V */JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod3(JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif編寫C代碼
有了上面的標頭檔,接下來就是最不好搞的C代碼了,按照套路來,首先把上面編譯好的標頭檔剪下到jni目錄下,在該目錄下建立一個Hello.c的C代碼檔案,將剛引入的標頭檔的函數簽名拷貝到Hello.c中使用,然後就是首先引入LOG日誌標頭檔,定義LOG日誌輸入,再然後就是編譯C代碼,如下:
#include<stdio.h>#include<jni.h>#include"com_example_ndkcallback_DataProvider.h"#include<android/log.h>#define LOG_TAG "System.out.c"#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod1(JNIEnv * env, jobject obj){//在C語言中調用Java的空方法//1.找到java代碼native方法所在的位元組碼檔案//jclass (*FindClass)(JNIEnv*, const char*);jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");if(clazz == 0){LOGD("find class error");return;}LOGD("find class");//2.找到class裡面對應的方法// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);jmethodID method1 = (*env)->GetMethodID(env,clazz,"nullMethod","()V");if(method1 == 0){LOGD("find method1 error");return;}LOGD("find method1");//3.調用方法//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);(*env)->CallVoidMethod(env, obj, method1);LOGD("method1 called");}JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod2(JNIEnv * env, jobject obj) {//1.找到java代碼native方法所在的位元組碼檔案//jclass (*FindClass)(JNIEnv*, const char*);jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");if(clazz == 0){LOGD("find class error");return;}LOGD("find class");//2.找到class裡面對應的方法// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);jmethodID method2 = (*env)->GetMethodID(env,clazz,"Add","(II)I");if(method2 == 0){LOGD("find method2 error");return;}LOGD("find method2");//3.調用方法//jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);int result = (*env)->CallIntMethod(env, obj, method2, 3,5);LOGD("result in C = %d", result);}JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod3(JNIEnv * env, jobject obj) {//1.找到java代碼native方法所在的位元組碼檔案//jclass (*FindClass)(JNIEnv*, const char*);jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");if(clazz == 0){LOGD("find class error");return;}LOGD("find class");//2.找到class裡面對應的方法// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);jmethodID method3 = (*env)->GetMethodID(env,clazz,"printString","(Ljava/lang/String;)V");if(method3 == 0){LOGD("find method3 error");return;}LOGD("find method3");//3.調用方法//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);(*env)->CallVoidMethod(env, obj, method3,(*env)->NewStringUTF(env,"haha in C ."));LOGD("method3 called");}注意:編寫C代碼時大致需要如下3個重要的步驟:
1.找到java代碼native方法所在的位元組碼檔案,在jni.h中的JNINativeInterface中可以找到
jclass (*FindClass)(JNIEnv*, const char*);
其中第1個參數是JNINativeInterface的指標env,第2個參數是java方法所在的類全路徑名,路徑之間用“/”來區分,不可以使用“.”
2.找到class裡面對應的方法,在jni.h中的JNINativeInterface中可以找到
擷取非靜態方法id:
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
擷取靜態方法id:
jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
其中第1個參數是JNINativeInterface的指標env,第2個參數是java位元組碼檔案,第3個參數是java中的方法名,第四個參數是java中對應方法的簽名。
3.調用方法
void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);jboolean (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);jbyte (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);jchar (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);jshort (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);jlong (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);jfloat (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...) __NDK_FPABI__;jdouble (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...) __NDK_FPABI__;
其中第1個參數是JNINativeInterface的指標env,第2個參數是java對象obj,第3個參數是找到的對應java中的方法,第4個參數是方法接收的參數。這裡列出的是常用的方法,jni.h裡的JNINativeInterface提供了大量的方法形式用來回調java中的方法,想瞭解的請參考jni.h這個檔案。
使用javap命令查看方法簽名
JDK為我們提供了這樣的一個工具,該工具可以從java位元組碼檔案中查看方法的本地簽名,這個工具就是javap,使用前,先在CMD的dos命令列中,把路徑切換到工程中的java位元組碼檔案所在的目錄下。
命令格式:javap -s 包名.方法所在的Java類名
的那樣,黃色標註的是方法名,是(*GetMethodID)(JNIEnv*, jclass, const char*, const char*)中的第3個參數,紅色標註的是方法簽名,是其第4個參數。
Android.mk配置和Application.mk配置
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := Hello LOCAL_SRC_FILES := Hello.c LOCAL_LDLIBS += -llog include $(BUILD_SHARED_LIBRARY)
APP_PLATFORM := android-8
編譯C代碼
首先在cygwin中切換到當前工程目錄下,執行“ndk-build clean”和“ndk-build”命令
在Java中調用Nattive方法
public class MainActivity extends Activity implements OnClickListener {static {// 載入動態庫.soSystem.loadLibrary("Hello");}private Button btn1, btn2, btn3;private DataProvider provider;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);btn1 = (Button) findViewById(R.id.btn1);btn2 = (Button) findViewById(R.id.btn2);btn3 = (Button) findViewById(R.id.btn3);btn1.setOnClickListener(this);btn2.setOnClickListener(this);btn3.setOnClickListener(this);provider = new DataProvider();}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btn1 : // c回調java中的空方法provider.callMethod1();break;case R.id.btn2 :// c回調java帶2個int參數的方法provider.callMethod2();break;case R.id.btn3 :// c回調java帶string參數的方法provider.callMethod3();break;default :break;}}}測試
注意:以下測試的LOG中,綠色代表Java產生的LOG,藍色代表C產生的LOG。
測試1:c回調java中的空方法
測試2:c回調java帶2個int參數的方法
測試3:c回調java帶string參數的方法
另外:native代碼與調用的java代碼不在同一個類裡
上述建立的Android工程中,native代碼和調用的java代碼是放在同一個DataProvider類中的,這樣在C代碼中調用Java代碼是非常方便的。但是,通常開發中我們不一定就這麼幹,一個項目中java檔案很多,要是在其它的java檔案中定義了native方法了,然後再去調另一個java類裡的Java方法,這種情況下會出現什麼問題呢?帶著這個疑問,我們就在MainActivity.java檔案中定義一個native方法,這個native方法又要調用DataProvider類的nullMethod方法。
在MainActivity.java中,我們定義這樣的方法:
private native void callMethod4();
切換到這個src目錄下javah擷取函數簽名,將得到的簽名標頭檔拷貝到jni目錄下,在C檔案中引用這個標頭檔,編寫相應的C代碼:
JNIEXPORT void JNICALL Java_com_example_ndkcallback_MainActivity_callMethod4 (JNIEnv * env, jobject obj){//1.找到java代碼native方法所在的位元組碼檔案//jclass (*FindClass)(JNIEnv*, const char*);jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");if(clazz == 0){LOGD("find class error");return;}LOGD("find class");//2.找到class裡面對應的方法// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);jmethodID method4 = (*env)->GetMethodID(env,clazz,"nullMethod","()V");if(method4 == 0){LOGD("find method4 error");return;}LOGD("find method4");//3.調用方法//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);(*env)->CallVoidMethod(env, obj, method4);LOGD("method4 called");}編譯運行之後,報錯了
實際啟動並執行時候,程式直接崩潰了,查看日誌發現,位元組碼class找到了,方法method找到了,但是就是沒有執行method方法,顯然是執行method方法這行代碼出了Bug,以下是調用method方法執行的代碼:
(*env)->CallVoidMethod(env, obj, method4);
那麼這行代碼是為什麼報錯了呢?仔細觀察一下,CallVoidMethod方法的第2個參數obj,這個obj是jobject類型的,預設是java native方法所在的類的對象,就是MainActivity類的對象,但是這個native方法實際上調用的java方法存在於DataProvider類的nullMethod,調用nullMethod顯然需要使用DataProvider類的對象。反正就一句話:obj對象不正確,需要java方法對應的對象,即DataProvider。
知道問題了,就可以著手解決問題了。在jni.h的標頭檔中,JNINativeInterface提供了這樣的一個方法,協助我們通過位元組碼jclass找到對應的對象:
jobject (*AllocObject)(JNIEnv*, jclass);
這個方法第1個參數是JNINativeInterface,第2個參數是jclass,返回值jobject。我們就拿這個方法擷取jobject,傳給CallVoidMethod:
JNIEXPORT void JNICALL Java_com_example_ndkcallback_MainActivity_callMethod4 (JNIEnv * env, jobject obj){//1.找到java代碼native方法所在的位元組碼檔案//jclass (*FindClass)(JNIEnv*, const char*);jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");if(clazz == 0){LOGD("find class error");return;}LOGD("find class");//2.找到class裡面對應的方法// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);jmethodID method4 = (*env)->GetMethodID(env,clazz,"nullMethod","()V");if(method4 == 0){LOGD("find method4 error");return;}LOGD("find method4");//3.通過jclass擷取jobject//jobject (*AllocObject)(JNIEnv*, jclass);jobject jobj = (*env)->AllocObject(env, clazz);if(jobj == 0){LOGD("find jobj error");return;}LOGD("find jobj");//4.調用方法//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);(*env)->CallVoidMethod(env, jobj, method4);LOGD("method4 called");} 寫完代碼之後,重新編譯C代碼檔案,Refresh和clean一下工程,運行後:
說明native方法callMethod4已經運行成功了。
源碼請在這裡下載
Android NDK開發(五)——C代碼回調Java代碼