Android进阶之路——NDK(二)

  上一篇博客介绍了NDK简介和环境的搭建以及一个简单的Demo,这篇准备总结一下JNI调用Java对象以及在JNI中开启线程。
  ps:这里说明一下,我是用Android Studio开发的,如果是用Eclipse开发的朋友,是不能直接导入我的程序,而且项目的结构和我的是有区别的。

点击下载

一、JNI实现回调

  通过JNI在Native层调用JAVA层的方法,来实现Native层向JAVA层传递消息。

我的项目结构:

Android进阶之路——NDK(二)_第1张图片

  1. 首先在java层注册native函数
public class MyJni {

    public static final String TAG = "MyNdkTest";

    /**
     * 载入动态库
     */
    static {
        System.loadLibrary("MyJni");
    }

    /**
     * 从JNI获取字符串
     *
     * @return
     */
    public static native String getStringFromNative();

    /**
     * 初始化native中创建线程需要的变量
     */
    public native void nativeInitialize();

    /**
     * 创建native中线程
     */
    public native void nativeThreadStart();

    /**
     * 停止native中线程
     */
    public native void nativeThreadStop();

    /**
     * 从native代码中回调Java的方法入口
     */
    public native void nativeCallback();

     /**
     * 从native代码中回调(非静态)
     * 在Jni中开启线程
     */
    public void onNativeThreadCallback(String str) {
        Log.i(TAG, "Thread ID = " + Thread.currentThread().getId());
        Log.i(TAG, "onNativeThreadCallback = " + str);
    }

    /**
     * 从native代码中回调Java(非静态)
     */
    public void onNativeCallback(String str) {
        Log.i(TAG, "Thread ID = " + Thread.currentThread().getId());
        Log.i(TAG, "onNativeCallback = " + str);
    }

    /**
     * 从native代码中回调(非静态)
     * 在Jni中开启线程
     */
    public static void onNativeStaticCallback(int count) {
        Log.i(TAG, "Thread ID = " + Thread.currentThread().getId());
    }

}

  如上面代码所示,Java层与JNI层的接口代码主要封装在Native类中,该类定义了五个native函数,分别是从jni层获取字符串,完成jni库的初始化,调用jni层开启线程,调用jni层关闭线程等功能。并且提供一个回调函数(一个为在开启的线程中回调,另一个是在jni开启的线程中回调),供jni层调用,并在回调函数中打印线程的Id和传递过来的字符串。这里先不讲解如何在JNI中创建线程。
  
2. jni中回调java层的函数
  这里创建头文件的方法和Android Studio下配置NDK的环境已经在前一篇叙述过,这里就不说了。在头文件中定义了这五个函数,MyJni.c是实现五个native函数的主要类:

JNIEXPORT void JNICALL
Java_com_ndk_MyJni_nativeCallback(JNIEnv *env, jobject instance) {

    onNativeCallback(env, "主线程中回调java函数");

}

  这里只贴出了在主线程回调java函数的代码,这里代码很简单,就是调用了onNativeCallback,然而这个函数实在哪里实现的呢?大家可以再看一下我的项目截图,就是在CallJava.c中实现的。代码中重要的部分我都有注释。

#include "CallJava.h"
#include "com_ndk_MyJni.h"

/**
 * C回调Java方法(非静态)
 */
void onNativeCallback(JNIEnv *env, jstring str) {

    // 获取类
    jclass gjclass = (*env)->FindClass(env, "com/ndk/MyJni");
    if (NULL == gjclass) {
        return;
    }

    // 实例化类对象
    jobject gjobject = getInstance(env, gjclass);
    if (NULL == gjobject) {
        (*env)->DeleteLocalRef(env, gjclass); // 删除类指引
        LOGI("删除类指引 !");
        return;
    }

    // 获取对象callback方法
    jmethodID callback = (*env)->GetMethodID(env, gjclass, "onNativeCallback",
                                             "(Ljava/lang/String;)V");
    if (NULL == callback) {
        (*env)->DeleteLocalRef(env, gjclass); // 删除类指引
        (*env)->DeleteLocalRef(env, gjobject); // 删除类对象指引
        LOGI("删除类对象指引 !");
        return;
    }
    // 调用非静态int方法
    (*env)->CallVoidMethod(env, gjobject, callback, (*env)->NewStringUTF(env, str));
}

/**
 * 实例化类对象
 */
jobject getInstance(JNIEnv *env, jclass clazz) {
    // 获取构造方法
    jmethodID constructor = (*env)->GetMethodID(env, clazz, "", "()V");
    if (NULL == constructor) {
        return NULL;
    }
    // 实例化类对象
    return (*env)->NewObject(env, clazz, constructor);
}

  大家可以看到,在JNI中回调Java层的函数需要四步:1、获得一个Java类的class引用(*env)->FindClass(env, “com/ndk/MyJni”),第二个参数代表这个类的相对路径。2、实例化该类,(*env)->GetMethodID(env, clazz, “”, “()V”),第二个是刚刚获得的class引用,第三个是方法的名称(这里是构造函数),最后一个就是方法的签名了(下一章节会详细介绍)。3、获取到调用该对象的方法。4、回调该方法。
  这样一个主线程的回调就完成了,这里大家在使用(*env)->GetMethodID时,注意第三个和第四个参数一定要和你java中函数名称和参数类型及个数相对应,不然会报错。
  

二、在JNI中开启子线程

  • 在java层注册native函数,与上一章节中的第一步一样,这里就不再赘述。
  • 在JNI中实现开启线程的代码,也就是MyJni.c,上一章节中只是贴出了主线程回调的一个函数,这里将剩下的四个本地方法都贴出来。
#include "com_ndk_MyJni.h"

JNIEXPORT jstring JNICALL
Java_com_ndk_MyJni_getStringFromNative(JNIEnv *env, jclass type) {

    return (*env)->NewStringUTF(env, "I am from native");
}

/*
 * Class:     com_ticktick_jnicallback_Native
 * Method:    设置全局变量
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeInitialize(JNIEnv *env,
                                                           jobject thiz) {

    //注意,直接通过定义全局的JNIEnv和jobject变量,在此保存env和thiz的值是不可以在线程中使用的
    //线程不允许共用env环境变量,但是JavaVM指针是整个jvm共用的,所以可以通过下面的方法保存JavaVM指针,在线程中使用
    (*env)->GetJavaVM(env, &gJavaVM);

    //同理,jobject变量也不允许在线程中共用,因此需要创建全局的jobject对象在线程中访问该对象
    gJavaObj = (*env)->NewGlobalRef(env, thiz);
}
static void *native_thread_exec(void *arg) {

    JNIEnv *env;

    //从全局的JavaVM中获取到环境变量
    (*gJavaVM)->AttachCurrentThread(gJavaVM, &env, NULL);

    //获取Java层对应的类
    jclass javaClass = (*env)->GetObjectClass(env, gJavaObj);
    if (javaClass == NULL) {
        LOGI("Fail to find javaClass");
        return 0;
    }

    //获取Java层被回调的函数
    jmethodID javaCallback = (*env)->GetMethodID(env, javaClass, "onNativeThreadCallback", "(I)V");
    if (javaCallback == NULL) {
        LOGI("Fail to find method onNativeCallback");
        return 0;
    }

    LOGI("native_thread_exec loop enter");

    int count = 0;

    //线程循环
    while (!gIsThreadExit) {

        //回调Java层的函数
        (*env)->CallVoidMethod(env, gJavaObj, javaCallback, count++);

        //休眠1秒
        sleep(1);
    }

    (*gJavaVM)->DetachCurrentThread(gJavaVM);

    LOGI("native_thread_exec loop leave");
}



/*
 * Class:     com_ticktick_jnicallback_Native
 * Method:    开启线程
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeThreadStart(JNIEnv *env,
                                                            jobject thiz) {

    gIsThreadExit = 0;

    //通过pthread库创建线程
    pthread_t threadId;
    if (pthread_create(&threadId, NULL, native_thread_exec, NULL) != 0) {
        LOGI("native_thread_start pthread_create fail !");
        return;
    }

    LOGI("native_thread_start success");
}

/*
 * Class:     com_ticktick_jnicallback_Native
 * Method:    NativeThreadStop
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeThreadStop(JNIEnv *env,
                                                           jobject thiz) {
    gIsThreadExit = 1;
    LOGI("native_thread_stop success");
}

  第一个本地方法:返回一个字符串。
  第二个本地方法:是初始化一些全局变量,在开启线程时使用。
  第三个本地方法:是开启线程。
  第四个本地方法:是关闭线程
  大部分代码是有注释的,大家应该是可以看懂的。

三、方法的签名

JNINativeMethod的定义如下:

typedef struct {
   const char* name;
   const char* signature;
   void* fnPtr;
} JNINativeMethod;

第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值
第三个变量fnPtr是函数指针,指向C函数。

其中比较难以理解的是第二个参数,例如
“()V”
“(II)V”
“(Ljava/lang/String;Ljava/lang/String;)V”

实际上这些字符是与函数的参数类型一一对应的。
“()” 中的字符表示参数,后面的则代表返回值。例如”()V” 就表示void Func();
“(II)V” 表示 void Func(int, int);

那其他情况呢?请查看下表:
类型
符号
Android进阶之路——NDK(二)_第2张图片
稍稍补充一下:

1、方法参数或者返回值为java中的对象时,签名中必须以“L”加上其路径,不过此路径必须以“/”分开,自定义的对象也使用本规则
比如说 java.lang.String为“java/lang/String”,com.nedu.jni.helloword.Student为”Lcom /nedu/jni/helloword/Student;”

2、方法参数或者返回值为数组类型时,请前加上[

例如[I表示 int[],[[[D表示 double[][][],即几维数组就加几个[
Android进阶之路——NDK(二)_第3张图片

四、总结

  1. 在JNI_OnLoad中,保存JavaVM*,这是跨线程的,持久有效的,而JNIEnv*则是当前线程有效的。一旦启动线程,用AttachCurrentThread方法获得env。
  2. 通过JavaVM*和JNIEnv可以查找到jclass。
  3. 把jclass转成全局引用,使其跨线程。
  4. 然后就可以正常地调用你想调用的方法了。
  5. 用完后,别忘了delete掉创建的全局引用和调用DetachCurrentThread方法。

你可能感兴趣的:(Android)