IndexInput/IndexOutput类系
综述:Lucene在存储和读取索引的时候,把文件内容都当作字节来对待。Int型拆分成1-5个byte分别存储;float则拆分成1-10个byte分别存储。Char型拆分成1-3个byte来存储。
1. IndexInput/IndexOutput类系的层次图
其中,FSDirectory.FSIndexInput和FSDirectory.FSIndexOutput是FSDirectory的内部类(protected static)。
2.部分代码说明
IndexInput/IndexOutput类
在综述里说过,Lucene把文本都以字节为单位进行处理。下面是IndexInput/IndexOutput部分方法的代码清单,从中我们能清楚的理解Lucene的文本处理方式。
writeInt(int)方法把int型变量处理成4个字节,从高位到低位分别存储。
1 public void writeInt(int i) throws IOException {
2 writeByte((byte) (i >> 24)); // 写高8位
3 writeByte((byte) (i >> 16)); // 写次高8位
4 writeByte((byte) (i >> 8)); // 写次次高8位
5 writeByte((byte) i); //写低8位
6 }
writeVInt(int)方法把int型变量处理成1-5个字节,从低位到高位分别存储。值小的,占用的字节数就少;值大的,占用的字节数就多。这个就是Lucene压缩存储的基石了。
1 public void writeVInt(int i) throws IOException {
2 while ((i & ~0x7F) != 0) { // 当最高位不为0,执行循环体
3 writeByte((byte) ((i & 0x7f) | 0x80)); // 写入低7位,最高位置1
4 i >>>= 7; // 向右偏移7位,也就是往高位移动7位
5 }
6 writeByte((byte) i); // 写入数据最高位(肯定不足7位了),最高位显然是0
7 }
IndexInput中的readInt()和readVInt()用来读取文件内容。
readInt()在读取时,把读取的4个字节从高位到底位依次拼接。这一点在下面的代码中可以很容易看出来。
1 public int readInt() throws IOException {
2 return ((readByte() & 0xFF) << 24) | ((readByte() & 0xFF) << 16)
3 | ((readByte() & 0xFF) << 8) | (readByte() & 0xFF);
4 }
readVInt()稍微有点复杂,它的读取顺序是由低位到高位,步骤如下:
(1).读入一个字节存入变量b
(2).b取后7位,存入变量i;若b首位是0,则返回i
(3).读取下个字节存入b,b往左偏移7*(n-1)位后与i拼接后存入i,转到(2)
注:
A.n为循环次数
B.其实只要理解了writeVInt(int)的写入方式后,readVInt()就不难理解了。
下面是readVInt()的代码清单:
1 public int readVInt() throws IOException {
2 byte b = readByte(); // 读取第一个字节
3 int i = b & 0x7F; // 取后7位
4 for (int shift = 7; (b & 0x80) != 0; shift += 7) { // 当该字节首位不为0,执行循环体
5 b = readByte(); // 读取下个字节
6 i |= (b & 0x7F) << shift; // 取该字节后7位,偏移到高位,跟原i值拼接
7 }
8 return i;
9 }
至于writeLong(long),它在形式上把long拆成2个int来处理;writeVLong(long)/readVLong()思路(代码)跟writeVInt(int)/readVLong()除了方法名之外,完全一样;realLong()通过两次readInt(),第一个值偏移32位后拼接第二个值。
writeChars(char[],int,int)用来把一个符合UTF-8编码的字符数组写入文件,它同样把字符拆分成字节来对待。对每个字符,按照其有效位数n(去掉高位的0)的不同,采用有三种不同的写入方法:
(1).0< n <=7,取后7位,首位置0写入文件
(2).7< n <=11或者n=0,取高1-5位,首3位置110;取后6位,首2位置10;写入文件
(3).11< n <=16,取高0-4位,首4位置1110;取中6位,首2位置10;取后6位,首2位置10;写入文件
其代码及注释如下:
1 public void writeChars(char[] s, int start, int length) throws IOException {
2 // start为开始的字符在char[]中的位置,length为需要写的字符的个数
3 final int end = start + length;
4 for (int i = start; i < end; i++) { // 循环遍历char[]中从start到end的字符
5 final int code = (int) s[i];
6 if (code >= 0x01 && code <= 0x7F)
7 // code值在0x01-0x7F,直接写入
8 // code的有效位数为1-7位
9 writeByte((byte) code);
10 else if (((code >= 0x80) && (code <= 0x7FF)) || code == 0) {
11 // code值在0x80-0x7FF或者为0,则分两个字节写入
12 // code的有效位数8-11位
13 writeByte((byte) (0xC0 | (code >> 6))); // 写高2-5位,首3位置110
14 writeByte((byte) (0x80 | (code & 0x3F))); // 写低6位,首2位置10
15 } else {
16 //0x7FF之后的用3个字节写入,code有效位数12-16位
17 writeByte((byte) (0xE0 | (code >>> 12))); // 写高0-4位,首4位置1110
18 writeByte((byte) (0x80 | ((code >> 6) & 0x3F))); //写此高位6位,首2位置10
19 writeByte((byte) (0x80 | (code & 0x3F))); //写低6位,首2位置10
20 }
21 }
22 }
writeChars(String, int, int)思路(代码)跟上面是一样的。
与writeChars(char[], int, int)对应的readChars(char[], int, int)代码及注释如下:
1 public void readChars(char[] buffer, int start, int length)
2 throws IOException {
3 final int end = start + length;
4 for (int i = start; i < end; i++) {
5 byte b = readByte(); // 读取一个字节
6 if ((b & 0x80) == 0) // 如果首位不为1,说明该字节单独为一字符
7 buffer[i] = (char) (b & 0x7F);
8 else if ((b & 0xE0) != 0xE0) { // 首4位不为1110
9 buffer[i] = (char) (((b & 0x1F) << 6) | (readByte() & 0x3F));
10 } else {
11 buffer[i] = (char) (((b & 0x0F) << 12)
12 | ((readByte() & 0x3F) << 6) | (readByte() & 0x3F));
13 }
14 }
15 }
writeString(String)用来写入字符串。它先写入该字符串的长度,然后调用writeChars(String, int, int)写入字符串。代码及注释如下:
1 public void writeString(String s) throws IOException {
2 int length = s.length(); // 字符串长度
3 writeVInt(length); // 写入字符串长度
4 writeChars(s, 0, length); //写入字符串
5 }
readString()在读取的时候利用了IndexInput类的私有变量(private char[] chars)来缓存字符串,唯一需要注意的就是在需要时,要给char扩充容量。代码及注释如下:
public String readString() throws IOException {
int length = readVInt();
if (chars == null || length > chars.length) // 需要时,给chars扩容
chars = new char[length];
readChars(chars, 0, length);
return new String(chars, 0, length);
}
BufferedIndexInput/BufferedIndexOutput类
BufferedIndexInput/BufferedIndexOutput依然是抽象类,它们给出了部分IndexInput/IndexOutput未曾实现的抽象方法,如getFilePointer(),writeByte()/readByte()等等。还提供了writeBytes()/readBytes()这样的在索引优化合并时使用的方法。
BufferedIndexOutput中的变量说明:
1 static final int BUFFER_SIZE = 16384; // buffer的总容量
2
3 private final byte[] buffer = new byte[BUFFER_SIZE]; // 用于写文件时的缓冲区
4 private long bufferStart = 0; // position in file of buffer: buffer 在文件中的偏移量
5 private int bufferPosition = 0; // position in buffer : 在buffer中的偏移量
writeByte(byte)为往buffer中写入byte,代码比较简单,如下:
1 public void writeByte(byte b) throws IOException {
2 if (bufferPosition >= BUFFER_SIZE) // 注意buffer装满时需要flush()
3 flush();
4 buffer[bufferPosition++] = b;
5 }
writeBytes(byte[], int, int)从名字就知道是存储一个byte数组。代码及注释如下:
1 public void writeBytes(byte[] b, int offset, int length) throws IOException { // 该方法在索引优化合并时使用
2 // offset: 首个字节在b中的位置; length: 序列长度(字节数)
3 int bytesLeft = BUFFER_SIZE - bufferPosition; // bytesLeft: buffer剩余容量
4 // is there enough space in the buffer?
5 if (bytesLeft >= length) { // 剩余容量可以放下长度length的字节数
6 // we add the data to the end of the buffer
7 System.arraycopy(b, offset, buffer, bufferPosition, length);
8 bufferPosition += length;
9 // if the buffer is full, flush it
10 if (BUFFER_SIZE - bufferPosition == 0)
11 flush();
12 } else { // 剩余容量放不下
13 // is data larger then buffer?
14 if (length > BUFFER_SIZE) { // BUFFER_SIZE < length 时
15 // we flush the buffer
16 if (bufferPosition > 0)
17 flush();
18 // and write data at once
19 flushBuffer(b, offset, length);
20 bufferStart += length;
21 } else { // bytesLeft < length < BUFFER_SIZE 时,分2次写入
22 // we fill/flush the buffer (until the input is written)
23 int pos = 0; // position in the input data
24 int pieceLength; // 一次往buffer中写入的字节数
25 while (pos < length) { // 我仰天狂吼:为什么?为什么要用循环?!为什么?天呀,为什么你如此偏爱它?
26 // 剩余字节数(length - pos)小于bytesLeft,pieceLength = lenght - pos,否则,pieceLength = bytesLeft
27 pieceLength = (length - pos < bytesLeft) ? length - pos
28 : bytesLeft;
29 System.arraycopy(b, pos + offset, buffer, bufferPosition, pieceLength);
30 pos += pieceLength;
31 bufferPosition += pieceLength; // 改变bufferPosiotion
32 // if the buffer is full, flush it
33 bytesLeft = BUFFER_SIZE - bufferPosition; // 计算剩余容量
34 if (bytesLeft == 0) { // b装满则flush()
35 flush();
36 bytesLeft = BUFFER_SIZE; // flush()后bytesLeft自然就要跟 BUFFER_SIZE 一样了
37 }
38 }
39 }
40 }
41 }
flush():把buffer中内容写入文件,清空buffer。代码及注释如下:
1 public void flush() throws IOException {
2 flushBuffer(buffer, bufferPosition); // buffer中的内容写入文件
3 bufferStart += bufferPosition; // 更改buffer在文件中的偏移量
4 bufferPosition = 0; // buffer为空,则 bufferPosition = 0
5 }
readByte(): 从buffer中读取一个字节。
1 public byte readByte() throws IOException {
2 if (bufferPosition >= bufferLength) //当前读取位置超过buffer中内容有效长度,refill()
3 refill();
4 return buffer[bufferPosition++];
5 }
refill():重新装填buffer。
1 private void refill() throws IOException {
2 long start = bufferStart + bufferPosition; // 计算在文件中的偏移位置
3 long end = start + bufferSize; // 结束位置
4 if (end > length()) // don't read past EOF: 超出文件大小
5 end = length();
6 int newLength = (int) (end - start); // 能读取的长度
7 if (newLength <= 0)
8 throw new IOException("read past EOF");
9
10 if (buffer == null) { // 需要初始化buffer
11 buffer = new byte[bufferSize]; // allocate buffer lazily
12 seekInternal(bufferStart);
13 }
14 readInternal(buffer, 0, newLength); // 这里才是真正的装填buffer
15 bufferLength = newLength; // 设置buffer中有效字节数
16 bufferStart = start; // 设置buffer在文件中的偏移量
17 bufferPosition = 0; // 当前buffer中的偏移量
18 }
readBytes(byte[], int, int, boolean):读取字节数组,跟writeBytes()一样,在索引优化合并时使用。源码中的注释本身已足够清晰了,我就偷了回懒,没写自己的注释,就粘过来了。
1 public void readBytes(byte[] b, int offset, int len, boolean useBuffer)
2 throws IOException {
3
4 if (len <= (bufferLength - bufferPosition)) {
5 // the buffer contains enough data to satisfy this request
6 if (len > 0) // to allow b to be null if len is 0
7 System.arraycopy(buffer, bufferPosition, b, offset, len);
8 bufferPosition += len;
9 } else {
10 // the buffer does not have enough data. First serve all we've got.
11 int available = bufferLength - bufferPosition;
12 if (available > 0) {
13 System.arraycopy(buffer, bufferPosition, b, offset, available);
14 offset += available;
15 len -= available;
16 bufferPosition += available;
17 }
18 // and now, read the remaining 'len' bytes:
19 if (useBuffer && len < bufferSize) {
20 // If the amount left to read is small enough, and
21 // we are allowed to use our buffer, do it in the usual
22 // buffered way: fill the buffer and copy from it:
23 refill();
24 if (bufferLength < len) {
25 // Throw an exception when refill() could not read len
26 // bytes:
27 System.arraycopy(buffer, 0, b, offset, bufferLength);
28 throw new IOException("read past EOF");
29 } else {
30 System.arraycopy(buffer, 0, b, offset, len);
31 bufferPosition = len;
32 }
33 } else {
34 // The amount left to read is larger than the buffer
35 // or we've been asked to not use our buffer -
36 // there's no performance reason not to read it all
37 // at once. Note that unlike the previous code of
38 // this function, there is no need to do a seek
39 // here, because there's no need to reread what we
40 // had in the buffer.
41 long after = bufferStart + bufferPosition + len;
42 if (after > length())
43 throw new IOException("read past EOF");
44 readInternal(b, offset, len);
45 bufferStart = after;
46 bufferPosition = 0;
47 bufferLength = 0; // trigger refill() on read
48 }
49 }
50 }
FSIndexInput/FSIndexOutput类
FSIndexInput/FSIndexOutput继承自BufferedIndexInput/BufferedIndexOutput,它最终补充实现了该类系所需提供服务的全部实现。
FSIndexOutput的flushBuffer(byte[], int, int)方法,它的功能在于真正的完成buffer到文件的数据存储。
1 public void flushBuffer(byte[] b, int offset, int size)
2 throws IOException {
3 file.write(b, offset, size); // 写文件
4 }
与flushBuffer()对应的,readInternal(byte[], int, int)从底层真正的把数据从文件提取到buffer中。
1 protected void readInternal(byte[] b, int offset, int len) // 从文件中读取内容到buffer
2 throws IOException {
3 synchronized (file) { // file需要同步访问
4 long position = getFilePointer(); // 获取当前文件读取位置
5 if (position != file.position) { // file定位到当前读取位置
6 file.seek(position);
7 file.position = position;
8 }
9 int total = 0;
10 do {
11 /**//* 一般情况下,此循环体只会执行一次,只有在第一次循环时,file中内容不能使b全部装满,
12 * 这时,total < len,而下次循环,已读到文件尾部,i = -1,抛出异常。
13 * 也就是说,当b不能读满时,此方法必会抛出异常
14 */
15 int i = file.read(b, offset + total, len - total);
16 if (i == -1)
17 throw new IOException("read past EOF");
18 file.position += i;
19 total += i;
20 } while (total < len);
21 }
22 }
RAMOutputStream类
RAMOutputStream继承自IndexOutput,是用于处理在内存中建索引时的写数据类,它在实例化是需要RAMFile类型的参数。实现了IndexOutput的writeByte()方法,也提供了在索引间拷贝数据用的writeBytes()和writeTo()方法。
writeByte():往内存缓冲区中写一个字节数据。
1 public void writeByte(byte b) throws IOException { // 写单个字节到buffer,如果当前buffer已满,则切换到下个buffer
2 if (bufferPosition == bufferLength) {
3 currentBufferIndex++;
4 switchCurrentBuffer(); // 切换buffer
5 }
6 currentBuffer[bufferPosition++] = b; // 写入 b
7 }
writeBytes():索引间拷贝数据用。
1 public void writeBytes(byte[] b, int offset, int len) throws IOException {
2 while (len > 0) { //
3 if (bufferPosition == bufferLength) { // 如果buffer装满,切换下个buffer
4 currentBufferIndex++;
5 switchCurrentBuffer(); // 切换buffer
6 }
7
8 int remainInBuffer = currentBuffer.length - bufferPosition; // buffer中剩余容量
9 int bytesToCopy = len < remainInBuffer ? len : remainInBuffer; // 实际拷贝长度
10 System.arraycopy(b, offset, currentBuffer, bufferPosition,
11 bytesToCopy); // 拷贝
12 offset += bytesToCopy; // 调整偏移量
13 len -= bytesToCopy; // 调整长度
14 bufferPosition += bytesToCopy; // 调整buffer中当前位置
15 }
16 }
writeTo(IndexOutput):把数据从当前内存缓冲区写到参数指定的IndexOutput中。
1 public void writeTo(IndexOutput out) throws IOException { // 拷贝整个缓冲区数据到out
2 flush();
3 final long end = file.length; // file总长度
4 long pos = 0; // 开始偏移位置
5 int buffer = 0; // buffer索引
6 while (pos < end) {
7 int length = BUFFER_SIZE;
8 long nextPos = pos + length;
9 if (nextPos > end) { // at the last buffer
10 length = (int) (end - pos);
11 }
12 out.writeBytes((byte[]) file.getBuffer(buffer++), length); // 拷贝数据
13 pos = nextPos; // 更改偏移位置
14 }
15 }