Android NDK開發(五)——C代碼回調Java代碼

來源:互聯網
上載者:User

標籤: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代碼

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.