上一篇文章介绍了JNI开发的基础知识,但是大多数同学在JNI开发中还是会遇到一些问题,我们选择一些问题给大家分析一下,希望对大家有些帮助。
JNIEnv和线程相关,一个线程对应一个JNIEnv,如果想要切换到当前线程的JNIEnv,首先通过JavaVM获取JNIEnv,然后调用JavaVM的AttachCurrentThread来切换到对应线程的JNIEnv,使用完了之后还要DetachCurrentThread切走。JNI开发中经常需要切换线程,那我们是不是要频繁地AttachCurrentThread和DetachCurrentThread。通常有两种方式来处理这种问题。
每个线程缓存JNIEnv
每个线程都缓存自己的JNIEnv实例表明如果这个线程attach到JavaVM的时候就将获取的JNIEnv保存起来,等会销毁这个线程的时候才将JNIEnv实例DetachCurrentThread销毁。webrtc中就有一个很好的例子:https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/android/src/jni/jvm.cc,此文件包含几个函数:
每个线程Attach的时候调用此函数,注意调用了pthread_setspecific会将JNIEnv实例和当前线程绑定,然后通过pthread_getspecific获取存储的JNIEnv实例。
// Return a |JNIEnv*| usable on this thread. Attaches to `g_jvm` if necessary.
JNIEnv* AttachCurrentThreadIfNeeded() {
JNIEnv* jni = GetEnv();
if (jni)
return jni;
RTC_CHECK(!pthread_getspecific(g_jni_ptr))
<< "TLS has a JNIEnv* but not attached?";
std::string name(GetThreadName() + " - " + GetThreadId());
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6;
args.name = &name[0];
args.group = nullptr;
// Deal with difference in signatures between Oracle's jni.h and Android's.
#ifdef _JAVASOFT_JNI_H_ // Oracle's jni.h violates the JNI spec!
void* env = nullptr;
#else
JNIEnv* env = nullptr;
#endif
RTC_CHECK(!g_jvm->AttachCurrentThread(&env, &args))
<< "Failed to attach thread";
RTC_CHECK(env) << "AttachCurrentThread handed back NULL!";
jni = reinterpret_cast<JNIEnv*>(env);
RTC_CHECK(!pthread_setspecific(g_jni_ptr, jni)) << "pthread_setspecific";
return jni;
}
JNI_OnLoad初始化的时候会注册一个函数:如果发现当前线程退出的情况下,就是调用Detach和当前线程解绑。
static void ThreadDestructor(void* prev_jni_ptr) {
// This function only runs on threads where `g_jni_ptr` is non-NULL, meaning
// we were responsible for originally attaching the thread, so are responsible
// for detaching it now. However, because some JVM implementations (notably
// Oracle's http://goo.gl/eHApYT) also use the pthread_key_create mechanism,
// the JVMs accounting info for this thread may already be wiped out by the
// time this is called. Thus it may appear we are already detached even though
// it was our responsibility to detach! Oh well.
if (!GetEnv())
return;
RTC_CHECK(GetEnv() == prev_jni_ptr)
<< "Detaching from another thread: " << prev_jni_ptr << ":" << GetEnv();
jint status = g_jvm->DetachCurrentThread();
RTC_CHECK(status == JNI_OK) << "Failed to detach thread: " << status;
RTC_CHECK(!GetEnv()) << "Detaching was a successful no-op???";
}
static void CreateJNIPtrKey() {
RTC_CHECK(!pthread_key_create(&g_jni_ptr, &ThreadDestructor))
<< "pthread_key_create";
}
jint InitGlobalJniVariables(JavaVM* jvm) {
RTC_CHECK(!g_jvm) << "InitGlobalJniVariables!";
g_jvm = jvm;
RTC_CHECK(g_jvm) << "InitGlobalJniVariables handed NULL?";
RTC_CHECK(!pthread_once(&g_jni_ptr_once, &CreateJNIPtrKey)) << "pthread_once";
JNIEnv* jni = nullptr;
if (jvm->GetEnv(reinterpret_cast<void**>(&jni), JNI_VERSION_1_6) != JNI_OK)
return -1;
return JNI_VERSION_1_6;
}
这样做的好处比较明显,就是每个线程绑定唯一的JNIEnv实例,不用频繁地Attach和Detach。大型的项目可以这样采用,比较方便。
使用时获取JNIEnv
还有一种方式就是每个需要使用JNIEnv的实例的时候Attach,用完之后立即Detach。下面就是例子,每次调用jni_get_env获取当前线程的JNIEnv实例,使用完之后,调用jni_detach_thread_env解绑。
JavaVM *media_jni_get_java_vm() {
void *vm;
pthread_mutex_lock(&lock);
vm = java_vm;
pthread_mutex_unlock(&lock);
return vm;
}
int jni_get_env(JNIEnv **env) {
JavaVM *vm = media_jni_get_java_vm();
int ret = (*vm)->GetEnv(vm, (void **) env, JNI_VERSION_1_6);
if (ret == JNI_EDETACHED) {
if ((*vm)->AttachCurrentThread(vm, env, NULL) != JNI_OK) {
LOGE("%s Failed to attach the JNI environment to the current thread", __func__);
*env = NULL;
return -10;
}
}
return ret;
}
void jni_detach_thread_env() {
JavaVM *vm = media_jni_get_java_vm();
(*vm)->DetachCurrentThread(vm);
}
这样的好处是比较容易,但是缺点是Attach和Detach过分频繁,可能会影响性能。选择第二种方式,有一个比较容易出错的点。因为JNIEnv不是缓存下来的,每次获取的JNIEnv实例都是不同的,如果需要调用Java层类中static方法,使用第二种方法调用会出现ClassNotFound的问题,为什么会出现这种现象了,因为JNIEnv变化了,对应的ClassLoader变化了,所以多次调用static方法,会出现前后的ClassLoader不一样,之前的类已经被前一个ClassLoader加载了,后面一个ClassLoader肯定就找不到了。
如何在JNI中访问Bitmap数据
如果想让Bitmap中的数据在native层也能访问到,怎么做呢?
通常也有两种方法,方法一当然是直接传输Bitmap对象到native层,JNI中会收到一个jobject对象。将Bitmap中的数据存储到uint8_t数组中,直接访问。
jobject j_bitmap = env->CallObjectMethod(j_album_info, env->GetMethodID(clazz, "getImage", "()Landroid/graphics/Bitmap;"));
uint8_t *image_data;
int64_t size = 0;
if (j_bitmap != nullptr) {
AndroidBitmapInfo info;
int result = AndroidBitmap_getInfo(env, j_bitmap, &info);
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
LOGE("get bitmap info failed, result=%d", result);
}
result = AndroidBitmap_lockPixels(env, j_bitmap, reinterpret_cast<void **>(&image_data));
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
LOGE("lock pixel failed, result=%d", result);
}
size = info.stride * info.height;
result = AndroidBitmap_unlockPixels(env, j_bitmap);
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
LOGE("unlock pixel failed, result=%d", result);
}
}
还有一种方法是在Java层将Bitmap中数据写入ByteBuffer,native层访问ByteBuffer。底层通过GetDirectBufferAddress来访问ByteBuffer地址上的数据。
Bitmap bitmap = BitmapFactory.decodeFile(path);
if (bitmap != null) {
mWidth = bitmap.getWidth();
mHeight = bitmap.getHeight();
int byteCount = bitmap.getByteCount();
ByteBuffer buffer = ByteBuffer.allocateDirect(byteCount);
bitmap.copyPixelsToBuffer(buffer);
buffer.flip();
return buffer;
}
你还遇到哪些JNI的问题,可以在留言区交流一下。