ByteBuf功能原理
ByteBuf是一个byte数组的缓冲区,通过两个位置指针完成缓冲区的读写操作,读操作使用readerIndex,写操作使用writeIndex。
readerIndex和writeIndex初始取值均为0,写入数据,writeIndex增加;读取数据则readerIndex增加。0~readerIndex之间的数据是已经读取的,调用discardReadBytes()可释放这部分空间,其作用类似于JDK ByteBuffer的compact()方法;readerIndex~writeIndex之间的数据是可读取的,等价于ByteBuffer position和limit之间的数据;writeIndex和capacity之间的空间是可写入的,等价于ByteBuffer limit和capacity之间的可用空间;调用clear()可重置readerIndex和writeIndex为0,但该操作不会清理buffer中的内容。
初始分配的ByteBuf:
+-------------------------------------------------------+| writable bytes |+-------------------------------------------------------+| |0 = readerIndex = writerIndex capacity
写入N个字节后的ByteBuf:
+-------------------------------------+------------------+| readable bytes | writable bytes || (CONTENT) | |+-------------------------------------+------------------+| | |0 = readerIndex N = writerIndex <= capacity
读取M(<=N)个字节后的ByteBuf:
+-------------------+------------------+------------------+| discardable bytes | readable bytes | writable bytes || | (CONTENT) | |+-------------------+------------------+------------------+| | | |0 M = readerIndex <= N = writerIndex <= capacity
调用discardReadBytes()方法之后的ByteBuf:
+-------------------+---------------------+| readable bytes | writable bytes |+-------------------+---------------------+| | |0 = readerIndex N-M = writerIndex <= capacity
调用clear()方法之后的ByteBuf:
+-------------------------------------------------------+| writable bytes |+-------------------------------------------------------+| |0 = readerIndex = writerIndex capacity
ByteBuf 动态扩展
通常情况下,当对JDK ByteBuffer进行put操作时,如果缓冲区可写空间不够,就会抛出BufferOverflowException异常。为了避免这个问题,在进行put操作时,需要对可写空间进行判断,如果剩余可写空间不足,需要创建一个新ByteBuffer,并将之前ByteBuffer的内容复制到新创建的ByteBuffer中,然后释放老的ByteBuffer。
//needSize为需要写入的字节数if(this.buffer.remaining()128 ? needSize:128; ByteBuffer newBuffer=ByteBuffer.allocate(this.buffer.capacity()+realAllocateSize); this.buffer.flip(); newBuffer.put(this.buffer); this.buffer=newBuffer;}
为防止ByteBuffer溢出,每次进行put操作都需要进行可写空间校验,这导致了代冗余。
为了解决这个问题,ByteBuf对write方法进行了封装,由write操作负责进行剩余可用空间的校验,当空间不足时,由ByteBuf自动进行动态扩展(不超过maxCapacity),使用者无需关心底层的校验和动态扩展细节。
源码如下:
@Override public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) { ensureWritable(length); setBytes(writerIndex, src, srcIndex, length); writerIndex += length; return this; }
当执行writeBytes时,先调用ensureWritable(length)进行可写空间的校验。
@Override public ByteBuf ensureWritable(int minWritableBytes) { if (minWritableBytes < 0) { throw new IllegalArgumentException(String.format( "minWritableBytes: %d (expected: >= 0)", minWritableBytes)); } if (minWritableBytes <= writableBytes()) { return this; } if (minWritableBytes > maxCapacity - writerIndex) { throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } // Normalize the current capacity to the power of 2. int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes); // Adjust to the new capacity. capacity(newCapacity); return this; }
当需要写入的字节数大于缓冲区最大可写字节数时,ByteBuf自动进行动态扩展。calculateNewCapacity(writerIndex + minWritableBytes)方法用于计算缓冲区新的容量,capacity(newCapacity)则用于实现动态扩展,后面会详细介绍其源码。
ByteBuf 主要API
顺序读操作(read)
方法名称 | 返回值 | 功能说明 | 抛出异常 |
readBoolean() | boolean | 从readerIndex开始读取1字节的数据 | throws IndexOutOfBoundsException readableBytes<1 |
readByte() | byte | 从readerIndex开始读取1字节的数据 | throws IndexOutOfBoundsException readableBytes<1 |
readUnsignedByte() | short | 从readerIndex开始读取1字节的数据(无符号字节值) | throws IndexOutOfBoundsException: readableBytes<1 |
readShort() | short | 从readerIndex开始读取16位的短整形值 | throws IndexOutOfBoundsException: readableBytes<2 |
readUnsignedShort() | int | 从readerIndex开始读取16位的无符号短整形值 | throws IndexOutOfBoundsException: readableBytes<2 |
readMedium() | int | 从readerIndex开始读取24位的整形值,(该类型并非java基本类型,通常不用) | throws IndexOutOfBoundsException: readableBytes<3 |
readUnsignedMedium() | int | 从readerIndex开始读取24位的无符号整形值,(该类型并非java基本类型,通常不用) | throws IndexOutOfBoundsException: readableBytes<3 |
readInt() | int | 从readerIndex开始读取32位的整形值 | throws IndexOutOfBoundsException: readableBytes<4 |
readUnsignedInt() | long | 从readerIndex开始读取32位的无符号整形值 | throws IndexOutOfBoundsException: readableBytes<4 |
readLong() | long | 从readerIndex开始读取64位的整形值 | throws IndexOutOfBoundsException: readableBytes<8 |
readChar() | char | 从readerIndex开始读取2字节的字符值 | throws IndexOutOfBoundsException: readableBytes<2 |
readFloat() | float | 从readerIndex开始读取32位的浮点值 | throws IndexOutOfBoundsException: readableBytes<4 |
readDouble() | double | 从readerIndex开始读取64位的浮点值 | throws IndexOutOfBoundsException: readableBytes<8 |
readBytes(int length) | ByteBuf | 将当前ByteBuf中的数据读取到新创建的ByteBuf中,从readerIndex开始读取length字节的数据。返回的ByteBuf readerIndex 为0,writeIndex为length。 | throws IndexOutOfBoundsException: readableBytes<length |
readSlice(int length) | ByteBuf | 返回当前ByteBuf新创建的子区域,子区域和原ByteBuf共享缓冲区的内容,但独立维护自己的readerIndex和writeIndex,新创建的子区域readerIndex 为0,writeIndex为length。 | throws IndexOutOfBoundsException: readableBytes<length |
readBytes(ByteBuf dst) | ByteBuf | 将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从当前ByteBuf readerIndex开始读取,直到目标ByteBuf无可写空间,从目标ByteBuf writeIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=读取的字节数。目标ByteBuf的writeIndex+=读取的字节数。 | throws IndexOutOfBoundsException: this.readableBytes<dst.writableBytes |
readBytes(ByteBuf dst, int length) | ByteBuf | 将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从当前ByteBuf readerIndex开始读取,长度为length,从目标ByteBuf writeIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=length,目标ByteBuf的writeIndex+=length | throws IndexOutOfBoundsException: this.readableBytes<length or dst.writableBytes<length |
readBytes(ByteBuf dst, int dstIndex, int length) | ByteBuf | 将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从readerIndex开始读取,长度为length,从目标ByteBuf dstIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=length,目标ByteBuf的writeIndex+=length | throws IndexOutOfBoundsException: dstIndex<0 or this.readableBytes<length or dst.capacity<dstIndex + length |
readBytes(byte[] dst) | ByteBuf | 将当前ByteBuf中的数据读取到byte数组dst中,从当前ByteBuf readerIndex开始读取,读取长度为dst.length,从byte数组dst索引0处开始写入数据。 | throws IndexOutOfBoundsException: this.readableBytes<dst.length |
readBytes(byte[] dst, int dstIndex, int length) | ByteBuf | 将当前ByteBuf中的数据读取到byte数组dst中,从当前ByteBuf readerIndex开始读取,读取长度为length,从byte数组dst索引dstIndex处开始写入数据。 | throws IndexOutOfBoundsException: dstIndex<0 or this.readableBytes<length or dst.length<dstIndex + length |
readBytes(ByteBuffer dst) | ByteBuf | 将当前ByteBuf中的数据读取到ByteBuffer dst中,从当前ByteBuf readerIndex开始读取,直到dst的位置指针到达ByteBuffer 的limit。读取完成后,当前ByteBuf的readerIndex+=dst.remaining() | throws IndexOutOfBoundsException: this.readableBytes<dst.remaining() |
readBytes(OutputStream out, int length) | ByteBuf | 将当前ByteBuf readerIndex读取数据到输出流OutputStream中,读取的字节长度为length | throws IndexOutOfBoundsException: this.readableBytes<length throws IOException |
readBytes(GatheringByteChannel out, int length) | int | 将当前ByteBuf readerIndex读取数到GatheringByteChannel 中,写入out的最大字节长度为length。GatheringByteChannel为非阻塞Channel,调用其write方法不能够保存将全部需要写入的数据均写入成功,存在半包问题。因此其写入的数据长度为【0,length】,如果操作成功,readerIndex+=实际写入的字节数,返回实际写入的字节数 | throws IndexOutOfBoundsException: this.readableBytes<length throws IOException |
顺序写操作(write)
方法名称 | 返回值 | 功能说明 | 抛出异常 |
writeBoolean(boolean value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=1 | throws IndexOutOfBoundsException: this.writableBytes<1 |
writeByte(int value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=1 | throws IndexOutOfBoundsException: this.writableBytes<1 |
writeShort(int value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=2 | throws IndexOutOfBoundsException: this.writableBytes<2 |
writeMedium(int value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=3 | throws IndexOutOfBoundsException: this.writableBytes<3 |
writeInt(int value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=4 | throws IndexOutOfBoundsException: this.writableBytes<4 |
writeLong(long value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=8 | throws IndexOutOfBoundsException: this.writableBytes<8 |
writeChar(int value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=2 | throws IndexOutOfBoundsException: this.writableBytes<2 |
writeFloat(float value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=4 | throws IndexOutOfBoundsException: this.writableBytes<4 |
writeDouble(double value) | ByteBuf | 将value写入到当前ByteBuf中。写入成功,writeIndex+=8 | throws IndexOutOfBoundsException: this.writableBytes<8 |
writeBytes(ByteBuf src) | ByteBuf | 将源ByteBuf src中从readerIndex开始的所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.readableBytes | throws IndexOutOfBoundsException: this.writableBytes<src.readableBytes |
writeBytes(ByteBuf src, int length) | ByteBuf | 将源ByteBuf src中从readerIndex开始,长度length的可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length | throws IndexOutOfBoundsException: this.writableBytes<length or src.readableBytes<length |
writeBytes(ByteBuf src, int srcIndex, int length) | ByteBuf | 将源ByteBuf src中从srcIndex开始,长度length的可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length | throws IndexOutOfBoundsException: srcIndex<0 or this.writableBytes<length or src.capacity<srcIndex + length |
writeBytes(byte[] src) | ByteBuf | 将源字节数组src中所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.length | throws IndexOutOfBoundsException: this.writableBytes<src.length |
writeBytes(byte[] src, int srcIndex, int length) | ByteBuf | 将源字节数组src中srcIndex开始,长度为length可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length | throws IndexOutOfBoundsException: srcIndex<0 or this.writableBytes<src.length or src.length<srcIndex + length |
writeBytes(ByteBuffer mignsrc) | ByteBuf | 将源ByteBuffer src中所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.remaining() | throws IndexOutOfBoundsException: this.writableBytes<src.remaining() |
writeBytes(InputStream in, int length) | int | 将源InputStream in中的内容写入到当前ByteBuf,写入的最大长度为length,实际写入的字节数可能少于length。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=实际写入的字节数。返回实际写入的字节数 | throws IndexOutOfBoundsException: this.writableBytes<length |
writeBytes(ScatteringByteChannel in, int length) | int | 将源ScatteringByteChannel in中的内容写入到当前ByteBuf,写入的最大长度为length,实际写入的字节数可能少于length。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=实际写入的字节数。返回实际写入的字节数 | throws IndexOutOfBoundsException: this.writableBytes<length |
writeZero(int length) | ByteBuf | 将当前缓冲区的内容填充为NUL(0x00),当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=length | throws IndexOutOfBoundsException: this.writableBytes<length |
readerIndex 和 writeIndex
调用ByteBuf的read操作时,从readerIndex开始读取数据,调用ByteBuf的write操作时,从writeIndex开始写入数据,readerIndex和writeInde关系如下:
+-------------------+------------------+------------------+| discardable bytes | readable bytes | writable bytes || | (CONTENT) | |+-------------------+------------------+------------------+| | | |0 <= readerIndex <= writerIndex <= capacity
方法名称 | 返回值 | 功能说明 | 抛出异常 |
readerIndex() | int | 返回当前ByteBuf的readerIndex | |
readerIndex(int readerIndex) | ByteBuf | 修改当前ByteBuf的readerIndex | throws IndexOutOfBoundsException this.writerIndex<readerIndex |
writerIndex() | int | 返回当前ByteBuf的writeIndex | |
writerIndex(int writerIndex) | ByteBuf | 修改当前ByteBuf的writeIndex | throws IndexOutOfBoundsException writeIndex<this.readerIndex or this.capacity<writerIndex |
readableBytes() | int | 获取当前ByteBuf的可读字节数 this.writerIndex -this.readerIndex | |
writableBytes() | int | 获取当前ByteBuf的可写字节数 this.capacity - this.writerIndex | |
setIndex(int readerIndex, int writerIndex) | ByteBuf | 快捷设置当前ByteBuf的readerIndex和writerIndex | throws IndexOutOfBoundsException readerIndex<0 or this.writerIndex<readerIndex or this.capacity<writerIndex |
skipBytes(int length) | ByteBuf | 更新当前ByteBuf的readerIndex,更新后将跳过length字节的数据读取。 | throws IndexOutOfBoundsException this.readableBytes<length |
释放空间和clear操作
方法名称 | 返回值 | 功能说明 |
discardReadBytes() | ByteBuf | 释放0到readerIndex之间已经读取的空间;同时复制readerIndex和writerIndex之间的数据到0到writerIndex-readerIndex之间;修改readerIndex和writerIndex的值。该操作会发生字节数据的内存复制,频繁调用会导致性能下降。此外,相比其他java对象,缓冲区的分配和释放是个耗时的操作,缓冲区的动态扩张需要进行进行字节数据的复制,也是耗时的操作,因此应尽量提高缓冲区的重用率 |
discardSomeReadBytes() | ByteBuf | 功能和discardReadBytes()相似,不同之处在于可定制要释放的空间,依赖于具体实现 |
clear() | ByteBuf | 与JDK 的ByteBuffer clear操作相同,该操作不会清空缓冲区内容本身,其主要是为了操作位置指针,将readerIndex和writerIndex重置为0 |
mark和rest
当对缓冲区进行读写操作时,可能需要对之前的操作进行回滚。ByteBuf可通过调用mark操作将当前的位置指针备份到mark变量中,调用rest操作后,重新将指针的当前位置恢复为备份在mark变量的值。ByteBuf主要有以下相关方法:
markReaderIndex():将当前的readerIndex备份到markedReaderIndex中;
resetReaderIndex():将当前的readerIndex重置为markedReaderIndex的值;
markWriterIndex() :将当前的writerIndex备份到markedWriterIndex中;
resetWriterIndex():将当前的writerIndex重置为markedWriterIndex的值;
相关源码:
@Override public ByteBuf markReaderIndex() { markedReaderIndex = readerIndex; return this; } @Override public ByteBuf resetReaderIndex() { readerIndex(markedReaderIndex); return this; } @Override public ByteBuf markWriterIndex() { markedWriterIndex = writerIndex; return this; } @Override public ByteBuf resetWriterIndex() { writerIndex = markedWriterIndex; return this; }
查找操作
方法名称 | 返回值 | 功能说明 | 抛出异常 |
indexOf(int fromIndex, int toIndex, byte value) | int | 从当前ByteBuf中查找首次出现value的位置,fromIndex<=查找范围<toIndex;查找成功返回位置索引,否则返回-1 | |
bytesBefore(byte value) | int | 从当前ByteBuf中查找首次出现value的位置,readerIndex<=查找范围<writerIndex;查找成功返回位置索引,否则返回-1 | |
bytesBefore(int length, byte value) | int | 从当前ByteBuf中查找首次出现value的位置,readerIndex<=查找范围<readerIndex+length;查找成功返回位置索引,否则返回-1 | IndexOutOfBoundsException: this.readableBytes<length |
bytesBefore(int index, int length, byte value) | int | 从当前ByteBuf中查找首次出现value的位置,index<=查找范围<index+length;查找成功返回位置索引,否则返回-1 | IndexOutOfBoundsException: this.readableBytes<index+length |
forEachByte(ByteBufProcessor processor); | int | 遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从readerIndex开始遍历直到writerIndex。如果满足条件,返回位置索引,否则返回-1 | |
forEachByte(int index, int length, ByteBufProcessor processor) | 遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从index开始遍历直到index+length。如果满足条件,返回位置索引,否则返回-1 | ||
forEachByteDesc(ByteBufProcessor processor) | 逆序遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从writerIndex-1开始遍历直到readerIndex。如果满足条件,返回位置索引,否则返回-1 | ||
forEachByteDesc(int index, int length, ByteBufProcessor processor) | 逆序遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从index+length-1开始遍历直到index。如果满足条件,返回位置索引,否则返回-1 |
Buffer视图
Derived Buffers类似于数据库视图,ByteBuf提供了多个接口用于创建某个ByteBuf的视图或者复制ByteBuf。主要操作如下:
方法名称 | 返回值 | 功能说明 |
duplicate() | ByteBuf | 返回当前ByteBuf的复制对象,复制后的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。 |
copy() | ByteBuf | 从当前ByteBuf复制一个新的ByteBuf对象,复制的新对象缓冲区的内容和索引均是独立的。该操作不修改原ByteBuf的readerIndex和writerIndex。(复制readerIndex到writerIndex之间的内容,其他属性与原ByteBuf相同,如maxCapacity,ByteBufAllocator) |
copy(int index, int length) | ByteBuf | 从当前ByteBuf 指定索引index开始,字节长度为length,复制一个新的ByteBuf对象,复制的新对象缓冲区的内容和索引均是独立的。该操作不修改原ByteBuf的readerIndex和writerIndex。(其他属性与原ByteBuf相同,,如maxCapacity,ByteBufAllocator) |
slice() | ByteBuf | 返回当前ByteBuf的可读子区域,起始位置从readerIndex到writerIndex,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。返回ByteBuf对象的长度为readableBytes() |
slice(int index, int length) | ByteBuf | 返回当前ByteBuf的可读子区域,起始位置从index到index+length,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。返回ByteBuf对象的长度为length |
转换为JDK ByteBuffer
当通过NIO的SocketChannel进行网络读写时,操作的对象为JDK的ByteBuffer,因此须在接口层支持netty ByteBuf到JDK的ByteBuffer的相互转换。
方法名称 | 返回值 | 功能说明 | 抛出异常 |
nioBuffer() | ByteBuffer | 将当前ByteBuf的可读缓冲区(readerIndex到writerIndex之间的内容)转换为ByteBuffer,两者共享共享缓冲区的内容。对ByteBuffer的读写操作不会影响ByteBuf的读写索引。注意:ByteBuffer无法感知ByteBuf的动态扩展操作。ByteBuffer的长度为readableBytes() | UnsupportedOperationException |
nioBuffer(int index, int length) | ByteBuffer | 将当前ByteBuf的可读缓冲区(index到index+length之间的内容)转换为ByteBuffer,两者共享共享缓冲区的内容。对ByteBuffer的读写操作不会影响ByteBuf的读写索引。注意:ByteBuffer无法感知ByteBuf的动态扩展操作。ByteBuffer的长度为length | UnsupportedOperationException |
随机读写(set和get)
除顺序读写之外,ByteBuf还支持随机读写,其最大的区别在于可随机指定读写的索引位置。
关于随机读写的API这里不再详述。无论set或get,执行前都会进行索引和长度的合法性验证,此外,set操作不同于write的是不支持动态扩展。部分源码:
@Override public ByteBuf getBytes(int index, byte[] dst) { getBytes(index, dst, 0, dst.length); return this; } // @Override public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { checkDstIndex(index, length, dstIndex, dst.length); System.arraycopy(array, index, dst, dstIndex, length); return this; } protected final void checkDstIndex(int index, int length, int dstIndex, int dstCapacity) { checkIndex(index, length); if (dstIndex < 0 || dstIndex > dstCapacity - length) { throw new IndexOutOfBoundsException(String.format( "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dstCapacity)); } } protected final void checkIndex(int index, int fieldLength) { ensureAccessible(); if (fieldLength < 0) { throw new IllegalArgumentException("length: " + fieldLength + " (expected: >= 0)"); } if (index < 0 || index > capacity() - fieldLength) { throw new IndexOutOfBoundsException(String.format( "index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity())); } }
@Override public ByteBuf setByte(int index, int value) { checkIndex(index); _setByte(index, value); return this; } //索引合法性验证 protected final void checkIndex(int index) { ensureAccessible(); if (index < 0 || index >= capacity()) { throw new IndexOutOfBoundsException(String.format( "index: %d (expected: range(0, %d))", index, capacity())); } } //确认ByteBuf对象可访问,引用计数器不为0 protected final void ensureAccessible() { if (refCnt() == 0) { throw new IllegalReferenceCountException(0); } } //UnpooledHeapByteBuf 实现 @Override protected void _setByte(int index, int value) { array[index] = (byte) value; }
- 关于ByteBuf继承结构请阅读:
欢迎指出本文有误的地方,转载请注明原文出处