• Android NDK开发(五)--C代码回调Java代码【转】


    转载请注明出处: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方法

    [java] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. package com.example.ndkcallback;  
    2.   
    3. public class DataProvider {  
    4.     /** 
    5.      * C调用java空方法 
    6.      */  
    7.     public void nullMethod() {  
    8.         System.out.println("hello from java");  
    9.     }  
    10.     /** 
    11.      * C调用java中的带两个int参数的方法 
    12.      *  
    13.      * @param x 
    14.      * @param y 
    15.      * @return 
    16.      */  
    17.     public int Add(int x, int y) {  
    18.         int result = x + y;  
    19.         System.out.println("result in java " + result);  
    20.         return result;  
    21.     }  
    22.     /** 
    23.      * C调用java中参数为String的方法 
    24.      *  
    25.      * @param s 
    26.      */  
    27.     public void printString(String s) {  
    28.         System.out.println("java " + s);  
    29.     }  
    30.   
    31.     // 本地方法  
    32.     public native void callMethod1();  
    33.     public native void callMethod2();  
    34.     public native void callMethod3();  
    35.   
    36. }  

    编译头文件

            在DOS命令行下,切换到工程目录所在的源码存放的src目录下,使用javah命令编译C语言的函数签名。而且得注意的是,由于我使用的JDK 是1.7版本的,所以必须得切换到工程目录/src目录下执行javah,如果大家使用的是JDK 1.6或者JDK 1.5,那就切换到工程目录/classes目录,执行javah命令。

    注意:使用javah命令时,需要指定-encoding utf-8 参数,防止编译报乱码错误,下面是编译好的头文件:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. /* DO NOT EDIT THIS FILE - it is machine generated */  
    2. #include <jni.h>  
    3. /* Header for class com_example_ndkcallback_DataProvider */  
    4.   
    5. #ifndef _Included_com_example_ndkcallback_DataProvider  
    6. #define _Included_com_example_ndkcallback_DataProvider  
    7. #ifdef __cplusplus  
    8. extern "C" {  
    9. #endif  
    10. /* 
    11.  * Class:     com_example_ndkcallback_DataProvider 
    12.  * Method:    callMethod1 
    13.  * Signature: ()V 
    14.  */  
    15. JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod1  
    16. (JNIEnv *, jobject);  
    17.   
    18. /* 
    19.  * Class:     com_example_ndkcallback_DataProvider 
    20.  * Method:    callMethod2 
    21.  * Signature: ()V 
    22.  */  
    23. JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod2  
    24. (JNIEnv *, jobject);  
    25.   
    26. /* 
    27.  * Class:     com_example_ndkcallback_DataProvider 
    28.  * Method:    callMethod3 
    29.  * Signature: ()V 
    30.  */  
    31. JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod3  
    32. (JNIEnv *, jobject);  
    33.   
    34. #ifdef __cplusplus  
    35. }  
    36. #endif  
    37. #endif  

    编写C代码

            有了上面的头文件,接下来就是最不好搞的C代码了,按照套路来,首先把上面编译好的头文件剪切到jni目录下,在该目录下新建一个Hello.c的C代码文件,将刚引入的头文件的函数签名拷贝到Hello.c中使用,然后就是首先引入LOG日志头文件,定义LOG日志输入,再然后就是编译C代码,如下:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. #include<stdio.h>  
    2. #include<jni.h>  
    3. #include"com_example_ndkcallback_DataProvider.h"  
    4. #include<android/log.h>  
    5. #define LOG_TAG "System.out.c"  
    6. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)  
    7. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)  
    8.   
    9. JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod1  
    10. (JNIEnv * env, jobject obj){  
    11.     //在C语言中调用Java的空方法  
    12.     //1.找到java代码native方法所在的字节码文件  
    13.     //jclass (*FindClass)(JNIEnv*, const char*);  
    14.     jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");  
    15.     if(clazz == 0){  
    16.         LOGD("find class error");  
    17.         return;  
    18.     }  
    19.     LOGD("find class");  
    20.     //2.找到class里面对应的方法  
    21.     // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);  
    22.     jmethodID method1 = (*env)->GetMethodID(env,clazz,"nullMethod","()V");  
    23.     if(method1 == 0){  
    24.         LOGD("find method1 error");  
    25.         return;  
    26.     }  
    27.     LOGD("find method1");  
    28.     //3.调用方法  
    29.     //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);  
    30.     (*env)->CallVoidMethod(env, obj, method1);  
    31.     LOGD("method1 called");  
    32. }  
    33.   
    34. JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod2  
    35.     (JNIEnv * env, jobject obj) {  
    36.     //1.找到java代码native方法所在的字节码文件  
    37.     //jclass (*FindClass)(JNIEnv*, const char*);  
    38.     jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");  
    39.     if(clazz == 0){  
    40.         LOGD("find class error");  
    41.         return;  
    42.     }  
    43.     LOGD("find class");  
    44.     //2.找到class里面对应的方法  
    45.     // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);  
    46.     jmethodID method2 = (*env)->GetMethodID(env,clazz,"Add","(II)I");  
    47.     if(method2 == 0){  
    48.         LOGD("find method2 error");  
    49.         return;  
    50.     }  
    51.     LOGD("find method2");  
    52.     //3.调用方法  
    53.     //jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);  
    54.     int result = (*env)->CallIntMethod(env, obj, method2, 3,5);  
    55.     LOGD("result in C = %d", result);  
    56. }  
    57.   
    58. JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod3  
    59. (JNIEnv * env, jobject obj) {  
    60.     //1.找到java代码native方法所在的字节码文件  
    61.     //jclass (*FindClass)(JNIEnv*, const char*);  
    62.     jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");  
    63.     if(clazz == 0){  
    64.         LOGD("find class error");  
    65.         return;  
    66.     }  
    67.     LOGD("find class");  
    68.     //2.找到class里面对应的方法  
    69.     // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);  
    70.     jmethodID method3 = (*env)->GetMethodID(env,clazz,"printString","(Ljava/lang/String;)V");  
    71.     if(method3 == 0){  
    72.         LOGD("find method3 error");  
    73.         return;  
    74.     }  
    75.     LOGD("find method3");  
    76.     //3.调用方法  
    77.     //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);  
    78.     (*env)->CallVoidMethod(env, obj, method3,(*env)->NewStringUTF(env,"haha in C ."));  
    79.     LOGD("method3 called");  
    80. }  

    注意:编写C代码时大致需要如下3个重要的步骤:

    1.找到java代码native方法所在的字节码文件,在jni.h中的JNINativeInterface中可以找到

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. jclass (*FindClass)(JNIEnv*, const char*);  

         其中第1个参数是JNINativeInterface的指针env,第2个参数是java方法所在的类全路径名,路径之间用“/”来区分,不可以使用“.”

    2.找到class里面对应的方法,在jni.h中的JNINativeInterface中可以找到

    获取非静态方法id:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);  

    获取静态方法id:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);  

          其中第1个参数是JNINativeInterface的指针env,第2个参数是java字节码文件,第3个参数是java中的方法名,第四个参数是java中对应方法的签名。

    3.调用方法

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);  
    2. jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);  
    3. jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);  
    4. jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);  
    5. jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);  
    6. jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);  
    7. jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);  
    8. jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);  
    9. jfloat      (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...) __NDK_FPABI__;  
    10. 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配置

    [javascript] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. LOCAL_PATH := $(call my-dir)  
    2.   
    3. include $(CLEAR_VARS)  
    4.   
    5. LOCAL_MODULE    := Hello  
    6.   
    7. LOCAL_SRC_FILES := Hello.c  
    8.   
    9. LOCAL_LDLIBS += -llog  
    10.   
    11. include $(BUILD_SHARED_LIBRARY)  
    [javascript] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. APP_PLATFORM := android-8  

    编译C代码

    首先在cygwin中切换到当前工程目录下,执行“ndk-build clean”和“ndk-build”命令

    在Java中调用Nattive方法

    [java] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. public class MainActivity extends Activity implements OnClickListener {  
    2.   
    3.     static {  
    4.         // 加载动态库.so  
    5.         System.loadLibrary("Hello");  
    6.     }  
    7.     private Button btn1, btn2, btn3;  
    8.     private DataProvider provider;  
    9.     @Override  
    10.     protected void onCreate(Bundle savedInstanceState) {  
    11.         super.onCreate(savedInstanceState);  
    12.         setContentView(R.layout.activity_main);  
    13.         btn1 = (Button) findViewById(R.id.btn1);  
    14.         btn2 = (Button) findViewById(R.id.btn2);  
    15.         btn3 = (Button) findViewById(R.id.btn3);  
    16.         btn1.setOnClickListener(this);  
    17.         btn2.setOnClickListener(this);  
    18.         btn3.setOnClickListener(this);  
    19.         provider = new DataProvider();  
    20.     }  
    21.     @Override  
    22.     public void onClick(View v) {  
    23.         switch (v.getId()) {  
    24.             case R.id.btn1 : // c回调java中的空方法  
    25.                 provider.callMethod1();  
    26.                 break;  
    27.             case R.id.btn2 :// c回调java带2个int参数的方法  
    28.                 provider.callMethod2();  
    29.                 break;  
    30.             case R.id.btn3 :// c回调java带string参数的方法  
    31.                 provider.callMethod3();  
    32.                 break;  
    33.             default :  
    34.                 break;  
    35.         }  
    36.     }  
    37.   
    38. }  

    测试

    注意:以下测试的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中,我们定义这样的方法:

    [java] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. private native void callMethod4();  

    切换到这个src目录下javah获取函数签名,将得到的签名头文件拷贝到jni目录下,在C文件中引用这个头文件,编写相应的C代码:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. JNIEXPORT void JNICALL Java_com_example_ndkcallback_MainActivity_callMethod4  
    2.   (JNIEnv * env, jobject obj){  
    3.     //1.找到java代码native方法所在的字节码文件  
    4.         //jclass (*FindClass)(JNIEnv*, const char*);  
    5.         jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");  
    6.         if(clazz == 0){  
    7.             LOGD("find class error");  
    8.             return;  
    9.         }  
    10.         LOGD("find class");  
    11.         //2.找到class里面对应的方法  
    12.         // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);  
    13.         jmethodID method4 = (*env)->GetMethodID(env,clazz,"nullMethod","()V");  
    14.         if(method4 == 0){  
    15.             LOGD("find method4 error");  
    16.             return;  
    17.         }  
    18.         LOGD("find method4");  
    19.         //3.调用方法  
    20.         //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);  
    21.         (*env)->CallVoidMethod(env, obj, method4);  
    22.         LOGD("method4 called");  
    23. }  

    编译运行之后,报错了

           实际运行的时候,程序直接崩溃了,查看日志发现,字节码class找到了,方法method找到了,但是就是没有执行method方法,显然是执行method方法这行代码出了Bug,以下是调用method方法执行的代码:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. (*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找到对应的对象:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. jobject     (*AllocObject)(JNIEnv*, jclass);  

           这个方法第1个参数是JNINativeInterface,第2个参数是jclass,返回值jobject。我们就拿这个方法获取jobject,传给CallVoidMethod:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. JNIEXPORT void JNICALL Java_com_example_ndkcallback_MainActivity_callMethod4  
    2.   (JNIEnv * env, jobject obj){  
    3.     //1.找到java代码native方法所在的字节码文件  
    4.         //jclass (*FindClass)(JNIEnv*, const char*);  
    5.         jclass clazz = (*env)->FindClass(env, "com/example/ndkcallback/DataProvider");  
    6.         if(clazz == 0){  
    7.             LOGD("find class error");  
    8.             return;  
    9.         }  
    10.         LOGD("find class");  
    11.         //2.找到class里面对应的方法  
    12.         // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);  
    13.         jmethodID method4 = (*env)->GetMethodID(env,clazz,"nullMethod","()V");  
    14.         if(method4 == 0){  
    15.             LOGD("find method4 error");  
    16.             return;  
    17.         }  
    18.         LOGD("find method4");  
    19.         //3.通过jclass获取jobject  
    20.         //jobject     (*AllocObject)(JNIEnv*, jclass);  
    21.         jobject jobj = (*env)->AllocObject(env, clazz);  
    22.         if(jobj == 0){  
    23.             LOGD("find jobj error");  
    24.             return;  
    25.         }  
    26.         LOGD("find jobj");  
    27.         //4.调用方法  
    28.         //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);  
    29.         (*env)->CallVoidMethod(env, jobj, method4);  
    30.         LOGD("method4 called");  
    31. }  

          写完代码之后,重新编译C代码文件,Refresh和clean一下工程,运行后:

          说明native方法callMethod4已经运行成功了。

  • 相关阅读:
    性能测试
    怎样开始用selenium进行自动化测试
    手机自动化测试的原理
    黑盒测试与白盒测试的区别
    白盒测试方法
    黑盒测试概念及设计方法
    接口测试的概念及常用方法
    运用c语言和Java写九九乘法表
    appium键值对的应用
    压力测试和负载测试的区别
  • 原文地址:https://www.cnblogs.com/zzb-Dream-90Time/p/6070332.html
Copyright © 2020-2023  润新知