Native线程attach方式

概述

写过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 __cplusplus
extern "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.)

### 关于Attach周期或Attach生命周期的理解 在编程领域,“attach周期”并不是一个广泛使用的术语,但在某些特定场景下可能具有一定的含义。以下是对其潜在意义的解释以及其在不同上下文中如何工作的分析。 #### 1. Attach 周期的概念 Attach 可以理解为“附加”的意思,在软件开发中通常指某个组件、模块或者资源被绑定到另一个实体的过程。例如: - **线程附着(Thread Attaching)**:在线程管理中,当一个本地方法需要访问 Java 虚拟机中的资源时,它必须先通过 JNIJava Native Interface)将当前线程附着到 JVM 上[^1]。 - **React 组件挂载(Mounting)**:虽然这不是严格意义上的 attach 生命周期,但它可以类比为一种“挂载”行为。在 React 中,`componentDidMount` 是组件完成首次渲染并成功挂载到 DOM 后触发的一个钩子函数[^4]。 因此,Attach 周期可以描述为某种对象或资源从独立状态转变为与其他系统关联的状态这一过程的时间范围。 #### 2. 在编程中的具体实现方式 根据不同环境和技术栈,Attach 的工作机制有所差异: ##### (1JNI 线程附着 对于基于 Java 的应用程序来说,如果使用 C/C++ 编写原生代码并通过 JNI 接口调用 Java 方法,则需要显式地把本机线程注册给 JVM。这涉及到两个主要操作——`AttachCurrentThread` 和 `DetachCurrentThread`。前者用于启动一个新的非 JAVA VM 创建出来的线程进入可运行 java code 的状态;后者则是结束该线程与虚拟机之间的连接关系[^3]。 示例代码如下所示: ```c++ JNIEnv* env; jvm->AttachCurrentThread((void**)&env, NULL); // 将当前线程附加至JVM // 执行一些逻辑... jvm->DetachCurrentThread(); // 解除附件关系 ``` ##### (2)CORBA 对象代理绑定 在分布式计算框架 CORBA (Common Object Request Broker Architecture)[^2]里边提到过类似的机制。“激活”服务器端实例之前往往要经历初始化阶段即所谓的 “preinvoke”,期间会涉及设置目标 servant 并将其映射到指定 object reference 上面来形成一对一的关系。这种设定实际上也是一种形式上的 attachment 过程因为只有完成了这些准备工作之后客户端才能正常发起远程请求并与服务方交互数据包等等信息流传输活动得以顺利开展下去。 ##### (3)前端框架中的 Mount 行动 像 Vue.js 或者 AngularJS 都有自己的生命周流程定义其中就包含了类似于 mount/unmount 功能点的设计理念。拿前面已经提及过的 reactjs 来举例说明的话每当新节点加入 dom 结构树形图之中便会自动触发展开一系列预定好的事件处理程序链路从而达成动态更新视图界面效果的目的同时也会伴随着性能优化方面的考量比如懒加载策略的应用减少不必要的重新绘制次数提高用户体验满意度等方面做出贡献. #### 总结 尽管没有统一的标准用来确切表述所谓 'attach' 生命时期到底是什么样子的东西但是我们可以发现无论是在低级操作系统层面还是高级应用层面上都会频繁出现这样一类现象那就是让原本孤立存在的个体之间建立起联系进而共同协作完成既定任务使命这样的整个演变历程就可以看作是一种广义上所说的attachment period. --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值