Android NDK 编程指南(二)
文章测试案例提交到Github:learnNdk
有了第一篇内容的基础之后,我们开始正式学习JNI
。如果前面一片文章你已经写出来了一个demo
,但是还有很多有疑问的地方,没关系,你可以把你的疑问记下来,在接下来的学习中,我们将会慢慢解开这些疑问。首先我们看一张图。
这张图很明晰的表达出了JNI
在NDK
编程中所担任的角色,以及JNI
在和java
虚拟机的关系。很显然它属于java
虚拟机的一部分。
我们知道在Java
中一般有两种类型的方法,一个是instance
方法,一个是类方法
,在JNI
对应的函数里面一般至少都会有两个参数,一个是JNIEnv
,一个是jobject
或者jclass
,其中第二个参数的不同,就是对应着Java
中的方法是所属于某个对象还是所属于这个类。这两个参数会在JNIEnv
方法调用的时候有些地方会用到。
我们使用NDK
编程的目的其实就是为了用C/C++
代码来帮助我们实现java
里不好实现或者不方便实现的内容,问题说的通俗一点NDK
编程其实就是java
如何与C/++
进行数据通信。
通过上图我们了解到,java想要和C/C++
进行数据通信,需要经过JNI
层进行桥梁转换,也就是说我们的java
层数据想要传递到C/C++
层,首先要经过JNI
层转换后才能到C/C++
层。同理,C/C++
层数据想要传递给java
层也是如此。
不同语言层级之间进行数据交互,必然涉及到数据类型的转换。不对等的数据类型是无法进行数据交互的,即使可以,也容易导致bug
甚至错误的发生。
下面我们就来看看这个三个层级之间数据类型是如何转换的。
基本数据类型转换
我们知道在java
里数据类型分成:基本数据类型
和引用数据类型
。
基本数据类型有8种
分别是:boolean
,byte
,char
,short
,int
,long
,float
,double
。
三者的对应关系如下表:
JavaType | JNIType | C/C++ Type |
---|---|---|
boolean | jboolean | uint8_t(unsigned char) |
byte | jbyte | int8_t (signed char) |
char | jchar | uint16_t (unsigned short) |
short | jshort | int16_t (short) |
int | jint | int32_t (int) |
long | jlong | int64_t (long) |
float | jfloat | float |
double | jdouble | double |
上面的基本类型数据在同等对应之间是可以直接转换的,举一个例子:我们以int
类型进行举例,其他类型类比如此就行了,还是我们的add
函数。
java中的原型:
public native int add(int a ,int b);
这个java
方法向JNI
层传递了两个int
类型的参数,同时需要从JNI
层,返回一个int
类型的参数。这个参数传递到JNI
层是怎样转换的呢?记住,我们并不能直接传递到C/C++
层,总是从Java
->JNI
->C/C++
。虽然很多JNI
的代码放在C/C++
文件,但是这部分代码却属于JNI
层。
接下来我们就来看看JNI
层的代码
JNIEXPORT jint JNICALL
Java_com_sivin_ndkdemo_NormalJni_add(JNIEnv *env, jobject instance, jint a ,jint b)
从个函数中我们发现int
类型的参数转成了jint
类型的参数,同时返回的类型也是jint
类型。
那么如何在将jni
层的数据传递geiC/C++
层呢,我们来看看代码实现
extern "C"
int add(int a ,int b){
return a+b;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_sivin_opengles_1andorid_MainActivity_add(JNIEnv *env, jobject instance, jint a,
jint b) {
jint sum = add(a,b);
return sum;
}
为了明显的突出三个层级之间的关系,我们特意在native
文件里写了一个C
语言实现的函数,从函数中我们可以看到jint
类型直接转换成C
层的int
类型,反之亦然,C
层的返回的int
类型也直接转换成jint
然后又返回给了java
层的int
类型。
我们想强调的一点是上面的类型转是java基本数据类型
才能这样做,其他的数据类型是如何转换的呢?
引用数据类型转换
清楚java
语言的都知道在java
中,除了基本数据类型,剩下就是引用数据
类型:
Refrenece 数据类型
Java
的引用数据类型
并不像原始数据类型
一样转换到JNI层
之后可以直接被C/C++
使用,它需要经过再次的变换
,使之可以与C/C++
进行数据交互,JNI
提供了一系列的API
来帮助我们完成这些变换
,这些API
通过JNIEnv
获取,并调用。
JAVA中的都有哪些引用数据类型呢?
- object
- class
- throwable
- String
- Arrays
- NIO Buffers
- Fields
- Methods
上面我们可以完全用object
代替所有,但是这里我们并不打算这样做,上面的object
仅代表普通的java
对象。因为在JNI
中不同的引用数据类型对应着不同的JNI
数据类型。具体对应我们看下表:
JNI引用数据类型对应表
JavaType | JNIType |
---|---|
java.lang.Class | jclass |
java.lang.Throwable | jthrwoable |
java.lang.String | jstring |
other objects | jobject |
object[ ] | jobjectArray |
基本数据类型[ ] (例如:int[ ]) | j基本数据类型Array (例如:jintArray) |
other arrays | jarray |
看上面的这个表,不懂的人看的是一头雾水,最显而易见的疑惑是,怎么没有C/C++
对应关系。没关系,我们下面的学习就知道了。上面的这个表我们就大致看一下,有一个整体感知就行了,下面我们就来具体的针对每一个对应关系进行解释说明。
首先我们就来从最基本的String
类型说起,有人怕是要问,为什么从String
而不是object
。我想说问的好,解释一下,很简单因为它常用而且特殊,同时JNI
为String
也提供专有的数据类型映射和一些处理函数。后面学习,我们会知道普通的object
类型的映射还需要用到其他的知识,而string
则相对更集中一些,同时处理字符串应该是每一个编程语言的一个很重要的任务。因此我们首先从String
类说起。
String 操作
首先我们回顾一下java String
类的一些基本知识,首先java.lang.String类
使用了final
修饰,不能被继承。即双引号括起的字符串,如"abc"
,都是作为String类
的实例实现的。String
是常量,其对象一旦构造就不能再被改变,换句话说,String
对象是不可变的,每一个看起来会修改String
值的方法,实际上都是创造了一个全新的String
对象,而最初的String
对象则丝毫未动。String
对象具有只读特性,指向它的任何引用都不可能改变它的值,因此,也不会对其他的引用有什么影响。但是字符串引用可以重新赋值。
基本的java String
的一些基础知识我们就说这么多,如果对这方面还有疑问的建议好好复习一下这方面的知识。
我们通过上面的表可以知道java中的String
类型的数据传递到JNI
层后,就转变成了jstring
数据类型。但是有一个问题,我们并不知道jstring
数据类型该如何在C/C++
中处理,记住我们的主线,总是java层
--> JNI层
-->C/C++层
,然后在反过来。那么jstring
是如何传递到C/C++
层的呢?
我们知道在C
里面我们处理字符串使用过char *
或者char [ ]
,当然在C++
里面还有string类
,这些可以向基本类型一样直接进行转换吗?答案当然是不能。
既然不能直接转换,肯定有转换的方式,否则我们就没法继续编程了。是的,在前面我们提到过,每一个JNI
函数都有一个JNIEnv *
的参数。这个参数里可以得到很多函数指针,通过这些函数,我们就可以让Java
和C/C++
进行数据通信。
因为Java
的String
对象是不可变的,因此JNI
并不提供任何修改Java
中已经存在String
类型数据内容的方法。
JNI
同样支持Unicode
和UTF-8
编码的String
,并且提供了两组方法集,来处理这些编码的字符串.
我们来看看那个方法能将jstring
-->转换到C/C++
层可用的数据,我们打开jni.h
头文件,找到JNIEnv
结构体。看看里面有没有相关的方法,怎么找?很简单,想要将jstring
变换到C/C++
一定是通过一个函数,我们先看返回值,看看有没有返回char *
相关的函数:
找了一圈,我们发现了下面这个相关的函数。
//将jvm内Unicode字符转换成UTF-8的字符串
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
在解释这个函数之前,我们先来补充一个知识点,在java中字符串
在内存中是采用unicode
编码方式存放的:任何一个字符对应两个字节的定长编码。即任何一个字符(无论中文还是英文)都算一个字符长度,占用两个字节。。UTF-8
字符串使用一种向上兼容7-bit ASCII
字符串的编码协议。UTF-8字符
串很像NULL
结尾的C字符串
,在包含非ASCII
字符的时候依然如此。所有的7-bitASCII
字符的值都在1~127
之间,这些值在UTF-8编码
中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符(16-bitUnicode
值)。
一般情况下我们使用GetStringUTFChars
函数进行转换成C
的字符串,细心的你可能在寻找的时候还会发现另外一个函数GetStringChars(this, string, isCopy)
,并且看到它的的返回值是jchar
类型,这个函数返回的字符是Unicode
编码的,一个字符占用两个字节,对应到C/C++
就是short
,对应到jni
就是jchar
由于jchar
是一个16位的short
类型,无法直接转换成C
类型的字符串。因此我们一般不使用这个函数。
/**
*jstring:从java层传递转换过来的string类型数据
*jbooean:表示当我们调用这个函数时将jstring转成成C字符串,是内存的直接指向还是,复制了一份,这里我们一般不关心它是怎么来的,因此我们一般在开发的过程中可以直接传递一个NULL或者nullptr
*/
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
需要注意一点是,调用这个函数我们将会得到一个C/C++
可以处理的字符串,究竟这个函数是将字符串的值复制一份到C/C++
,还是直接将内存地址指向,是由java虚拟机实现机制决定的,但是作为开发者,我们应该遵循一个规则我们总应该认为他是复制一份数据到C/C++,这样做至少不会有错
。既然我们认为是复制一份数据到C/C++
,那么这份内存空间就应该需要我们自己管理了,否则可能会引发内存泄露
,因此我们在不需要这份内存数据之后应该将这份内存释放掉。
如何释放内存呢,是不是像C/C++
一样使用free
或者delete
呢?因为我们不清楚从JNI
到C/C++
是如何转换的,因此如果直接使用free
显然是不合适的,同样JNI
为我们提供相关的释放这段字符串内存
的方法。
/**
*jstring :是从java层传递过来的jstring
*char * :由jstring转换成的字符串
ReleaseStringUTFChars(jstring ,const char *);
转换和释放都已经说完了,剩下的我们就可以利用C/C++
相关的东西来处理我们的业务了。在处理完成之后,我们想将我们处理的结果在返回给java
层。这一步如何实现呢?
显然我们需要将char *
数据转换成jstring
然后JNI
就会将jstring
传递到java层转换成String
。
我们知道在java
层String
实例在Java中可以通过new
的方式被实例化出来,那么在JNI
层中是否有方式也能创造一个jstring
然后传递到java
层呢?显然是有的,同样我们可以查阅JNIEnv *
,w其中NewString
和一个NewStringUTF
函数,这个两个函数的区别和上面说的一样,这里我们使用newStringUTF
这个函数,同样有一个问题,我们可以用C/C++
分配的char *
来创造jstring
对象,那么这个块内存空间是否需要释放呢?是return前
释放还是return后
释放呢?显然我们不能在return前
释放,因为释放了内存,我们java层如何处理呢?。在return后
释放?,这个更不现实了,这段代码就不会执行。那么该怎么办呢?答案是,不用管理这块内存,因为我们转成jstring
之后传递给了java虚拟机,这块内存空间就由java虚拟机自己管理了。
示例代码如下:
jstring javaString;
javaString = (*env)->NewStringUTF(env, "Hello World!");
return javaString
这个方法传入一个c
类型的字符串,返回一个Java
类型的字符,由于可能会由于内存空间不足,因此,这个函数将会返回NULL
阻止Native code
继续运行,同时会抛出一个异常.
这样我们就把一个字符串
处理流程就讲解完了,当然还有很多其他的细节我们没有讲到,如unicode
字符串等,这里我们后续在补充,因为它还涉及到字符编码
问题。这里我们知道有这么回事就行了。但是这已经可以满足我们常规的处理需求了。