C++ 线程多次回调 JNI 到 Java 时的一个坑

5/28/2019 posted in  Android comments

JNI 是个好东西,但是用起来千万要小心。

0x00 Background

最近刚刚接手开始做一个新项目的 Android client,主要负责 Video rendering 部分的问题,现有的代码里已经有了一些代码,主要的逻辑是从 Java 层向底层 addVideoRender(render),再通过底层的 callback 回调 render.renderFrame() 方法,到 Java 层进行渲染。

发现的问题是在进行渲染的时候,前8-10s一切都很正常,但是很快 video frame 就开始不动了,打的 Log 发现,每一次 C++ callback 上来的时候,都会创建一个新的 Java 线程,然后抛到 Java 层,10s 基本上就 2000+ 个线程了,所以一切就都完蛋了。

0x01 Root cause

问题的根源在于,每一次回调的时候,都需要利用 JNI env 去 call Java method,而调用之前需要先获取 JVM env,然后 attach thread。
大概的流程是这样的。

    // Get JNI env
    JNIEnv* env = nullptr;
    jvm->GetEnv((void**)&env, JNI_VERSION_1_6);
    
    // Attach thread
    jvm->AttachCurrentThread(&env, nullptr);
    
    // Call Java method
    env->CallVoidMethodV(jobject, method, vl);
    
    // Detach thread
    jvm->DetachCurrentThread();

很显然,每一次调用都会走这样的流程,而底层 C++ 的调用上来的时候都是在同一个线程里,于是在 JNI 层,同一个 C++ 线程被创建了 N 多个不同的 Java 线程,很快资源就消耗干净了。

0x02 Solution

当然最简单的方法是,大不了我就不 Detach 了。

··· 千万别这么干 ···

这样干的后果就是,的确同一个线程不会创建多个了,但是没有人来释放已经销毁的线程了,当 C++ 中线程已经销毁,但是 JNI 这边依旧还是会持有,这样会导致不可预期的问题。

一个比较好的方式是,在创建线程的时候放一个我们自己的析构器,在这个析构函数中进行 Detach 的操作。而调用的时候只负责创建和调用,不负责销毁,当 C++ 的线程销毁的时候,这个线程也就自然被 Detach 了。 代码参考 WebRTC