2.4 使数据可以被OpenGL存取
我们已经完成顶点的定义了,但是,在OpenGL可以存取它们之前,我们仍然需要完成另外一步。主要的问题是这些代码运行的环境与OpenGL运行的环境使用了不同的语言,我们需要理解如下两个主要的概念。
1.当我们在模拟器或者设备上编译和运行Java代码的时候,它并不是直接运行在硬件上的;相反,它运行在一个特殊的环境上,即Dalvik虚拟机(Dalvik virtual machine);运行在虚拟机上的代码不能直接访问本地环境(native environment),除非通过特定的API。
- Davik虚拟机还使用了垃圾回收(garbage collection)机制。这意味着,当虚拟机检测到一个变量、对象或者其他内存片段不再被使用时,就会把这些内存释放掉以备重用;它也能腾挪内存以提高空间使用效率。
本地环境并不是这样工作的,它不期望内存块会被移来移去或者被自动释放。
Android之所以这样设计,是因为开发者在开发程序的时候不必关心特定的CPU或者机器架构,也不必关心底层的内存管理。这通常都能工作得很好,除非要与本地系统交互,比如OpenGL。OpenGL作为本地系统库直接运行在硬件上;没有虚拟机,也没有垃圾回收或内存压缩。
2.4.1 从Java调用本地代码
Dalvik方案是Android的主要特点之一,但是,如果代码运行在虚拟机内部,那它怎么与OpenGL通信呢?有两种技术,第一种技术是使用Java本地接口(JNI),这个技术已经由Android软件开发包提供了;当调用android.opengl.GLES20包里的方法时,软件开发包实际上就是在后台使用JNI调用本地系统库的。
2.4.2 把内存从Java堆复制到本地堆
第二种技术就是改变内存分配的方式,Java有一个特殊的类集合,它们可以分配本地内存块,并且把Java的数据复制到本地内存。本地内存可以被本地环境存取,而不受垃圾回收器的管控。
我们需要按如图2-5所示传输数据;在类的顶部,在构造函数之前加入如下代码:
这里加入了一个整型常量BYTES_PERFLOAT和一个FloatBuffer类型变量;一个Java的浮点数(float)有32位(bit)精度,而一个字节(byte)只有8位精度;这点可能看起来很明显,每个浮点数都占用4个字节;在后面的开发讲解中,很多地方都会提到这点。那个FloatBuffer用来在本地内存中存储数据。
让我们加入更多的代码,这次加在构造函数体的结尾处:
让我们看一下代码的每一个部分。首先,我们使用ByteBuffer.allocateDirect()分配了一块本地内存,这块内存不会被垃圾回收器管理。这个方法需要知道要分配多少字节的内存块;因为顶点都存储在一个浮点数组里,并且每个浮点数有4个字节,所以这块内存的大小应该是tableVerticesWithTriangles.length * BYTES_PER_FLOAT。
下一行告诉字节缓冲区(byte buffer)按照本地字节序(native byte order)组织它的内容;本地字节序是指,当一个值占用多个字节时,比如32位整型数,字节按照从最重要位到最不重要位或者相反顺序排列;可以认为这与从左到右或者从右到左写一个数类似。知道这个排序并不重要,重要的是作为一个平台要使用同样的排序;调用order(ByteOrder.nativeOrder())可以保证这一点。
最后,我们不愿直接操作单独的字节,而是希望使用浮点数,因此,调用asFloatBuffer()得到一个可以反映底层字节的FloatBuffer类实例;然后就可以调用vertexData.put(tableVerticesWithTriangles)把数据从Dalvik的内存复制到本地内存了。当进程结束的时候,这块内存会被释放掉,所以,我们一般情况下不用关心它;但是,如果你在编写代码的时候,创建了很多ByteBuffer,或者随着程序运行产生了很多ByteBuffer,你也许想学习一些堆碎片化以及内存管理的技术。
为了把数据从Dalvik传进OpenGL,用了这么多步骤,但是在继续讲解之前,理解它是如何工作的是很重要的;就像文化和风俗一样,国与国有很多不同,我们也要知道跨越本地代码边界时的差异。