NDK Samples [2] - hello-jniCallback

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. 基本的资源手动释放处理,以避免内存泄漏
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容