另一篇相关文章:https://androidblog.blog.csdn.net/article/details/109855062
前言
ByteBuffer,顾名思义,它表示字节缓冲区
。一般我们在代码中使用字节的时候一般用字节数组,即byte[]
,但是使byte[]
的方式效率不高,而使用ByteBuffer
的方式来操作数组效率是比较高的,具体描述可以查看jdk文档声明,可以查看ByteBuffer
类,以及它的父类Buffer
的文档声明,可以详细的了解它们的功能,它们是nio包下面的,nio就是new io,是在jdk1.4版本的时候新出的IO相关的类,是为了解决旧的IO效率低的问题的。ByteBuffer
除了效率高之外,也提供了一些比较好用的方法,比如writeInt
、getInt
、putFloat
、getFloat
等一些读写Java基本数据类型的方法,在保存基本类型数据时,还可以通过order(ByteOrder)
函数来定义使用大端或小端来保存数据,通过ByteOrder.nativeOrder()
可以获取获取底层平台的本机字节顺序(即大端、小端)。
Buffer
Buffer类文档说明
ByteBuffer继承自Buffer,所以先了解一下Buffer的使用。JDK文档上有如下描述:
Buffer表示一个用于特定基本类型数据的容器。Java有8大基本数据类型,除了boolean外,另外7个类型都有对应的Buffer实现类,如:ByteBuffer、IntBuffer、CharBuffer等。
除内容外,缓冲区的基本属性还包括容量(capacity)、限制(limit)和位置(position):
- capacity:缓冲区的大小,不能更改。
- limit:第一个不应该读取或写入的元素的索引
- position:下一个要读取或写入的元素的索引
不变式:
mark、position、limit和capacity遵守以下不变式:0 <= mark<= position<= limit<= capacity
新创建的缓冲区总有一个 0 位置和一个未定义的标记。初始限制可以为 0,也可以为其他值,这取决于缓冲区类型及其构建方式。
传输数据
此类的每个子类都定义了两种get
和put
操作:
-
相对:
get
或put
一个或多个元素。position会随之改变。如果请求的传输超出限制,则get
操作将抛出BufferUnderflowException
,put
操作将抛出BufferOverflowException
;这两种情况下,都没有数据被传输。 -
绝对:操作采用显式元素索引,该操作不影响位置。如果索引参数超出限制,则
get
操作和put
操作将抛出IndexOutOfBoundsException
。
mark和reset
调用mark()
时,将使用mark
保存position
,当调用reset()
时,将把position
恢复为mark
。mark
不能大于position
,如果调用了mark()
,则在position
或limit
调整为小于mark
时,mark
将被丢弃。如果未调用mark()
,那么调用 reset()
方法将导致抛出 InvalidMarkException
。
只读缓冲区
每个缓冲区都是可读取的,但并非每个缓冲区都是可写入的。每个缓冲区类的转变方法都被指定为可选操作,当对只读缓冲区调用写入时,将抛出 ReadOnlyBufferException
。只读缓冲区不允许更改其内容,但其标记、位置和限制值是可变的。可以调用其 isReadOnly()
方法确定缓冲区是否为只读。
线程安全
多个线程同时使用缓冲区是不安全的。如果一个缓冲区由不止一个线程使用,则应该通过适当的同步来控制对该缓冲区的访问。
调用链
val buffer = ByteBuffer.allocateDirect(4)
buffer
.flip()
.position(23)
.limit(42)
Buffer中的方法
共17个方法。
array()方法的使用
有些时候,我们无法直接使用ByteBuffer
对象,必须得使用原始的方式,如使用byte[]
,则此时可以使用array()
方法,该方法的JDK文档说明如下:
在IntelliJ中运行如下代码:
val buf1 = ByteBuffer.allocate(4)
val buf2 = ByteBuffer.allocateDirect(4)
println(buf1.hasArray())
println(buf2.hasArray())
输出结果如下:
true
false
这说明ByteBuffer.allocate()
方法创建的ByteBuffer
对象的底层实现是一个数组,而ByteBuffer.allocateDirect()
方法创建的ByteBuffer
对象的底层实现不是数组,注:这不是绝对的,比如相同的代码,运行到Android手机上,输出结果如下:
true
true
这是否可以认为在Android中,ByteBuffer.allocate()
和ByteBuffer.allocateDirect()
是一样的,因为返回的ByteBuffer
都是由一个数组实现的。当然了,这样的结论还是不要轻易的下,如果要高效,就要用ByteBuffer.allocateDirect()
,我们不管它是否真的高效,反正用它就不会错了。
再来看一下hasArray()
的JDK文档说明:
这里又发现有一个arrayOffset()
方法,JDK文档说明如下:
也就是说,如果ByteBuffer
的底层实现是一个byte[]
,则数组前面可能有几个字节并不是用来给我们存储数据的,所以我们需要使用偏移量(arrayOffset()
)。示例如下:
val buf1 = ByteBuffer.allocate(4)
val buf2 = ByteBuffer.allocateDirect(4)
buf1.put(byteArrayOf(1, 2, 3, 4))
buf2.put(byteArrayOf(4, 3, 2, 1))
if (buf1.hasArray()) {
val byteArray = buf1.array()
println("buf1 = ${Arrays.toString(byteArray)}")
}
if (buf2.hasArray()) {
val byteArray = buf2.array()
println("buf2 = ${Arrays.toString(byteArray)}")
}
在Android手机上运行如上代码,输出结果如下:
buf1.arrayOffset() = 0
buf1 = [1, 2, 3, 4]
buf2.arrayOffset() = 4
buf2 = [0, 0, 0, 0, 4, 3, 2, 1, 0, 0, 0]
如上代码,我们给buf1
和but2
都是分配的4个字节的大小,但是从输出结果看,buf2的底层实现数组多了7个字节:在数组的最前面多了4个字节,在数组的最后面多了3个字节。所以我们在使用buf2的底层数组时,需要调用arrayOffset()
来获取偏移量,对于buf2
它会返回4,说明这个底层数组从4的位置开始是用于保存我们的数据的,当然也要注意后面也是多了3个字节的,读取的长度可以使用remaining()
,这样可以确保不会读取越界,remaining()
的JDK文档说明如下:
ByteBuffer
有一个get(byte[] dst, int offset, int length)
函数,可以把缓冲区中的数据保存到指定的byte[]
数组中,这就多了一层数据拷贝了,所以,如果我们知道该ByteBuffer
的底层实现是一个数组,而且我们需要使用数组的形式,则应该使用array()
来获取数组,而避免使用get(byte[] dst, int offset, int length)
来获取数组,这样可以节省一次数组拷贝。对于hasArray()
返回false的情况,则只能使用get(byte[] dst, int offset, int length)
来获取数组了!示例如下:
fun main() {
val console = PrintStream(System.out)
val buffer = ByteBuffer.allocateDirect(4)
buffer.put(byteArrayOf(65, 66, 67, 68)) // 4个数字分别对应的4个ASCII码为:A、B、C、D
buffer.flip()
val remaining = buffer.remaining()
if (buffer.hasArray()) {
console.println("hasArray() = true")
val byteArray = buffer.array()
console.write(byteArray, buffer.arrayOffset(), remaining)
console.println()
} else {
console.println("hasArray() = false")
val byteArray = ByteArray(remaining)
buffer.get(byteArray, 0, remaining)
console.write(byteArray)
console.println()
}
}
在IntelliJ中运行结果如下:
hasArray() = false
ABCD
这说明在Windows
平台下ByteBuffer.allocateDirect()
分配的缓冲区底层实现不是数组,所以不能使用buffer.array()
来获取数据,只能通过buffer.get(byteArray, 0, remaining)
来获取数据。
在Android手机上运行结果如下:
hasArray() = true
ABCD
可以看到,在Android手机上,ByteBuffer.allocateDirect()
分配的缓冲区底层实现是一个数组,所以我们就可以直接使用array()
方法来获取数组,省去一次数组拷贝,需要注意的是:在使用这个数组时要从arrayOffset()
的位置开始,在我的一个Android项目中,写音频文件,使用到了ByteBuffer
,音频数据保存在了ByteBuffer
对象中,我需要把数据写到文件中,但是写文件的方法只能接收byte[]
,不能接收ByteBuffer
,所以我就使用了array()
方法来获取byte[]
,结果发现保存的音频数据有点异常,声音听起来像有电流音,开始以为是手机插着数据线导致的,后来拨了线还有问题,又以为是设备有问题,其实是代码的问题,因为我在使用array()
的时候没有使用arrayOffset()
偏移量,导致每次获取的音频数据前面4个字节为0。
需要提醒的是,我这里说的是,如果你只能使用数组时,才应该使用array()
,比如前面例子中的PrintStream支持写byte[]
,但是不支持写ByteBuffer
。如果可以直接使用ByteBuffer
时,则不要使用array()
,如下示例直接使用ByteBuffer
:
fun main() {
val fileChannel = FileOutputStream("D:\\demo.txt").channel
val buffer = ByteBuffer.allocateDirect(4)
buffer.put(byteArrayOf(65, 66, 67, 68))
buffer.flip()
fileChannel.write(buffer)
fileChannel.close()
}
这里我们把buffer中的数据写到了demo.txt
文件中,因为FileChannel的write方法支持接收ByteBuffer对象,所以在这里我们就不需要调用hasArray()
来判断buffer的底层实现是否是数组了,因为我们不需要使用数组了,直接使用ByteBuffer对象即可。