Android进阶之路Android-NDK/JNIandroid技术收藏

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

2020-09-26  本文已影响0人  后厂村追寻

人间观察

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

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

今天我们看一个比较常见的场景:
当我们处理一个密集型计算数据(比如音视频的软编解码处理,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

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

上一篇下一篇

猜你喜欢

热点阅读