💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] # JNI基础 上面一节主要描述了系统中Java层和Native层交互和实现,并没有对JNI的基础理论,流程进行分析 ## JNI命名规则 JNI方法名规范 : ~~~ 返回值 + Java前缀 + 全路径类名 + 方法名 + 参数① JNIEnv + 参数② jobject + 其它参数 ~~~ 简单的一个例子,返回一个字符串 ~~~ extern "C" JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI(JNIEnv *env, jclass jclass1) { LOGD("##### from c"); return env->NewStringUTF("Hello JNI"); } ~~~ * 返回值:jstring * 全路径类名:com\_yeungeek\_jnisample\_NativeHelper * 方法名:stringFromJNI ## JNI开发流程 * 在Java中先声明一个native方法 * 编译Java源文件javac得到.class文件 * 通过javah -jni命令导出JNI的.h头文件 * 使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法。) * 将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib) * 通过Java命令执行Java程序,最终实现Java调用本地代码。 ## 数据类型 ### 基本数据类型 | Signature | Java | Native | | --- | --- | --- | | B | byte | jbyte | | C | char | jchar | | D | double | jdouble | | F | float | jfloat | | I | int | jint | | S | short | jshort | | J | long | jlong | | Z | boolean | jboolean | | V | void | jvoid | ### 引用数据类型 | Signature | Java | Native | | --- | --- | --- | | L+classname +; | Object | jobject | | Ljava/lang/String; | String | jstring | | \[L+classname +; | Object\[\] | jobjectArray | | Ljava.lang.Class; | Class | jclass | | Ljava.lang.Throwable; | Throwable | jthrowable | | \[B | byte\[\] | jbyteArray | | \[C | char\[\] | jcharArray | | \[D | double\[\] | jdoubleArray | | \[F | float\[\] | jfloatArray | | \[I | int\[\] | jintArray | | \[S | short\[\] | jshortArray | | \[J | long\[\] | jlongArray | | \[Z | boolean\[\] | jbooleanArray | ## 方法签名 JNI的方法签名的格式: ~~~ (参数签名格式...)返回值签名格式 复制代码 ~~~ demo的native 方法: ~~~ public static native java.lang.String stringFromJNI(); 复制代码 ~~~ 可以通过javap命令生成方法签名``: ~~~ ()Ljava/lang/String; 复制代码 ~~~ # JNI原理 Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI环境中创建JVM的函数为JNI\_CreateJavaVM。 JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”,两者本质上都是指向函数表的二级指针。 ## JavaVM JavaVM是Java虚拟机在JNI层的代表,JavaVM 提供了“调用接口”函数,您可以利用此类函数创建和销毁 JavaVM。理论上,每个进程可以包含多个JavaVM,但AnAndroid只允许每个进程包含一个JavaVM。 ## JNIEnv JNIEnv是一个线程相关的结构体,该结构体代表了Java在本线程的执行环境。JNIEnv 提供了大多数 JNI 函数。您的原生函数均会接收 JNIEnv 作为第一个参数。 JNIEnv作用: * 调用Java函数 * 操作Java代码 JNIEnv定义(jni.h): `libnativehelper/include/nativehelper/jni.h` ~~~ #if defined(__cplusplus) typedef _JNIEnv JNIEnv; typedef _JavaVM JavaVM; #else typedef const struct JNINativeInterface* JNIEnv; typedef const struct JNIInvokeInterface* JavaVM; #endif 复制代码 ~~~ 定义中可以看到JavaVM,Android中一个进程只会有一个JavaVM,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构 ![](https://img.kancloud.cn/18/93/1893602172709cde07c48970ba284cc5_834x449.png) ## 注册JNI函数 Java世界和Native世界的方法是如何关联的,就是通过JNI函数注册来实现。JNI函数注册有两种方式: ### 静态注册 这种方法就是通过函数名来找对应的JNI函数,可以通过`javah`命令行来生成JNI头文件 ~~~ javah com.yeungeek.jnisample.NativeHelper 复制代码 ~~~ 生成对应的`com_yeungeek_jnisample_NativeHelper.h`文件,生成对应的JNI函数,然后在实现这个函数就可以了 ~~~ /* * Class: com_yeungeek_jnisample_NativeHelper * Method: stringFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI (JNIEnv *, jclass); 复制代码 ~~~ 静态注册方法中,Native是如何找到对应的JNI函数,在[JNI查找方式](#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F "#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F")中介绍系统的流程,并没有详细说明静态注册的查找。这里简单说明下这个过程(以上面的声明为例子s): 当Java调用native stringFromJNI函数时,会从对应JNI库中查找`Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI`函数,如果没有找到,就会报错。 静态注册方法,就是根据函数名来关联Java函数和JNI函数,JNI函数需要遵循特定的格式,这其中就有一些缺点: * 声明了native方法的Java类,需要通过`javah`来生成头文件 * JNI函数名称非常长 * 第一次调用native函数,需要通过函数名来搜索关联对应的JNI函数,效率比较低 如何解决这些问题,让native函数,提前知道JNI函数,就可以解决这个问题,这个过程就是动态注册。 ### 动态注册 动态注册在前面的Camera例子中,已经有涉及到,JNI函数`classInit`的声明。 ~~~ static const JNINativeMethod gCameraMetadataMethods[] = { // static methods { "nativeClassInit", "()V", (void *)CameraMetadata_classInit }, //和Java层nativeClassInit()对应 ...... } 复制代码 ~~~ JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义: ~~~ typedef struct { const char* name; //Java层native函数名 const char* signature; //Java函数签名,记录参数类型和个数,以及返回值类型 void* fnPtr; //Native层对应的函数指针 } JNINativeMethod; 复制代码 ~~~ 在[JNI查找方式](#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F "#JNI%E6%9F%A5%E6%89%BE%E6%96%B9%E5%BC%8F")说到,JNI注册的两种时间,第一种已经介绍过了,我们自定义的native函数,基本都是会使用`System.loadLibrary(“xxx”)`,来进行JNI函数的关联。 #### loadLibrary(Android7.0) ~~~ public static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); } 复制代码 ~~~ 调用到Runtime(libcore/ojluni/src/main/java/java/lang/Runtime.java)的loadLibrary0方法: ~~~ synchronized void loadLibrary0(ClassLoader loader, String libname) { ...... String libraryName = libname; if (loader != null) { String filename = loader.findLibrary(libraryName); if (filename == null) { // It's not necessarily true that the ClassLoader used // System.mapLibraryName, but the default setup does, and it's // misleading to say we didn't find "libMyLibrary.so" when we // actually searched for "liblibMyLibrary.so.so". throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\""); } //doLoad String error = doLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } //loader 为 null ...... for (String directory : getLibPaths()) { String candidate = directory + filename; candidates.add(candidate); if (IoUtils.canOpenReadOnly(candidate)) { String error = doLoad(candidate, loader); if (error == null) { return; // We successfully loaded the library. Job done. } lastError = error; } } ...... } 复制代码 ~~~ #### doLoad ~~~ private String doLoad(String name, ClassLoader loader) { //调用 native 方法 synchronized (this) { return nativeLoad(name, loader, librarySearchPath); } } 复制代码 ~~~ #### nativeLoad 进入到虚拟机代码`/libcore/ojluni/src/main/native/Runtime.c` ~~~ JNIEXPORT jstring JNICALL Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) { return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath); } 复制代码 ~~~ 然后调用`JVM_NativeLoad`,JVM\_NativeLoad方法申明在jvm.h中,实现在`OpenjdkJvm.cc(/art/runtime/openjdkjvm/OpenjdkJvm.cc)` ~~~ JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) { ScopedUtfChars filename(env, javaFilename); if (filename.c_str() == NULL) { return NULL; } std::string error_msg; { art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM(); bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, javaLibrarySearchPath, &error_msg); if (success) { return nullptr; } } // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF. env->ExceptionClear(); return env->NewStringUTF(error_msg.c_str()); } 复制代码 ~~~ #### LoadNativeLibrary 调用JavaVMExt的LoadNativeLibrary方法,方法在(art/runtime/java\_vm\_ext.cc)中,这个方法代码非常多,选取主要的部分进行分析 ~~~ bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader, jstring library_path, std::string* error_msg) { ...... bool was_successful = false; //加载so库中查找JNI_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功, //如果找到JNI_OnLoad就会调用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注册的函数, //所以如果采用动态注册就必须要实现JNI_OnLoad方法,否则调用java中申明的native方法时会抛出异常 void* sym = library->FindSymbol("JNI_OnLoad", nullptr); if (sym == nullptr) { VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]"; was_successful = true; } else { // Call JNI_OnLoad. We have to override the current class // loader, which will always be "null" since the stuff at the // top of the stack is around Runtime.loadLibrary(). (See // the comments in the JNI FindClass function.) ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride())); self->SetClassLoaderOverride(class_loader); VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]"; typedef int (*JNI_OnLoadFn)(JavaVM*, void*); JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym); //调用JNI_OnLoad方法 int version = (*jni_on_load)(this, nullptr); if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) { // Make sure that sigchain owns SIGSEGV. EnsureFrontOfChain(SIGSEGV); } self->SetClassLoaderOverride(old_class_loader.get()); } ...... } 复制代码 ~~~ 代码里的主要逻辑: * 加载so库中查找JNI\_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功 * 如果找到JNI\_OnLoad就会调用JNI\_OnLoad方法,JNI\_OnLoad方法中一般存放的是方法注册的函数 * 所以如果采用动态注册就必须要实现`JNI_OnLoad`方法,否则调用Java中的native方法时会抛出异常 ## jclass、jmethodID和jfieldID 如果要通过原生代码访问对象的字段,需要执行以下操作: 1. 使用 FindClass 获取类的类对象引用 2. 使用 GetFieldID 获取字段的字段 ID 3. 使用适当内容获取字段的内容,例如 GetIntField 具体的使用,放在第二篇文章中讲解 ## JNI的引用 JNI规范中定义了三种引用: * 局部引用(Local Reference) * 全局引用(Global Reference) * 弱全局引用(Weak Global Reference) ### 局部引用 也叫本地引用,在 JNI层函数使用的非全局引用对象都是Local Reference,最大的特点就是,JNI 函数返回后,这些声明的引用可能就会被垃圾回收 ### 全局引用 这种声明的对象,不会主动释放资源,不会被垃圾回收 ### 弱全局引用 一种特殊的全局引用,在运行过程中可能被回收,使用之前需要判断下是否为空 # 参考资料 [Android NDK-深入理解JNI](https://juejin.cn/post/6844903933375152136)