最近项目有native层的开发需求。以前没有接触过,正好练手。
最先遇到的是JNI层的线程问题。
有这么一个需求: 从网络上获取视频流的帧数据,每一帧都要回调到java层去处理h264的解码,以及播放。所以调用非常频繁,10ms到300ms调用一次。
刚开始是下面这样的代码:
// 负责回调java方法把帧数据传上去。
void OnRecvVideoData(void *observer, h264_decode_struct *data) {
jobject obj = (jobject) observer;
JNIEnv *jniEnv = NULL;
g_JavaVM->AttachCurrentThread(&jniEnv, NULL);
jclass cls = jniEnv->GetObjectClass(obj);
jmethodID onRecvVideoDataM = jniEnv->GetMethodID(cls, "onRecvVideoData", "([BIJJ)V");
jniEnv->DeleteLocalRef(cls);
jbyteArray barr = jniEnv->NewByteArray(data->data_len);
if (barr == NULL) return;
jniEnv->SetByteArrayRegion(barr, (jint) 0, data->data_len, (jbyte *) data->data);
jniEnv->CallVoidMethod(obj, onRecvVideoDataM, barr, data->frame_type,
data->pts, data->pts);
jniEnv->DeleteLocalRef(barr);
g_JavaVM->DetachCurrentThread();
}
导致的问题是:视频不流畅,而且log里显示GC在疯狂的工作 !
很好奇到底是怎么回事。然后猜测是不是每一次回调,在java层都会产生新的线程?!
@NativeCallback
public void onRecvVideoData(byte[] nal, int nalType, long dts, long pts) {
if (mProvider == null)
return;
Log.d(TAG, "name:" + Thread.currentThread().getName() + " id:" + Thread.currentThread().getId());
mProvider.onVideoData(nalPacket);
}
经过验证后,真的发现每一次回调线程id都不一样! 怪不得gc log在疯狂的输出。
接下来就想办法解决这个问题。
调试发现:
JNIEnv *jniEnv = NULL;
g_JavaVM->AttachCurrentThread(&jniEnv, NULL);
//在这中间多次调用java层方法,线程id都是一样的。
g_JavaVM->DetachCurrentThread();
也就是说和JNIEnv有关。查了JNI的官方文档,说是每一个想回调java层的c/c++线程, 都需要JVM给生成一个JNIEnv与c/c++线程关联,这样才能在c/c++线程的方法里用JNIEnv调用方法。而新的JNIEnv,JVM都会为java层生成一个新的线程。attach就是c/c++线程关联新生成的java线程。
好像有点绕,简单说就是这样的对应关系: 新c/c++线程 -----》新JNIEnv-----》新java线程。
知道了原因后,把代码改成下面就好了:
//生成JNIEnv, 和java方法句柄onRecvVideoDataM。在线程开始的时候调用。
extern "C" void
jvm_attach_process(void *observer, void **jni_env, void **data_callback) {
LOGD("video_attach_process start");
jobject obj = (jobject) observer;
JNIEnv *jniEnv = NULL;
int state = g_JavaVM->GetEnv(jni_env, JNI_VERSION_1_6);
if (state == JNI_EDETACHED) {
g_JavaVM->AttachCurrentThread(&jniEnv, NULL);
}
jclass cls = jniEnv->GetObjectClass(obj);
jmethodID onRecvVideoDataM = jniEnv->GetMethodID(cls, "onRecvVideoData", "([BIJJ)V");
*data_callback = onRecvVideoDataM;
jniEnv->DeleteLocalRef(cls);
*jni_env = jniEnv;
LOGD("video_attach_process end");
}
//删除jniEnv
extern "C" void
jvm_detach_process() {
g_JavaVM->DetachCurrentThread();
}
//线程方法
static THREAD_RETVAL rtp_video_thread(void *arg) {
Callback callbacks = *((Callback*)arg);
void *jni_env = NULL;
void *data_callback = NULL;
callbacks.jvm_attach_process(callbacks.cls, &jni_env, &data_callback);
//接收每一帧视频数据h264_data
//最终回调到OnRecVideoData()。 回调到java层的时候需要jni_env。
callbacks.video_process(callbacks.cls, data_callback, jni_env, &h264_data);
callbacks.jvm_detach_process();
return 0;
}
- JVM的GetEnv(jni_env, JNI_VERSION_1_6)方法是判断jni_env有没有被attach上,如果没有被attach的话jni_env会被赋值为NULL。
- JVM的AttachCurrentThread(&jniEnv, NULL)方法是生成一个java层的线程,和与之相对应的jniEnv。
- JVM的DetachCurrentThread(&jniEnv, NULL)方法就是关闭之前AttachCurrentThread生成的java层线程,和delete 掉jniEnv。
- jvm_attach_process函数,生成好JNIEnv和java方法句柄onRecvVideoDataM。在视频流线程开始的时候调用,这样就是把c/c++线程attach到新生成的java线程。
- jvm_detach_process()函数,是在视频流线程结束的时候调用,相当于关闭java线程,delete掉JNIEnv。
还有一个问题,如果在系统自己生成的JNI函数里回调java,那么函数传进来的JNIEnv ,与之相对应的是调用native方法的java线程。所以在java层会阻塞。
JNICALL
Java_com_test_server_start(JNIEnv *env, jobject object) {
}