Android-JNI开发系列《二》在jni层的线程中回调到java层

人间观察

忽有故人心上头,回首山河已是秋。

马上国庆+中秋了。~~~

今天我们看一个比较常见的场景:
当我们处理一个密集型计算数据(比如音视频的软编解码处理,bitmap的特效处理等),这时候就需要用c/c++实现。当在c/c++处理完后需要异步回调/通知到java中,这样代码看起来才很优雅有气质。
如果你知道这个知识那就return吧。~~

在Android中你可以用Thread+Handler很容易的来实现,我相信你闭着眼都能写了。但在jni层中不是这么简单的,我们如何实现?

我们先看一下在jni中非子线程中如何回调再看下在子线程如何回调到java层中。

jni中非子线程回调到java方法中

和普通的在jni中调用java的实例方法没啥区别,上代码:

// java回调接口 INativeListener.java
public interface INativeListener {
    void onCall();
}
public native void nativeCallBack(INativeListener callBack);
// jni_thread_callback.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_cb_JNIThreadCallBack_nativeCallBack(JNIEnv *env, jobject thiz,
                                                           jobject call_back) {
    // 获取java中的对象
    jclass cls = env->GetObjectClass(call_back);
    // 获取回调方法的id
    jmethodID mid = env->GetMethodID(cls, "onCall", "()V");
    // 调用java中的方法
    env->CallVoidMethod(call_back, mid);
}

总结: 分三步

  1. 根据java的obj获取jclass。jclass可以理解为java中的class对象(如果熟悉jni就没啥问题)
  2. 根据1步中的jclass和方法名字和方法的签名获取该方法的jmethodID
  3. env调用java实例的方法。(当前如果是静态的只是调用的方法不一样)

jni中的子线程回调到java方法

主要方法是JavaVM中的AttachCurrentThreadDetachCurrentThread两个方法,这两个是对应的。
官方文档:
官网doc地址

有关注释

Attaching to the VM
The JNI interface pointer (JNIEnv) is valid only in the current thread. Should another thread need to access the Java VM, 
it must first call AttachCurrentThread() to attach itself to the VM and obtain a JNI interface pointer.
Once attached to the VM, a native thread works just like an ordinary Java thread running inside a native method. 
The native thread remains attached to the VM until it calls DetachCurrentThread() to detach itself.

The attached thread should have enough stack space to perform a reaonable amount of work. 
The allocation of stack space per thread is operating system-specific. For example, using pthreads,
the stack size can be specified in the pthread_attr_t argument to pthread_create.

译一下:

依附到Java虚拟机上
JNI接口指针(JNIEnv)仅在当前线程中有效。
如果另一个线程需要访问jvm,它必须首先调用AttachCurrentThread()将自己附加到 JVM并获取JNI接口指针。
一旦连接到JVM上,本地线程(jni线程)的工作方式与在本地方法中运行的普通Java线程一样。
本机线程在调用DetachCurrentThread()来分离它自己之前一直连接到VM。

附加的线程应该有足够的堆栈空间来执行合理数量的任务。
每个线程的堆栈空间分配是取决于操作系统。
例如,使用pthreads,可以在pthread_attr_t参数中为pthread_create指定堆栈大小。

而在调用JavaVM中的AttachCurrentThreadDetachCurrentThread我们需要拿到JavaVM *vm指针。怎么拿到这个呢?一种是调用JNI_CreateJavaVM加载并初始化Java虚拟机,并返回指向JNI接口指针的指针。我们可以用另外一种jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)全局变量保存一下vm即可。
如果不了解JNI_OnLoad可以看上一篇文章 jni动态库的函数注册

接下来,我们写一个简单的功能:在jni中创建一个线程实现一个写入随机字符串到文件(用来模拟线程任务的耗时),然后写入完成后给java层一个回调告诉java层写入成功。

// java回调接口 INativeThreadListener.java
public interface INativeThreadListener {
    void onSuccess(String msg);
}
public native void nativeInThreadCallBack(INativeThreadListener listener);
JavaVM *gvm;
jobject gCallBackObj;
jmethodID gCallBackMid;

extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_cb_JNIThreadCallBack_nativeInThreadCallBack(JNIEnv *env, jobject thiz,
                                                                   jobject call_back) {
    // 创建一个jni中的全局引用
    gCallBackObj = env->NewGlobalRef(call_back);
    jclass cls = env->GetObjectClass(call_back);
    gCallBackMid = env->GetMethodID(cls, "onSuccess", "(Ljava/lang/String;)V");
    // 创建一个线程
    pthread_t pthread;
    jint ret = pthread_create(&pthread, nullptr, writeFile, nullptr);
    LOG_D("pthread_create ret=%d", ret);
}

这里简单说一下线程的几个参数

 pthread_create
 参数1 pthread_t* pthread 线程句柄
 参数2  pthread_attr_t const* 线程的一些属性
 参数3 void* (*__start_routine)(void*) 线程具体执行的函数
 参数4 void* 传给线程的参数
 返回值 int  0 创建成功
/**
 * 相当于java中线程的run方法
 * @return
 */
void *writeFile(void *args) {
    // 随机字符串写入
    FILE *file;
    if ((file = fopen("/sdcard/thread_cb", "a+")) == nullptr) {
        LOG_E("fopen filed");
        return nullptr;
    }
    for (int i = 0; i < 10; ++i) {
        fprintf(file, "test %d\n", i);
    }
    fflush(file);
    fclose(file);
    LOG_D("file write done");

    // https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html
    JNIEnv *env = nullptr;
    // 将当前线程添加到Java虚拟机上,返回一个属于当前线程的JNIEnv指针env
    if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
        jstring jstr = env->NewStringUTF("write success");
        // 回调到java层
        env->CallVoidMethod(gCallBackObj, gCallBackMid, jstr);
        // 删除jni中全局引用
        env->DeleteGlobalRef(gCallBackObj);
        // 从Java虚拟机上分离当前线程
        gvm->DetachCurrentThread();
    }
    return nullptr;
}

其实还是jni中非子线程回调到java方法中的三个步骤,只不是多了AttachCurrentThreadDetachCurrentThread的操作。基本的注释在代码中体现了,另外关于文件的写入,属于linux下c的基本操作这里不多说了,不了解的可以看下有关知识。

备注:jni中有写入文件的操作,记得加入Android 权限哦。

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

测试一下,结果:

// 看一下文件内容,符合预期
tb8765ap1_bsp_1g:/ # cat /sdcard/thread_cb                                                                                                      
test 0
test 1
test 2
test 3
test 4
test 5
test 6
test 7
test 8
test 9

// 回调,符合预期onSuccess的回调运行在非ui线程中
2020-09-21 21:43:40.441 8004-8004/com.bj.gxz.jniapp D/JNI: JNI_OnLoad Call
2020-09-21 21:43:40.443 8004-8004/com.bj.gxz.jniapp D/JNI: onCall invoked,threadName:main
2020-09-21 21:43:40.443 8004-8004/com.bj.gxz.jniapp D/JNI: pthread_create ret=0
2020-09-21 21:43:40.447 8004-8067/com.bj.gxz.jniapp D/JNI: file write done
2020-09-21 21:43:40.448 8004-8067/com.bj.gxz.jniapp D/JNI: onSuccess invoked,msg:write success
2020-09-21 21:43:40.449 8004-8067/com.bj.gxz.jniapp D/JNI: onSuccess invoked,threadName:Thread-111

源代码:https://github.com/ta893115871/JNIAPP

最后,祝大家中秋国庆做个三好学生(吃好喝好玩好)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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