在上篇博客里了解了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() {
- "hello from java");
- }
- /**
- * C调用java中的带两个int参数的方法
- *
- * @param x
- * @param y
- * @return
- */
- public int Add(int x, int y) {
- int result = x + y;
- "result in java " + result);
- return result;
- }
- /**
- * C调用java中参数为String的方法
- *
- * @param s
- */
- public void printString(String s) {
- "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 __cplusplus
- extern "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*);
- "com/example/ndkcallback/DataProvider");
- if(clazz == 0){
- "find class error");
- return;
- }
- "find class");
- //2.找到class里面对应的方法
- // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
- "nullMethod","()V");
- if(method1 == 0){
- "find method1 error");
- return;
- }
- "find method1");
- //3.调用方法
- //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
- (*env)->CallVoidMethod(env, obj, method1);
- "method1 called");
- }
- JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod2
- (JNIEnv * env, jobject obj) {
- //1.找到java代码native方法所在的字节码文件
- //jclass (*FindClass)(JNIEnv*, const char*);
- "com/example/ndkcallback/DataProvider");
- if(clazz == 0){
- "find class error");
- return;
- }
- "find class");
- //2.找到class里面对应的方法
- // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
- "Add","(II)I");
- if(method2 == 0){
- "find method2 error");
- return;
- }
- "find method2");
- //3.调用方法
- //jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
- int result = (*env)->CallIntMethod(env, obj, method2, 3,5);
- "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*);
- "com/example/ndkcallback/DataProvider");
- if(clazz == 0){
- "find class error");
- return;
- }
- "find class");
- //2.找到class里面对应的方法
- // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
- "printString","(Ljava/lang/String;)V");
- if(method3 == 0){
- "find method3 error");
- return;
- }
- "find method3");
- //3.调用方法
- //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
- "haha in C ."));
- "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)
[javascript] view plain copy
- APP_PLATFORM := android-8
编译C代码
首先在cygwin中切换到当前工程目录下,执行“ndk-build clean”和“ndk-build”命令
在Java中调用Nattive方法
- public class MainActivity extends Activity implements OnClickListener {
- static {
- // 加载动态库.so
- "Hello");
- }
- private Button btn1, btn2, btn3;
- private DataProvider provider;
- @Override
- protected 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);
- this);
- this);
- this);
- new DataProvider();
- }
- @Override
- public 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*);
- "com/example/ndkcallback/DataProvider");
- if(clazz == 0){
- "find class error");
- return;
- }
- "find class");
- //2.找到class里面对应的方法
- // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
- "nullMethod","()V");
- if(method4 == 0){
- "find method4 error");
- return;
- }
- "find method4");
- //3.调用方法
- //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
- (*env)->CallVoidMethod(env, obj, method4);
- "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*);
- "com/example/ndkcallback/DataProvider");
- if(clazz == 0){
- "find class error");
- return;
- }
- "find class");
- //2.找到class里面对应的方法
- // jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
- "nullMethod","()V");
- if(method4 == 0){
- "find method4 error");
- return;
- }
- "find method4");
- //3.通过jclass获取jobject
- //jobject (*AllocObject)(JNIEnv*, jclass);
- jobject jobj = (*env)->AllocObject(env, clazz);
- if(jobj == 0){
- "find jobj error");
- return;
- }
- "find jobj");
- //4.调用方法
- //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
- (*env)->CallVoidMethod(env, jobj, method4);
- "method4 called");
- }
写完代码之后,重新编译C代码文件,Refresh和clean一下工程,运行后:
说明native方法callMethod4已经运行成功了。