7.4 读写API
本质上,关于ByteBuf的读写都可以看作从指针开始的地方开始读写数据。
writeBytes(byte[] src)与buffer.readBytes(byte[] dst)
writeBytes()表示把字节数组src里的数据全部写到ByteBuf,而readBytes()表示把ByteBuf里的数据全部读取到dst。这里dst字节数组的大小通常等于readableBytes(),而src字节数组大小的长度通常小于等于writableBytes()。
writeByte(byte b)与buffer.readByte()
writeByte()表示往ByteBuf中写一字节,而buffer.readByte()表示从ByteBuf中读取一字节,类似的API还有writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble(),以及readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble(),这里不再赘述,相信读者应该很容易理解这些API。
与读写API类似的API还有getBytes()、getByte()与setBytes()、setByte()系列,唯一的区别就是get、set不会改变读写指针,而read、write会改变读写指针,这一点在解析数据的时候千万要注意。
release()与retain()
由于Netty使用了堆外内存,而堆外内存是不被JVM直接管理的。也就是说,申请到的内存无法被垃圾回收器直接回收,所以需要我们手动回收。这有点类似于C语言里,申请到的内存必须手工释放,否则会造成内存泄漏。
Netty的ByteBuf是通过引用计数的方式管理的,如果一个ByteBuf没有地方被引用到,则需要回收底层内存。在默认情况下,当创建完一个ByteBuf时,它的引用为1,然后每次调用retain()方法,它的引用就加一,release()方法的原理是将引用计数减一,减完之后如果发现引用计数为0,则直接回收ByteBuf底层的内存。
slice()、duplicate()、copy()
在通常情况下,这三个方法会被放到一起比较,三者的返回值分别是一个新的ByteBuf对象。
1.slice()方法从原始ByteBuf中截取一段,这段数据是从readerIndex到writeIndex的,同时,返回的新的ByteBuf的最大容量maxCapacity为原始ByteBuf的readableBytes()。
2.duplicate()方法把整个ByteBuf都截取出来,包括所有的数据、指针信息。
3.slice()方法与duplicate()方法的相同点是:底层内存及引用计数与原始ByteBuf共享,也就是说,经过slice()方法或者duplicate()方法返回的ByteBuf调用write系列方法都会影响到原始ByteBuf,但是它们都维持着与原始ByteBuf相同的内存引用计数和不同的读写指针。
4.slice()方法与duplicate()方法的不同点就是:slice()方法只截取从readerIndex到writerIndex之间的数据,它返回的ByteBuf的最大容量被限制到原始ByteBuf的readableBytes(),而duplicate()方法是把整个ByteBuf都与原始ByteBuf共享。
5.slice()方法与duplicate()方法不会复制数据,它们只是通过改变读写指针来改变读写的行为,而最后一个方法copy()会直接从原始ByteBuf中复制所有的信息,包括读写指针及底层对应的数据,因此,往copy()方法返回的ByteBuf中写数据不会影响原始ByteBuf。
6.slice()方法和duplicate()方法不会改变ByteBuf的引用计数,所以原始ByteBuf调用release()方法之后发现引用计数为零,就开始释放内存,调用这两个方法返回的ByteBuf也会被释放。这时候如果再对它们进行读写,就会报错。因此,我们可以通过调用一次retain()方法来增加引用,表示它们对应的底层内存多了一次引用,引用计数为2。在释放内存的时候,需要调用两次release()方法,将引用计数降到零,才会释放内存。
7.这三个方法均维护着自己的读写指针,与原始ByteBuf的读写指针无关,相互之间不受影响。
retainedSlice()与retainedDuplicate()
相信读者应该已经猜到这两个API的作用了,它们的作用是在截取内存片段的同时,增加内存的引用计数,分别与下面两段代码等价。
使用slice()和duplicate()方法的时候,千万要理清内存共享、引用计数共享、读写指针不共享等概念。下面举两个常见的容易出错的例子。
例1:多次释放
这里的doWith有时候是用户自定义的方法,有时候是Netty的回调方法,如channelRead()等。
例2:不释放造成内存泄漏
想要避免以上两种情况的发生,大家只需要记住一点,在一个函数体里面,只要增加了引用计数(包括ByteBuf的创建和手动调用retain()方法),就必须调用release()方法。