另一篇相关文章:https://androidblog.blog.csdn.net/article/details/109855062

前言

ByteBuffer,顾名思义,它表示字节缓冲区。一般我们在代码中使用字节的时候一般用字节数组,即byte[],但是使byte[]的方式效率不高,而使用ByteBuffer的方式来操作数组效率是比较高的,具体描述可以查看jdk文档声明,可以查看ByteBuffer类,以及它的父类Buffer的文档声明,可以详细的了解它们的功能,它们是nio包下面的,nio就是new io,是在jdk1.4版本的时候新出的IO相关的类,是为了解决旧的IO效率低的问题的。ByteBuffer除了效率高之外,也提供了一些比较好用的方法,比如writeIntgetIntputFloatgetFloat等一些读写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,也可以为其他值,这取决于缓冲区类型及其构建方式。

传输数据

此类的每个子类都定义了两种getput 操作:

  • 相对:getput一个或多个元素。position会随之改变。如果请求的传输超出限制,则get操作将抛出 BufferUnderflowExceptionput操作将抛出 BufferOverflowException;这两种情况下,都没有数据被传输。

  • 绝对:操作采用显式元素索引,该操作不影响位置。如果索引参数超出限制,则get操作和put操作将抛出 IndexOutOfBoundsException

mark和reset

调用mark()时,将使用mark保存position,当调用reset()时,将把position恢复为markmark不能大于position,如果调用了mark(),则在positionlimit调整为小于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]

如上代码,我们给buf1but2都是分配的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对象即可。

04-28 15:00