欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

研究了一下Android JNI,有几个知识点不太懂,

来源: 开发者 投稿于  被查看 13361 次 评论:72

研究了一下Android JNI,有几个知识点不太懂,


本文转载自微信公众号「程序喵大人」,作者程序喵大人 。转载本文请联系程序喵大人公众号。

Java线程与Native(OS)线程的区别

联系:Java线程其实是一层OS线程的封装,本质上就是OS线程。

区别:

Java线程可以直接拿到JNIEnv,OS线程需要先attach到JVM,才可以拿到JNIEnv。

  1. jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args); 

Java线程可以FindClass成功,OS线程则FindClass失败,原因是两者的ClassLoader不同,OS线程AttachCurrentThread后持有的ClassLoader是系统的ClassLoader,如果想要FindClass成功,需要在JNI_Onload时获取一份当前库的ClassLoader保存起来,下次FindClass时使用此ClassLoader去操作。

  1. static jobject g_class_loader = NULL; 
  2. static jmethodID g_find_class_method = NULL; 
  3. void on_load() { 
  4.     JNIEnv *env = get_jni_env(); 
  5.     if (!env) { 
  6.         return; 
  7.     } 
  8.     jclass capture_class = (*env)->FindClass(env, "com/captureandroid/BMMCaptureEngine"); 
  9.     jclass class_class = (*env)->GetObjectClass(env, capture_class); 
  10.     jclass class_loader_class = (*env)->FindClass(env, "java/lang/ClassLoader"); 
  11.     jmethodID class_loader_mid = (*env)->GetMethodID(env, class_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); 
  12.     jobject local_class_loader = (*env)->CallObjectMethod(env, capture_class, class_loader_mid); 
  13.     g_class_loader = (*env)->NewGlobalRef(env, local_class_loader); 
  14.     g_find_class_method = 
  15.         (*env)->GetMethodID(env, class_loader_class, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); 
  16.  
  17. jclass find_class(const char *name) { 
  18.     JNIEnv *env = bmm_util_get_jni_env(); 
  19.     if (!env) { 
  20.         return NULL; 
  21.     } 
  22.     jclass ret = (*env)->FindClass(env, name); 
  23.     jthrowable exception = (*env)->ExceptionOccurred(env); 
  24.     if (exception) { 
  25.         (*env)->ExceptionClear(env); 
  26.         jstring name_str = (*env)->NewStringUTF(env, name); 
  27.         ret = (jclass)(*env)->CallObjectMethod(env, g_class_loader, g_find_class_method, name_str); 
  28.         (*env)->DeleteLocalRef(env, name_str); 
  29.     } 
  30.     return ret; 

JNI的作用

贴出别人翻译的的一段话:

JNI最重要的设计目标就是在不同操作系统上的JVM之间提供二进制兼容,做到一个本地库不需要重新编译就可以运行不同的系统的JVM上面。为了达到这一点儿,JNI设计时不能关心JVM的内部实现,因为JVM的内部实现机制在不断地变,而我们必须保持JNI接口的稳定。JNI的第二个设计目标就是高效。我们可能会看到,有时为了满足第一个目标,可能需要牺牲一点儿效率,因此,我们需要在平台无关和效率之间做一些选择。最后,JNI必须是一个完整的体系。它必须提供足够多的JVM功能让本地程序完成一些有用的任务。JNI不能只针对一款特定的JVM,而是要提供一系列标准的接口让程序员可以把他们的本地代码库加载到不同的JVM中去。有时,调用特定JVM下实现的接口可以提供效率,但更多的情况下,我们需要用更通用的接口来解决问题。

JNIEnv和JavaVM

就是个函数指针。

下图是JNIEnv的指针结构:

JNIEnv其实是一个指向本地线程数据的接口指针,指针里面包含指向函数接口的指针,每一个接口函数在这表中都有一个预定义的偏移位置,类似C++虚函数表。

代码如下:

  1. typedef const struct JNINativeInterface *JNIEnv;  
  2.  
  3. struct JNINativeInterface { 
  4.     void*       reserved0; 
  5.     void*       reserved1; 
  6.     void*       reserved2; 
  7.     void*       reserved3; 
  8.  
  9.     jint        (*GetVersion)(JNIEnv *); 
  10.  
  11.     jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, 
  12.                         jsize); 
  13.     jclass      (*FindClass)(JNIEnv*, const char*); 
  14.     jobject     (*AllocObject)(JNIEnv*, jclass); 
  15.     jobject     (*NewObject)(JNIEnv*, jclass, jmethodID, ...); 
  16.     jobject     (*NewObjectV)(JNIEnv*, jclass, jmethodID, va_list); 
  17.     jobject     (*NewObjectA)(JNIEnv*, jclass, jmethodID, const jvalue*); 
  18.     ... 
  19. }; 
  20. JavaVM类似 
  21. struct JNIInvokeInterface { 
  22.     void*       reserved0; 
  23.     void*       reserved1; 
  24.     void*       reserved2; 
  25.  
  26.     jint        (*DestroyJavaVM)(JavaVM*); 
  27.     jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*); 
  28.     jint        (*DetachCurrentThread)(JavaVM*); 
  29.     jint        (*GetEnv)(JavaVM*, void**, jint); 
  30.     jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*); 
  31. }; 
  32.  
  33. typedef const struct JNIInvokeInterface* JavaVM; 

知识点1:为什么使用函数表而不是写死某些函数项?

可将JNI命名空间与本地代码分离,一个虚拟机可以提供多个版本的JNI函数表,用于不同场景。例如,虚拟机可支持两种JNI函数表:

一个用于调试,做较多的错误检查。

一个用于发布,做较少的错误检查,更高效。

知识点2:JNIEnv是thread-local,只在当前线程有效,Native方法不能将JNIenv从当前线程传递到另一个线程。不能跨线程使用JNIEnv。

知识点3:线程间虽然不共享JNIEnv,但是共享JavaVM,然后可以通过GetEnv获取到当前线程的JNIEnv。

jint GetEnv(JavaVM *vm, void **env, jint version);

知识点4:Native方法接收JNI接口指针作为参数。虚拟机保证在同一个线程传入Native方法的是相同的JNIEnv。如果不同线程调用Native方法,传入他们的JNIEnv不同。但JNIEnv间接指向的函数表在多个线程间是共享的。

知识点5:为什么在C语言中调用Native方法需要将JNIEnv当作参数传递,而C++中却不需要?

  1. // C语言 
  2. jstring model_path = (*env)->NewStringUTF(env, path); 
  3. // C++ 
  4. jstring model_path = env->NewStringUTF(path); 

前面列出的JNIEnv是C语言形式,Java还单独为C++封装了一层JNIEnv,简化版代码:

  1. struct _JNIEnv { 
  2.     /* do not rename this; it does not seem to be entirely opaque */ 
  3.     const struct JNINativeInterface* functions; 
  4.  
  5. #if defined(__cplusplus) 
  6.  
  7.     jint GetVersion() 
  8. { return functions->GetVersion(this); } 
  9.  
  10.     jclass FindClass(const char* name) 
  11. { return functions->FindClass(this, name); } 
  12. #endif 

其实本质上还是调用的C语言那种形式的接口。

JNI中数据如何传递

这里不详细介绍了,大体就是int,float这种基本类型采用拷贝,对象和byte数组等使用引用形式,所以其实Java层的byte字节流数据传到Native层基本不耗时,不会发生拷贝。

还有些GlobalReference、LocalReference以及为什么要Delete LocalReference的这类知识点,这些比较基础,就不介绍了,估计大家也都懂。

推荐阅读

https://www.cnblogs.com/kexinxin/p/11689641.html

ndk官方文档

https://developer.android.com/ndk/guides

参考资料

http://luori366.github.io/JNI_doc/jni_design_theory.html

https://www.cnblogs.com/kexinxin/p/11689641.html

https://developer.android.com/ndk/guides

用户评论