NDK Samples [2] - hello-jniCallback

96
七零八落问号
2018.11.11 16:36* 字数 662

NDK Samples目录:GoogleSamples - NDK Samples


项目地址:https://github.com/googlesamples/android-ndk/tree/master/hello-jniCallback
说明文档:https://github.com/googlesamples/android-ndk/blob/master/hello-jniCallback/README.md

该项目演示如何从C代码调用Java层方法。


最低要求:

  1. Android Studio 版本大于 2.2

该项目的演示的方法主要是:

  1. JniHandler :: getBuildVersion()
    演示C代码如何回调Java层的静态方法。

  2. JniHandler :: getRuntimeMemorySize()
    演示C代码如何回调Java层的实例方法。

  3. JniHandler :: updateStatus(String msg)
    演示C代码如何回调Java层带参方法。


代码主要分析 hello-jnicallback.c ,java层代码比较简单直接省略:

声明了结构体 TickContext,主要负责了:
1.缓存JavaVM、jniHelper/MainActiviy的类和实例
2.互斥锁lock
3.主要逻辑中的循环的执行状态 done。
对应的结构体变量为 g_ctx。

/*
  声明结构体 tick_context 
 */
typedef struct tick_context {
    JavaVM  *javaVM;                // javaVM
    jclass   jniHelperClz;          // JniHandler类,全局引用
    jobject  jniHelperObj;          // JniHandler实例,全局引用
    jclass   mainActivityClz;       // MainActivity类,全局引用
    jobject  mainActivityObj;       // MainActivity实例,全局引用
    pthread_mutex_t  lock;          // 一个互斥锁
    int      done;                  // 循环的执行状态,1表示已完成,退出循环
} TickContext;

/*
  定义结构体变量 g_ctx
 */
TickContext g_ctx;

JNI_OnLoad在System.loadLibrary后被调用,相当于动态库的初始化方法:
1:缓存JavaVM
2:缓存JniHelper类的缓存,创建了一个JniHelper实例并缓存
3:调用queryRuntimeInfo方法

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {
    // JniEnv
    JNIEnv* env;

    // 初始化结构体 g_ctx
    memset(&g_ctx, 0, sizeof(g_ctx));

    // 缓存 JavaVM
    g_ctx.javaVM = vm;

    // 检查是否支持 JNI_VERSION_1_6 版本, 不支持时返回JNI_ERR
    if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // 查找JniHandler类,并缓存
    jclass clz = (*env)->FindClass(env, "com/example/hellojnicallback/JniHandler");
    g_ctx.jniHelperClz = (*env)->NewGlobalRef(env, clz);
    
    // 创建一个JniHandler实例,并缓存
    jmethodID jniHelperCtor = (*env)->GetMethodID(env, g_ctx.jniHelperClz, 
                                                  "<init>", "()V");
    jobject handler = (*env)->NewObject(env, g_ctx.jniHelperClz, jniHelperCtor);
    g_ctx.jniHelperObj = (*env)->NewGlobalRef(env, handler);

    // 获取引用的运行信息
    // 主要是 展示如何从 c代码调用java层 JniHelper 的方法。 
    queryRuntimeInfo(env, g_ctx.jniHelperObj);

    // 初始化运行状态
    g_ctx.done = 0;

    g_ctx.mainActivityObj = NULL;

    // 返回该动态库的JNI版本
    return  JNI_VERSION_1_6;
}

queryRuntimeInfo方法主要演示了如何从C层调用Java层方法,包括静态方法和实例方法。
注意:还演示如何处理资源的手动释放,以避免内存泄漏
以下代码分析忽略非空检测,移除非空检测代码块:

void queryRuntimeInfo(JNIEnv *env, jobject instance) {
    
    // 查找 JniHelper 静态方法 getBuildVersion 的 ID
    jmethodID versionFunc = (*env)->GetStaticMethodID(
                                            env, g_ctx.jniHelperClz, 
                                            "getBuildVersion", "()Ljava/lang/String;");

    // 调用静态方法 getBuildVersion,获取返回值
    // !Java对象需要在使用结束后释放引用
    jstring buildVersion = (*env)->CallStaticObjectMethod(
                                           env, g_ctx.jniHelperClz, versionFunc);

    // 将 jstring 转换成一个 UTF-8 的 C字符串
    // !在不关注拷贝结果的情况下,请勿对 GetStringXXX 结果进行修改
    // !GetXXX 返回值在使用后,需要调用对应的 ReleaseXXX,以释放对Java层对象的引用
    // !参考:https://www.cnblogs.com/codc-5117/archive/2012/09/06/2672833.html
    const char *version = (*env)->GetStringUTFChars(env, buildVersion, NULL);
    
    // 打印信息
    LOGI("Android Version - %s", version);

    // 释放 GetStringUTFChars 产生的引用
    (*env)->ReleaseStringUTFChars(env, buildVersion, version);

    // 释放 CallStaticObjectMethod 返回的 jstring 的引用
    (*env)->DeleteLocalRef(env, buildVersion);
    
    // 查找 JniHelper 实例方法 getRuntimeMemorySize 的 ID
    jmethodID memFunc = (*env)->GetMethodID(env, g_ctx.jniHelperClz,
                                            "getRuntimeMemorySize", "()J");

    // 调用实例方法 getRuntimeMemorySize,获取返回值 
    // !jlong等基本数据类型不需要手动释放
    // !参考:https://blog.csdn.net/zhangguixian5/article/details/8490114
    jlong result = (*env)->CallLongMethod(env, instance, memFunc);

    // 打印信息
    LOGI("Runtime free memory size: %" PRId64, result);

    // 屏蔽编译器警告   
    (void)result; 
}

在MainActivity的声明周期onResume中,会调用本地方法startTick:
1.缓存MainActivity类和实例
2.创建一个新线程,新线程中调用UpdateTicks完成实际逻辑
注意:还演示了C层如何创建一个新的线程

JNIEXPORT void JNICALL
Java_com_example_hellojnicallback_MainActivity_startTicks(
    JNIEnv *env, jobject instance) {
    // 线程ID
    pthread_t       threadInfo_;
    // 线程属性
    pthread_attr_t  threadAttr_;

    // 初始化线程属性
    pthread_attr_init(&threadAttr_);

    // 设置为分离线程
    // !参考:https://blog.csdn.net/inuyashaw/article/details/78904231
    pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);

    // 初始化互斥锁
    pthread_mutex_init(&g_ctx.lock, NULL);

    // 缓存MainActivity类和实例
    jclass clz = (*env)->GetObjectClass(env, instance);
    g_ctx.mainActivityClz = (*env)->NewGlobalRef(env, clz);
    g_ctx.mainActivityObj = (*env)->NewGlobalRef(env, instance);

    // 创建线程
    int result  = pthread_create( &threadInfo_, &threadAttr_, UpdateTicks, &g_ctx);
    assert(result == 0);
    
    // 销毁线程属相,释放内存
    pthread_attr_destroy(&threadAttr_);

    (void)result;
}

UpdateTicks 是一个死循环,主要逻辑为定时调用Java层 MainActivity::updateTimer 方法:
注意:还展示了C层时间函数的简单使用
以下代码分析忽略sendJavaMsg(主要为Java层日志),移除sendJavaMsg及相关的 statusId:

void* UpdateTicks(void* context) {
    // TickContext 结构体变量
    TickContext *pctx = (TickContext*) context;

    // javaVM
    JavaVM *javaVM = pctx->javaVM;

    // 调用 GetEnv 获取当前线程 JniEnv
    // 当前当前线程未附加到 VM 时,调用 AttachCurrentThread 将当前线程附加到 VM
    // !文档:
    // https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html#GetEnv
    // https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html#attach_current_thread
    JNIEnv *env;
    jint res = (*javaVM)->GetEnv(javaVM, (void**)&env, JNI_VERSION_1_6);
    if (res != JNI_OK) {
        res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
        if (JNI_OK != res) {
            LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res);
            return NULL;
        }
    }

    // 获取 MainActivity 实例方法 updateTimer 的 ID
    // updateTimer 的 Java层逻辑 十分简单:每调用一次,时间显示 +1s
    jmethodID timerId = (*env)->GetMethodID(env, pctx->mainActivityClz, "updateTimer", "()V");

    // 定义时间结构体 timeval 变量 beginTime, curTime, usedTime, leftTime
    // !timeval 有两个成员,一个是秒,一个是微秒
    struct timeval beginTime, curTime, usedTime, leftTime;
    // 定义时间结构体 timeval 常量 kOneSecond 
    const struct timeval kOneSecond = {
            (__kernel_time_t)1,
            (__kernel_suseconds_t) 0
    };
    
    // 主要逻辑的死循环
    while(1) {
        // 记录开始时间
        gettimeofday(&beginTime, NULL);
        
        // 互斥锁加锁
        pthread_mutex_lock(&pctx->lock);
        // 获取当前的调用状态
        int done = pctx->done;
        // 还原结构体的调用状态为0
        if (pctx->done) {
            pctx->done = 0;
        }
        // 互斥锁解锁
        pthread_mutex_unlock(&pctx->lock);
        
        // 当前状态为1时,表示完成逻辑,退出主循环
        if (done) {
            break;
        }

        // 主逻辑开始
        // 调用 MainActivity 静态方法 updateTimer (Java层的时间显示 +1s)
        (*env)->CallVoidMethod(env, pctx->mainActivityObj, timerId);

        // 记录结束时间
        gettimeofday(&curTime, NULL);

        // 计算调用 updateTimer 的耗时(结束时间 - 开始时间)
        timersub(&curTime, &beginTime, &usedTime);
        // 计算剩余需要等待的时间 (1s - 耗时)
        timersub(&kOneSecond, &usedTime, &leftTime);
        
        // 定义结构体 timespec 变量 sleepTime
        // !timespec 有两个成员,一个是秒,一个是纳秒
        struct timespec sleepTime;
        // 把计算的剩余时间赋值到 sleepTime
        sleepTime.tv_sec = leftTime.tv_sec;
        sleepTime.tv_nsec = leftTime.tv_usec * 1000;    // 微杪转纳秒 * 1000

        // 等待时间的秒数等于1秒或少于1秒,调用 nanosleep 进行纳米级睡眠
        // 等待时间大于1秒???啥情况
        if (sleepTime.tv_sec <= 1) {
            nanosleep(&sleepTime, NULL);
        } else { /* Log */ }
    }
    
    // 把当前线程从 VM 中分离
    // 不进行分离操作会导致线程无法退出引起资源泄漏
    (*javaVM)->DetachCurrentThread(javaVM);
    return context;
}

在MainActivity的声明周期onPause中,会调用本地方法StopTicks:
1.修改执行状态为 1 (表示已完成)
2.销毁startTicks中产生的全局引用和变量
(这里的StopTicks方法首字母是大写,可能是一个小坑吧,不影响)

JNIEXPORT void JNICALL
Java_com_example_hellojnicallback_MainActivity_StopTicks(JNIEnv *env, jobject instance) {
    // 互斥锁加锁,确保执行状态的线程安全
    pthread_mutex_lock(&g_ctx.lock);
    // 执行状态设置为已完成,会导致逻辑线程退出循环
    g_ctx.done = 1;
    // 互斥锁解锁
    pthread_mutex_unlock(&g_ctx.lock);

    // 定义结构体 timespec 变量 sleepTime
    struct timespec sleepTime;
    // 初始化sleepTime,置为0
    memset(&sleepTime, 0, sizeof(sleepTime));
    // 设置sleepTime,时长等于0.1秒
    sleepTime.tv_nsec = 100000000;

    // 等待逻辑线程的循环结束
    // 当逻辑线程执行时,会将上面的 状态1 设成 状态0,这里就是等待逻辑线程的执行
    // 每次等待的时间为0.1秒
    while (g_ctx.done) {
        nanosleep(&sleepTime, NULL);
    }

    // 此处开始,逻辑线程已退出主循环

    // 删除在 startTicks 中缓存的 MainActivity类和对象 的全局引用,避免内存泄漏
    (*env)->DeleteGlobalRef(env, g_ctx.mainActivityClz);
    (*env)->DeleteGlobalRef(env, g_ctx.mainActivityObj);
    
    // 置空
    g_ctx.mainActivityObj = NULL;
    g_ctx.mainActivityClz = NULL;

    // 销毁在 startTicks 中创建的互斥锁
    pthread_mutex_destroy(&g_ctx.lock);
}

此次完成项目的主要部分的源码分析,共演示了:

  1. C层中调用Java层的静态/实例方法
  2. C层中创建线程并附加到VM
  3. 部分函数,如互斥锁,时间函数的简单使用
  4. 基本的资源手动释放处理,以避免内存泄漏
Google官方Demo
Web note ad 2