2018-06-04 19:57:49
这几天研究了一下JNI,其实以前也看过,总感觉理解没那么透彻,这次优化时间看了看,做点记录~
1、JNI是什么?存在的意义是什么?
Java Native Interface,Java本地接口。作用是实现Java代码和Native代码(C、C++、汇编语言)之间的相互调用。
2、为什么Java和Native代码要能够相互调用?
Java是一种高级语言,JVM的存在抹平了系统之间的差异性,但这是一把双刃剑,虽然实现了Java的跨平台性,也产生了一些问题,SourceCode->BitCode->BinaryCode,Java是一种解释性语言,JVM在执行时,读取字节码,读一行执行一行。比起C++那种编译、链接之后直接生产当前平台的机器指令来,性能上会有损失。虽然随着Java的发展,这种性能损失已经变得越来越小,但还是客观存在的。
所以一些对性能要求比较高的场景,一般都会采用Native代码来实现(一般都会提供跨平台的编译版本),同时还有一些Java出现之前就存在的Native库,考虑到兼容性,也应该提供一种手段,而这种手段就是JNI。JNI不是Android特有的,而是Java的。
3、如何使用?
首先要有一个Java类,其中定义一些native方法,同时要在程序启动的时候加载native库,代码示例如下:
3.1 JniHelper是一个普通类,定义了一些native方法,这些native方法是有C/C++代码实现的;
3.2 在static语句块中加载so库,Linux平台Native代码生成的是so库。因为这些native方法都是JniHelper的方法,所以要在调用这些方法之前加载so库,最合适的地方就是static语句块中,当JniHelper类被第一次使用时,会触发ClassLoader加载,加载之后就会执行static语句块中的内容;
3.3 这个类中还有一些非native的方法,这些方法可以被Native代码调用,具体的使用后面介绍。
3.4 生成so库的名称如下图,但是我们在加载时不需要前缀lib和后缀.so。还有一点,System.loadLibrary()方法中使用了相对路径,前面的完整路径是java.library.path指定的,这是一个系统属性,可以通过System.getProperty("java.library.path")获得,代码及结果如下:
1 Log.e("David", "java.library.path = " + System.getProperty("java.library.path")); 2 3 java.library.path = /system/lib64:/system/vendor/lib64
当然也可是使用绝对路径,但是除非你的程序只在本机执行,否则不要使用。尤其是Android系统,在安装APK的时候会把响应目录下的so库拷贝到system/lib64或者system/vendor/lib64目录下,所以强烈不建议使用绝对路径。
4、如何编写C/C++代码实现native方法呢?
以JniHelper为例
4.1 生成JniHelper的class文件,如果JniHelper中没有Android的API,可以直接使用javac,否则最好先编译APK,然后再把class文件拷贝出来使用;
4.2 javah -jni com.baidu.****.JniHelper 生成对应的.h文件,如下图:
如上所示,JNIEnv指针是一个指向Jni函数表的指针,默认的第一参数。第二个参数比较特殊,如果你定义的native方法是static的,那么第二个参数是jclass,如果是普通方法,则是jobject,如上图中第2、3个函数定义所示。从第三个参数开始就是Java中定义的native方法中的参数,顺序也是一致的。
4.3 Java的数据类型和C/C++代码的有一个对应关系,但事实上,除了集中基本数据类型,其他的类型都没法无缝衔接,需要做一个转换,对应关系如下:
引用类型:
具体看jni中的定义:
可见基本数据类型的对应关系比较简单,可以直接互相转换。比如jlong可以和long一样使用,介意忽略他们的区别。再看看引用类型,如下:
可以看到jstring、jintarray等都是JNI中自定义的,所以无法和Java中的数据类型无缝切换,需要转换一下。
5 以jstring为例,如何无缝转换使用String和jstring
5.1 如上图可见,Java传过来的String变成了JNI数据类型jstring,但是这里的content只是一个指向JVM中某个数据结构的指针,C++无法直接通过这个指针访问该数据结构,所以需要通过JNIEnv提供的方法来转换车C++可识别、可访问的数据结构方可。注意最后一行,由于GetStringUTFChars()方法会分配内存,所以方法执行结束后必须要释放内存。
5.2 生成新的字符串
注意方法三,GetStringUTFRegion()方法不会分配内存,它只会将源字符串中的内存拷贝到指定的缓存空间中。
6、C++调用Java类中的静态方法
根据之前分析的,所有JNI方法第二个参数要么是jclass,要么是jobject,如果是static native方法,则是jclass,否则是jobject。如下:
1 public static native void writeFileAndCallbackJava(JniHelper jniHelper); 2 //JNI方法有3个参数:JNIEnv,jclass,jobject,前两个是默认的,第3个时jniHelper。 3 4 public native void nonStaticWrite(); 5 //JNI方法有2个参数,均为默认参数,JNIEnv,jobject。 6 7 public static native void showMessage(String content); 8 //JNI方法有3个参数:JNIEnv,jclass,jstring
所以要想调用静态方法,我们可以直接使用jclass,找到要调用的方法,然后调用即可。前提是,C++调用的代码在Java声明native方法的类中。当然可以调用任意一个Java类,只要知道他的包路径及类名即可,因为是直接调用static方法,所以不需要调用构造函数,代码如下:
7、C++中调用Java类中的非静态方法
这个原理和调用静态方法的类似,如果参数中有jobject对象,可以直接使用。如果是自己查找的Class或者jclass对象,那么先要调用Class的构造器,得到jobject,然后再调用方法,代码如下:
如上图,这个方法在Java声明为static的,所以没有jobject,这种情况,如果直接使用默认的jclass,那么就不需要查找Class类。步骤如下:
(1)查找Class或者使用参数jclass;(2)找到构造方法Id;(3)调用构造方法,得到jobject;(4)找到非静态方法Id;(5)调用非静态方法。
这个过程中涉及几个关键函数,下面一一解析:
1、查找Class,得到jclass对象 jclass clazz = env->FindClass(完整的类名); //注意,这里得到的clazz有可能为NULL,需要做好判断 2、查找构造方法 查找构造方法和普通方法是一样的,都使用GetMethodID()方法,只不过方法名称和签名比较特别,如下: jmethodID constrct_methodID = env->GetMethodID(clazz, "<init>", "()V"); //这个指的是调用该类的默认构造方法,没有任何参数的。 3、执行构造方法,得到jobject对象 jobject jobj = env->NewObject(clazz, construct_method); //得到jobject对象,有可能为NULL,需要做判断 4、查找方法,得到jmethodID对象 jmethodID methodId = env->GetMethodID(clazz, "method_name", "method_sign"); //clazz表示要查找的类,第二个参数是方法名称,第三个参数是方法签名,如果void showMessage(String content, int index),如下 env->GetMethodID(clazz, "showMessage", "(Ljava/lang/String;I)V"); //得到methodID有可能为NULL,需要做好判断 5、调用普通方法 其实调用方法的JNIEnv函数是一样的,只不过传入的第一个参数不同,如下: env->CallVoidMethod(jobj, jmethodID1, params); //如果是static方法,那么第一个参数传入的是jclass。