Native线程attach方式

来源:互联网 发布:欧洲女人身材变形知乎 编辑:程序博客网 时间:2024/06/03 16:48

概述

写过jni的知道,可以从native线程回调java方法。而回调java方法时,必要的两个标准步骤是调用前attach native线程,调用结束后detach native线程。

为方便后文表述,代码先行。我们先定义一个Java类:

public class Foo {    static {        System.loadLibrary("foo");    }    public static native void startThread();    public static void onRun(){        Log.i("Foo", "onRun: thread run");    }}

该类被vm加载时执行static块加载了native库libfoo.so,其中startThread会启动一个native线程,并回调onRun方法。

接着使用javah自动生成头文件:

/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_cy_myapplication_Foo */#ifndef _Included_com_cy_myapplication_Foo#define _Included_com_cy_myapplication_Foo#ifdef __cplusplusextern "C" {#endif/* * Class:     com_cy_myapplication_Foo * Method:    startThread * Signature: ()V */JNIEXPORT void JNICALL Java_com_cy_myapplication_Foo_startThread  (JNIEnv *, jclass);#ifdef __cplusplus}#endif#endif

自动生成的头文件很简单,就一个Java_com_cy_myapplication_Foo_startThread方法。

接着在foo.cpp实现该函数:

JNIEXPORT void JNICALL Java_com_cy_myapplication_Foo_startThread(JNIEnv * jenv, jclass jcls){    jenv->GetJavaVM(&jvm);    cls = (jclass)jenv->NewGlobalRef(jcls);    method = jenv->GetStaticMethodID(cls, "onRun", "()V");    pthread_t thd;    __android_log_print(ANDROID_LOG_INFO, "jni", "create thread");    pthread_create(&thd, NULL, myproc, NULL);//    pthread_create(&thd, NULL, otherproc, (void*)java_callback1);//    pthread_create(&thd, NULL, otherproc, (void*)java_callback);    pthread_join(thd, NULL);    __android_log_print(ANDROID_LOG_INFO, "jni", "join thread");    if (detachKey!=0)        pthread_key_delete(detachKey);    jenv->DeleteGlobalRef(cls);}

上述代码使用NewGlobalRef将Foo这个类保存到全局变量方便线程函数使用(获取onRun methodID目的相同)。这里提前涉及到了pthread_key,读者可忽略,后文将详细阐述。

接下来,详细剖析如何attach native线程。

常用方式

较为常见的情况,线程函数是可控的,也就是自己可以修改到的,也方便添加jni代码的。对于这种情况,可以实现如下:

static void* myproc(void* arg){    JNIEnv* jenv = NULL;    jvm->GetEnv((void**)&jenv, JNI_VERSION_1_2);    __android_log_print(ANDROID_LOG_INFO, "jni", "attach thread");    jvm->AttachCurrentThread(&jenv, NULL);    __android_log_print(ANDROID_LOG_INFO, "jni", "run thread body");    jenv->CallStaticVoidMethod(cls, method);    __android_log_print(ANDROID_LOG_INFO, "jni", "detach thread");    jvm->DetachCurrentThread();    return NULL;}

即,先GetEnv获取当前线程的环境(JNIEnv),然后AttachCurrentThread,调用java方法后DetachCurrentThread

高级方式

但还有一种情况,线程函数是不受控的,也就是你无法修改线程函数,或者是无法将jni代码“侵入式”地加入到线程函数中。

比如考虑下面这种情形:一个成熟的库,提供了一个启动后台线程的入口(libStartThread),并在后台线程中回调传递给它的函数指针。而该库的源码不可修改,或还需为非java平台提供服务,从而导致你无法将jni代码注入,而不能完成必须的Attach、Detach步骤。示例代码如下:

typedef void(*Callback)(int);static void* otherproc(void* arg){    Callback func = (Callback)arg;    //__android_log_print(ANDROID_LOG_INFO, "jni", "do callback first time");    func(1);    //__android_log_print(ANDROID_LOG_INFO, "jni", "do callback second time");    func(2);}//void libStartThread(Callback callback);

otherproc是成熟库内部的后台线程,将回调Callback函数指针。不巧的是,移植到java后,你得在java为其提供回调。

可选的一种方案,仅需做一个简单的封装:

static void java_callback(int time){    JNIEnv* jenv = NULL;    jvm->GetEnv((void**)&jenv, JNI_VERSION_1_2);    __android_log_print(ANDROID_LOG_INFO, "jni", "attach thread");    jvm->AttachCurrentThread(&jenv, NULL);    __android_log_print(ANDROID_LOG_INFO, "jni", "run thread body %d", time);    jenv->CallStaticVoidMethod(cls, method);    __android_log_print(ANDROID_LOG_INFO, "jni", "detach thread");    jvm->DetachCurrentThread();}

native实现一个java_callback,作为java方法(onRun)的代理,每次有回调产生时先Attach当前线程,然后调用java方法,之后Detach即可。

另一种方案,与上述方案中每次回调都attach、detach不同,只对native线程做一次attach、detach。这就需要借助于pthread_key_t。关于pthread_key_t可以参考linux manpage。其中我们关心的特性描述如下:

An optional destructor function may be associated with each key value. At thread exit, if a key value has a non-NULL destructor pointer, and the thread has a non-NULL value associated with that key, the value of the key is set to NULL, and then the function pointed to is called with the previously associated value as its sole argument.

简单理解,即值非空的pthread_key_t,将在线程退出时回调一个“析构函数”,该“析构函数”的参数是由此前通过pthread_setspecific设置得到的。

那么我们可以据此实现java_callback1:

static pthread_key_t detachKey=0;static void detachKeyDestructor(void* arg){    pthread_t thd = pthread_self();    JavaVM* jvm = (JavaVM*)arg;    __android_log_print(ANDROID_LOG_INFO, "jni", "detach thread");    jvm->DetachCurrentThread();}static void java_callback1(int time){    JNIEnv* jenv = NULL;    int status = jvm->GetEnv((void**)&jenv, JNI_VERSION_1_2);    __android_log_print(ANDROID_LOG_INFO, "jni", "getenv: %d", status);    if (status == JNI_EDETACHED)    {        if (detachKey == 0)        {            __android_log_print(ANDROID_LOG_INFO, "jni", "create thread key");            pthread_key_create(&detachKey, detachKeyDestructor);        }        __android_log_print(ANDROID_LOG_INFO, "jni", "attach thread");        jvm->AttachCurrentThread(&jenv, NULL);        pthread_setspecific(detachKey, jvm);    }    __android_log_print(ANDROID_LOG_INFO, "jni", "run thread body %d", time);    jenv->CallStaticVoidMethod(cls, method);}

首先也是获取JNIEnv,但这次需要判断返回值,如果返回值为JNI_EDETACHED,表示之前该线程一直处于detached状态,从未attach过。那么这时我们需要设置pthread_key_t的值,即pthread_setspecific(detachKey, jvm);,将jvm传给“析构函数”,方便析构函数detach线程。当然,如果detachKey还未创建,我们需要创建一下,并绑定“析构函数”。接下来就是同样的AttachCurrentThread和CallStaticVoidMethod了。(当然,读者也可以通过别的方式判断是否第一次Attach,不一定要通过判断GetEnv的返回值这一方法)

“析构函数”通过pthread_self获取调用“析构函数”的当前线程,并利用传入的jvm调用DetachCurrentThread detach线程。至此,就完成了一次高端的“炫技”。

pthread_key_t+DetachCurrentThread的背后原理

当然,上述提及的“炫技”是有官方文档支持的:

Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward, in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific to store the JNIEnv in thread-local-storage; that way it’ll be passed into your destructor as the argument.)

0 0