Android native开发需要注意native线程的Detach

本文介绍了在跨平台开发中如何解决JNI崩溃问题,重点讲解了线程缓存JNIEnv和一次性使用Env两种解决方案,包括线程特有数据pthread_key_t的应用以及Cocos2d和WebRTC的示例。

前言

如题在跨平台开发的过程中,通常跨平层/C++会创建native线程,如果创建的native线程运行的代码有调用到Java层接口(跟java层有交互),即有调到了AttachCurrentThread,当native线程退出时需要调DetachCurrentThread接口跟JVM世界做分离。否侧会遇到JNI经典崩溃问题之—Native thread exited without calling DetachCurrentThread,如果Deatch的处理逻辑写的不好,可能还会继续遇到JNI经崩溃崩溃问题之—attempting to detach while still running code (这种情况就是java的线程被错误的Detach了!)

解决方案

1.每个线程缓存自己的Env

每个线程缓存自己的Env意味着线程第一个attach到JVM就进行就缓存env实例,待线程退出/销毁时进行deattach操作并置空env实例!实例这个功能需要依赖如下三个接口跟一个线程存储变量/TSD(pthread_key_t变量,变量声名是全局变量,但各个线程会有自己独立的存诸空间)

//创建一个key实例,并绑定key实例释放时的回调函数,即线程退出时会清理所有key,同时调用跟key实例绑定的回调函数!(特别注意这个回调函数,后面会使到它)
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
//保存key实例对应的值,同hashmap的用法
int pthread_setspecific(pthread_key_t key, const void *value);
//获取key对应的值
void *pthread_getspecific(pthread_key_t key);

我们可以参考或抄或拿来主义等等方式使用cocos2d的jni的功能类 cocos2d JinHelperwebtrc的 jvm.cc , JinHelper缓存env的关键实现是getEnv中的cacheEnv函数

    JNIEnv* JniHelper::getEnv() {
         //优先从线程存储的cache中拿
        JNIEnv *_env = (JNIEnv *)pthread_getspecific(g_key);
        if (_env == nullptr)
            _env = JniHelper::cacheEnv(_psJavaVM);
        return _env;
    }

JNIEnv* JniHelper::cacheEnv(JavaVM* jvm) {
        JNIEnv* _env = nullptr;
        // get jni environment
        jint ret = jvm->GetEnv((void**)&_env, JNI_VERSION_1_4);
        
        switch (ret) {
        case JNI_OK :
            // Success! ,已经attch了,就存在线程存储中(这种情况是java调用进来又回调到java层的情况)
            pthread_setspecific(g_key, _env);
            return _env;
                
        case JNI_EDETACHED :
            // Thread not attached, detached的话,再attach一把
            if (jvm->AttachCurrentThread(&_env, nullptr) < 0)
                {
                    LOGE("Failed to get the environment using AttachCurrentThread()");

                    return nullptr;
                } else {
                // Success : Attached and obtained JNIEnv!
                pthread_setspecific(g_key, _env);
                return _env;
            }
                
        case JNI_EVERSION :
            // Cannot recover from this error
            LOGE("JNI interface version 1.4 not supported");
        default :
            LOGE("Failed to get the environment using GetEnv()");
            return nullptr;
        }
    }

  void JniHelper::setJavaVM(JavaVM *javaVM) {
        pthread_t thisthread = pthread_self();
        //LOGD("JniHelper::setJavaVM(%p), pthread_self() = %ld", javaVM, thisthread);
        _psJavaVM = javaVM;
		//绑定key被回收时的回调函数_detachCurrentThread (里边会DetachCurrentThread的方法),从而可以实现线程退出时detach JVM
        pthread_key_create(&g_key, _detachCurrentThread);
    }

void _detachCurrentThread(void* a) {
    cocos2d::JniHelper::getJavaVM()->DetachCurrentThread();
}

2.一次性使用Env

一次性使用Env的意思是,每次attch到JVM获到env并使用env后就deattch JVM。保持attch与eattch的配对!这种方法有点简单但有点暴力!(可能会有性能问题)
示例代码如下

  JNIEnv* env = nullptr;
  int result = g_jvm->GetEnv((void**)&env, JNI_VERSION_1_6);
  bool shouldDetach = false;
  if (result == JNI_EDETACHED) {
    jint attachResult = jvm->AttachCurrentThread(&jenv, NULL);
    if (attachResult >= 0)
      shouldDetach = true;
  }
  
  // env的使用,调java的接口接
  ....
  
  if (shouldDetach) {
    g_jvm->DetachCurrentThread();  
  }
  env = nullptr;

参考资料

<think>嗯,用户这次询问的是AndroidNative层用C++创建线程的具体方法,特别提到了pthread的示例。从历史记录看,之前讨论过C++11的std::thread用法,但用户现在明确需要原生pthread的方案,可能正在处理NDK开发或底层移植相关的工作。用户引用的材料很有针对性:引用[1]展示了std::thread的基础用法,引用[2][3]则提到Android设置线程名的坑和底层机制。这说明用户应该遇到过线程命名失效的问题,可能需要特别注意pthread_attr_setname_np这个限制。具体需求可以拆解为:1基本线程创建流程(用户明确要求pthread示例)2线程属性设置(特别是名字设置这个易错点)3资源回收机制(避免线程泄漏)考虑到native开发的实际场景,需要强调两个重点:-pthread_create的返回值检查(Android上容易因权限问题失败)-分离状态设置(DETACHEDvsJOINABLE)对资源管理的影响用户提到“示例”,那就应该提供完整可编译的代码片段。但要注意代码必须包含错误处理,因为引用[2]特别强调了设置线程名可能失效的情况,这正是Android开发的常见痛点。突然想到还有个隐含需求:用户可能需要对比pthread和std::thread的优劣。虽然问题没明说,但既然引用[1]提到std::thread,或许后续会有相关疑问。这次回复里先埋个伏笔比较合适。安全方面要注意:在线程函数里访问JNIEnv是危险操作,但用户没问就不展开,避免混淆核心问题。不过示例代码里的全局字符串传参需要加volatile修饰吗?看来用结构体指针传参更稳妥。</think> 好的,这是在Android Native层(C++)使用POSIX Threads (`pthread`) 创建线程的详细示例和说明。结合您提供的引用材料,我将重点突出Android Native开发中的关键点和注意事项。 ### 核心函数:`pthread_create` POSIX标准的核心线程创建函数是 `pthread_create`,其原型如下: ```cpp #include <pthread.h> int pthread_create(pthread_t* thread, const pthread_attr_t* attr, // 线程属性,可为NULL void* (*start_routine)(void*), // 线程函数指针 void* arg); // 传递给线程函数的参数 ``` ### 完整的创建、运行和回收示例 ```cpp #include <pthread.h> #include <unistd.h> // for sleep(), usleep() #include <android/log.h> // Android日志输出 #define TAG "NativeThreading" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) // 线程函数 - 必须符合 void* (*)(void*) 签名 void* thread_function(void* arg) { const char* thread_name = static_cast<const char*>(arg); // 接收传入的参数 LOGD("线程 '%s' 开始执行 - TID: %d", thread_name, gettid()); // 模拟工作 (计算、I/O等) for (int i = 0; i < 5; ++i) { LOGD("'%s' - 任务进度: %d/5", thread_name, i+1); usleep(500000); // 休眠500毫秒 (微秒单位) } LOGD("线程 '%s' 执行完毕", thread_name); return nullptr; // 线程返回值,这里无具体意义 } int main() { pthread_t thread_id; const char* thread_name = "WorkerThread#1"; LOGD("主线程 (PID: %d, TID: %d) 即将创建新线程", getpid(), gettid()); // 创建线程 (属性用默认值NULL, 传入线程名) int result = pthread_create(&thread_id, nullptr, thread_function, (void*)thread_name); if (result != 0) { LOGD("线程创建失败! 错误码: %d", result); return -1; } LOGD("成功创建线程,ID: %lu", (unsigned long)thread_id); // 注意打印方式 // 主线程继续自己的工作... sleep(1); LOGD("主线程继续执行其他任务..."); // 等待子线程结束(回收资源) - 必需步骤! void* thread_return; result = pthread_join(thread_id, &thread_return); if (result != 0) { LOGD("等待线程结束失败! 错误码: %d", result); } else { LOGD("已回收线程 '%s' 资源", thread_name); } LOGD("程序正常退出"); return 0; } ``` ### 关键点解释 (结合引用材料) 1. **线程函数 (`start_routine`):** * 必须定义为 `void* function_name(void* arg)` 的格式。 * `void* arg` 用于接收 `pthread_create` 的第四个参数(`arg`)传递进来的数据。可以传递任何数据的指针(如结构体、对象指针、基本类型的指针等)。示例中传递了一个 `const char*`。 * 返回值也是 `void*`。如果不需要返回值,返回 `nullptr` 即可。需要时,可以返回复杂结构的指针。调用 `pthread_join` 可以捕获这个返回值。 * 避免直接使用 JNIEnv 指针(它是线程相关的),如需使用 JNI,必须通过 `AttachCurrentThread`/`GetEnv` 获取当前线程的 `JNIEnv`。 2. **线程标识符 (`pthread_t thread_id`):** * 用于唯一标识一个线程。 * `pthread_create` 成功时,将在此变量中存储新线程的 ID。 * 用于后续的线程操作(如 `pthread_join`, `pthread_cancel`, 设置线程属性等)。注意其打印方式可能需要类型转换 `(unsigned long)`。 3. **线程属性 (`pthread_attr_t attr`):** * 用于设置新线程的属性(栈大小、调度策略、分离状态等)。如果使用默认值(NULL),大部分属性符合预期。 * **设置线程名(避免 `PR_SET_NAME` 失效):** 如引用[2]所述,在 Android 上通过 `prctl(PR_SET_NAME, ...)` 设置 native 线程名有时会失效。更可靠的做法是在线程函数*内部*调用 `pthread_setname_np` (Android NDK API Level 9+ 支持): ```cpp void* thread_function(void* arg) { pthread_setname_np(pthread_self(), "MyWorker"); // ... 线程工作 ... } ``` 4. **连接线程 (`pthread_join`):** * **关键步骤!** 在子线程创建后,主线程(或父线程)通常需要调用 `pthread_join` 来等待子线程终止。 * `pthread_join` **阻塞**调用它的线程,直到指定的线程终止。 * 回收线程的资源(如退出状态值、堆栈内存等),**防止资源泄露**。 * 第二个参数 `thread_return` 是一个指向 `void*` 的指针,用于接收线程函数的返回值。如果不需要返回值,可以传入 `NULL`。 5. **分离线程 (`pthread_detach`):** * 如果**不需要**等待线程结束或获取其返回值(例如后台任务),可以将线程设置为*分离(detached)*状态。 * 方式: * 创建时设置属性:`pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);` 然后传入 `attr`。 * 创建后分离:`pthread_detach(thread_id);` * 分离线程终止时,其资源会自动被系统回收。**不能对分离线程调用 `pthread_join`**。 * 避免忘记 `pthread_join` 导致资源泄露的好方法,适用于纯粹的后台任务。 6. **Android 注意事项:** * **使用 `__android_log_print`:** 在 Native 代码中使用 `android/log.h` 提供的日志宏(如示例中的 `LOGD`)进行调试输出,比 `std::cout` 更方便在 `logcat` 中查看。 * **线程名设置可靠性:** 如引用[2]强调,在线程函数**内部**调用 `pthread_setname_np` 比在外部通过 `prctl` 设置更可靠,特别是在某些 Android 版本上。 * **JNI 交互:** 在线程中调用 Java 方法时,必须正确获取属于当前 `pthread` 的 `JNIEnv`。通常做法是: 1. 在创建线程前获取全局的 `JavaVM` 指针(通过 `JNI_OnLoad` 或作为参数传递)。 2. 在线程函数开始部分,使用 `AttachCurrentThread` 或 `GetEnv` 获取当前线程的 `JNIEnv`。 3. 在线程函数结束前(或不再需要调用 Java 时),使用 `DetachCurrentThread`。 * **NDK 版本:** 确保使用的 `pthread` 函数在项目配置的 NDK 版本和目标 API Level 上都支持。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值