美文网首页Android开发Android技术知识
NDK Samples [2] - hello-jniCallb

NDK Samples [2] - hello-jniCallb

作者: 七零八落问号 | 来源:发表于2018-11-11 16:36 被阅读3次

    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. 基本的资源手动释放处理,以避免内存泄漏

    相关文章

      网友评论

        本文标题:NDK Samples [2] - hello-jniCallb

        本文链接:https://www.haomeiwen.com/subject/ntguxqtx.html