JNI 原理进阶

来源:互联网 发布:见过最漂亮的女生知乎 编辑:程序博客网 时间:2024/06/07 20:37

回顾

JNI 要点

  1. Java 要 load 库
  2. Java 要把方法声明为 native 的
  3. C 语言实现 Java 的 native 方法时,函数名有固定形式,可以用 javah 生成头文件得到函数名
  4. Java 调用 C 函数时,JVM 会自动传两个参数下去: 分别是 JNIEnv * 和 jobject 类型的

层:Java -> JNI -> Native

  • 分三层
    • 最上面是 Java 层
    • 中间是JIN层,C语言编写,以.so形式存在
    • 最下是Native层,C语言编写,以.so形式存在
  • 举例
    • Java(MediaScanner) —> JIN(libmedia_jin.so) —> Native(libmedia.so)
  • jni 层和 Native 层虽然都是 C/C++ 写的,但还是有区别的:
    • Native 层完全不知道有 Java 层的存在,是完完全全的C/C++语言写程序。
    • JNI 层的C/C++函数的第一个参数都是 JNIEnv* env,函数可以通过这个参数跟Java层交互。

JNIEnv

  • Java 传给 C 的第一个参数是 JNIEnv* 类型的,下面我们就看看 JNIEnv 是什么

JNIEnv 是什么

  • JNIEnv 是个结构体,这个结构体里,全都是函数指针,一共有300个左右。
  • 这些函数指针形如:
jclass      (*FindClass)(JNIEnv*, const char*);jint        (*CallIntMethodV)(JNIEnv*, jobject, jmethodID, va_list);void        (*CallVoidMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);void        (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);jbyteArray    (*NewByteArray)(JNIEnv*, jsize);const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
  • JNIEnv* 是 java 虚拟机传的
  • 也就是说是 java 虚拟机初始化了一个 JNIEnv 结构体,把300个函数指针都初始化了。
  • 但函数指针指向的都是C的函数,所以这是Java虚拟机干的事儿。Java虚拟机作为Java世界和C世界的桥梁,这也是体现之处。
  • 当你愉快的调用 (*env)->FindClass(env, "java/lang/String") 时,虽然这是一个C函数,但它是 java 虚拟机初始化的。
  • 总之, JNIEnv 是 Java 虚拟机为 C 层准备的一系列函数的集合。

C 和 C++ 中 JNIEnv 的区别

  • jni 层函数的第一个参数都是 JNIEnv* env, 无论C还是C++
  • 但是这个 JNIEnv 对于C和C++是不同的,看一下 jni.h 中的定义(来自 NDK r13b):
#if defined(__cplusplus)/* 在C++中,JNIEnv 等同于 _JNIEnv,而_JNIEnv是一个结构体,一会儿下面会有代码 */typedef _JNIEnv JNIEnv;     typedef _JavaVM JavaVM;#else/* 在C语言中,JNIEnv 是个指向结构体的指针,指向的是 JNINativeInterface 类型的结构体 */typedef const struct JNINativeInterface* JNIEnv;   typedef const struct JNIInvokeInterface* JavaVM;#endif...// 里面定义的全是函数指针 struct JNINativeInterface {    jclass  (*FindClass)(JNIEnv*, const char*);}...// 里面定义的全是函数指针 struct _JNIEnv {    // C++ 的 JNIEnv 是个 wrapper,其函数调用还是 JNINativeInterface 的函数    const struct JNINativeInterface_ *functions;    jclass FindClass(const char *name) {        return functions->FindClass(this, name);    }}
  • 区别1:对于C语言,使用 (*env)-> 来调用函数;对于 C++ 用 env->
  • 区别2:对于C语言,使用 (*env)->xxx(env, ...) 来调用函数;对于 C++ 用 env->(),即 C 语言第一个参数还要把 env 写上去。

动态注册

什么是动态注册

  • 目前我们知道,JNI 层的函数名,一定得叫 Java_包名_类名_函数名
  • 这是因为,当 Java 调用 native 方法时,会去 so 库里搜索,搜索的函数名就是这么约定的。
  • 如果不按这种规则命名,也有方法能让 Java 找到对应的 C/C++ 函数,就是动态注册
  • 动态注册是 JNI 层主动告诉 Java 层函数对应关系,让 Java 层不必去库里搜索。
  • 动态注册除了有缩短函数名,不借助 javah 工具这两个好处,还有提升效率的好处,因为可以避免搜索。

动态注册的原理

  • Java 在调用 C/C++ 之前,肯定要加载 C/C++ 的库,即 System.loadLibrary()
  • 在加载库的时候,JVM 会去被加载的库中寻找函数 JNI_OnLoad(JavaVM* jvm, void* reserved),如果找到了就调用它。它是一个 C/C++ 函数,工作在 JNI 层。
  • 我们就在这个函数里实现动态注册。即我们在 JNI 代码里实现这个函数,在函数体内完成动态注册。

JNI_OnLoad()

  • 在实现这个函数之前,先来看看它的参数:JNI_OnLoad(JavaVM* jvm, void* reserved)
  • 第二个参数从名字就能看出来,是保留参数,暂不讨论。
  • 第一个参数是 JavaVM* 类型的。而普通的 JNI 函数的第一个参数都是 JNIEnv* 类型的。

JavaVM 和 JNIEnv

  • JavaVM 是进程相关的,JNIEnv 是线程相关的
  • 在 Android 里,每个进程只有一个 JavaVM(DalvikVM) 的实例
  • Android 中每当一个 Java 线程第一次要调用本地 C/C++ 代码时,Dalvik 虚拟机实例会为该 Java 线程产生一个 JNIEnv* 指针;
  • Java 每条线程在和 C/C++ 相互调用时,JNIEnv* 是相互独立的,互不干扰,这就提升了并发执行时的安全性;
  • 当本地的 C/C++ 代码想获得当前线程所想要使用的 JNIEnv 时,可以使用 Dalvik VM 对象的 JavaVM* jvm->GetEnv() 方法,该方法即会返回当前线程所在的 JNIEnv*。
    JNI_OnLoad(JavaVM* jvm, void* reserved) 会把虚拟机对象传递到 JNI 层,这几乎是整个 JNI so 库唯一一次获得 JVM 指针的机会。
    所以一般正经的 Native Code 的 jni 层,都会实现 JNI_OnLoad() 函数,并且在函数里用全局变量把 JavaVM* 保存下来,以便以后使用。

关于Android

  • 可以不准确的理解为:一个 Android app 就是一个 Android Linux 上的进程
  • 在 Android 里,可以简单的理解为:一个进程对应一个 Dalvik 虚拟机。Java 的 dex 字节码和 c/c++ 的 so 库同时运行这个进程之内。
  • Dalvik 虚拟机当然已经实现了JNI标准,所以在 Dalvik 虚拟机加载 so 库时,会先调用 JNI_Onload()

动态注册的实现

  • Java 虚拟机已经准备好了函数,专门用于动态注册:(*env)->RegisterNatives(env, clazz, gMethods, numMethods)
  • 我们在 JNI_OnLoad() 里调用它即可
  • 其中 gMethods 是一个数组,数组里放的都是 JNINativeMethod 类型的变量
  • JNINativeMethod 这个结构体如下:
typedef struct{    //Java 中native函数名,不用包含路径,比如 processFile    const char* name;    //Java 函数的签名信息,用字符串表示,是参数类型和返回值类型的组合    const char* signature;    //JNI 层对应的函数的指针    void* fnPtr;}JNINativeMethod;
  • 这个结构体就是把 Java 层的函数名和 JNI 层的函数指针对应起来的
  • 所以,把实现了的 Java 层的函数的指针和 Java 层函数名对应写好,封装到这个结构体里
  • 再把所有这样的结构体放到数组 gMethods 里
  • 就可以用 RegisterNatives() 进行函数注册了
  • 因为 JNINativeMethod 结构体里只写 Java 里的函数名,不包含路径,所以不知道这个函数是哪个类的
  • 所以 RegisterNatives() 需要一个参数 clazz,这是 jclass 类型的,代表着java类
  • 注册过的函数就有了对应关系了,当 Java 层调用 native 函数时,直接就能找到 JNI 层的对应的函数指针了
  • JNINativeMethod 这个结构体里之所以需要一个函数签名信息,是因为 Java 支持函数重载
  • 只有把参数返回值都确定了,才能找到对应的到底是哪个 Java 层函数

动态注册小结

  • 动态注册是 C/C++ 世界告诉某个 Java 虚拟机:我的这个函数,是给这个 Java 类用的,它对应该类的这个 native 方法。
  • 注意: C/C++ 是把这种对应关系告诉了虚拟机,而不是告诉某个 Java 类,即不是告诉了 Java 程序员。跟静态注册一样,只有虚拟机需要关心 java 层的方法和 jni 层的函数之间的对应关系,而不是 Java 程序员需要关心。
  • 注意:C/C++ 里的一个函数,对应的是 Java 里的某个类的某个方法。Java里必须有类。
  • 因为动态注册的过程,是 C/C++ 跟虚拟机对话的过程,所以 C/C++ 必须先拿到那个虚拟机才能进行注册。在 C/C++ 世界,Java 虚拟机的代表就是这两个结构体变量:JavaVM, JNIEnv。

Android 动态注册的一般流程

  • 先把实现了 Java 层 native 函数的那些函数的指针都封装到 JNINativeMethod 数组里
static JNINativeMethod gMethods[]={    {        "processFile",        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",        (void*)android_meida_MediaScanner_processFile    },    {        "native_init",        "()V",        (vodi*)android_meida_MediaScanner_native_init    }};
  • 通过方便函数 AndroidRuntime::registerNativeMethods(env,"android/media/MediaScanner",gMethods,ELEN(gMethods)) 来注册
  • 而不是直接使用 (*env)->RegisterNatives(env,clazz,gMethods,numMethods)
  • 因为方便函数的第二个参数直接传字符串就可以了,不用先通过字符串生成一个 jclass 类型再传

在JNI层操作Java层的东西

调用Java层的方法

  1. 通过 jclass clazz = env->FindClass("含有路径的类名"); 找到类
  2. 通过 jmethodID mid = env->GetMethodID(clazz,"方法名","方法签名信息");找到Java层方法的ID
    • 注意 jmethodID 是一个专门记录 Java 层方法的类型
    • 类似的还有一个 jfieldID
  3. 通过 env->CallxxxMethod(jobj,mid,param1,param2...); 调用 Java 层的方法
    • CallxxxMethod 中的 xxx 是 Java 方法的返回值类型,比如 CallVoidMethod,CallIntMethod
    • 第一个参数是指调用哪个对象的方法,就是 Java 中.前面的那个对象
    • 第二个参数 Java 中的 MethodID
    • 后面的参数就是 Java 方法的参数了,其类型都要是 java 中能处理的类型,比如 jstring,jint,jobject

get和set Java层的field

  1. 通过 jclass clazz = env->FindClass("含有路径的类名"); 找到类
  2. 通过 jfieldID fid = env->GetFieldID(clazz,"成员名","成员类型标示");找到Java层成员变量的ID
  3. 通过 GetxxxField(env,obj,fid); / SetxxxField(env,obj,fid,value); 来get/set相应的成员变量

从无到有: The Invocation API

  • 以上方法都是 Java 主动调用了 native 代码之后,native 代码拿着 JNIEnv 去操作 Java 层的东西
  • 如果现在连 JVM 都没有,C/C++ 世界能不能使用 Java 世界的代码呢?
  • 就像 JAVA 可以通过 loadLibrary 把 C 库加载到内存,然后使用其中的方法一样。C/C++C 也可以先启动一个 JVM,然后使用 Java 的方法。
  • 这要用到 The Invocation API。

hello world

  • 目的: 一个 C/C++ 写的可执行程序,在代码里使用已经编译好的 class 文件里的功能。
  • 下面的例子来自JNI官方文档第5章,本文介绍具体怎么把这个例子运行起来。

1. JAVA Code

public class JavaApp{    public static void javaMethod(){        System.out.println("I have done a lot of things by Java!");    }}

2. 编译 java:

  • javac JavaApp.java 直接生成 JavaApp.class 没什么好说的。

3. CPP Code

#include <jni.h>       int main(){    JavaVM *jvm;       /* denotes a Java VM */    JNIEnv *env;       /* pointer to native method interface */    JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */    vm_args.version = JNI_VERSION_1_6;    vm_args.ignoreUnrecognized = false;    JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);    jclass cls = env->FindClass("JavaApp");    jmethodID mid = env->GetStaticMethodID(cls, "javaMethod", "()V");    env->CallStaticVoidMethod(cls, mid);    jvm->DestroyJavaVM();}

4. 编译CPP

  • 加上-I选项,让gcc能找到 jni.h 和 jni_md.h:
    -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux
  • 加上连接器选项,让ld能找到 libjvm.so:
    -L/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server CppApp.cpp -ljvm
    注意: gcc 在做链接时会严格的按照从左到右的顺序,如果 -lxxx 这个库的左边并没有任何东西需要它,那么这个 -lxxx 会被忽略。 因为我们 CppApp.cpp 编译出来的 .o 是需要 libjvm.so 的,所以我们的 -ljvm 一定要出现在 CppApp.cpp 的右边
    参考: http://stackoverflow.com/questions/16860021/undefined-reference-to-jni-createjavavm-linux
  • 最后,完整的编译命令:
gcc -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux -L/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server  -o app CppApp.cpp  -ljvm

5. 运行

  • 直接运行 app 会报错链接不上 libjvm.so 这个库。
  • 因为安装完 openJDK8,操作系统并不能找到 libjvm.so。 可以运行 sudo ldconfig -v | grep jvm 验证一下,果然啥也没有。
  • 我们需要自己配置一下,利用 ldconfig 工具。
  • 具体做法是: 在目录 /etc/ld.so.conf.d/ 里添加一个文件: openJDK.conf,这个文件里写上:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server 。 这就是 libjvm.so 所在的目录。 然后运行一下 ldconfig 即可。因为 ldconfig 运行的时候,会去加载目录 /etc/ld.so.conf.d/ 里的所有 .conf 文件。
  • 搞定后直接运行 ./app,会打印出 Java 层的输出,大功告成。

总结

  • 至此,我们已经成功运行了该例子,现在来总结一下Native程序到底是怎么调到java的功能的。
  • 想使用 java code 实现的功能,首先我们得搞一个 JVM 出来,这就是:JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
  • 这个函数是在 libjvm.so 里实现的,此函数是 JNI 标准的一部分,所以随JDK发布。
  • 这个函数能创建一个 JVM 对象,该对象肯定也是活在我们的 Native 进程里的。
  • 这个函数中创建 JVM 的同时,把 JNIEnv 也返回给我们了,即第二个参数。
  • 我们拿着 JNIEnv 就可以随便搞了,像获取 Java 类,获取 methodID,调用 java method 的什么的,都不是新鲜事了。

其他零星

jstring 要手动释放

  • JNI层的jstring要手动释放,这和jstring内部实现有关
  • char *cString = env->GetStringUTFChars(jstring javaString, NULL) 能从jstring类型得到C语言的字符串(char*)
  • jstring javaString = env->NewStringUTF(const char* cString) 能从C语言的字符串得到jstring的类型
  • 以上两种方法调用之后,都要调用 env->ReleaseStringUTFChars(jstring javaString, char* cString)来释放
  • 否则会导致JVM内存泄露

JNI类型签名

  • Java 类型对应到 C/C++ 里都有个对应的类型标示。 比如 Java 的 long,在C/C++里用 “J” 做类型标示。
  • 函数签名信息的格式是:

    (参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示
  • 可以用 javap 工具生成函数签名信息

logcat

  • jni c 语言层往 logcat 里写 log

要点:
1. #include <android/log.h>
2. Android.mk 里:LOCAL_LDLIBS := -llog
3. __android_log_print(ANDROID_LOG_DEBUG, "YourTag", "Your log here %s", a_c_string_var);
4. 上述函数能把log写入logcat,第一个参数是log级别,第二个是Tag,第三个是log的内容。并且第三个参数可以按照print()的方式进行格式化字符串。

参考文档

  • 《深入理解Android卷一》 第二章
  • JNI 官方文档第5章: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html
0 0
原创粉丝点击