上一章重点讲了Okio的写入和 Segment 的源码及用处,读取内容和写入的原理一样,对比着读一下就行,这一章讲一下文件的复制及源码细节,复制一个文件如下
public static void copeContent(File file, File fileDst) {
try {
BufferedSource bufferedSource = Okio.buffer(Okio.source(file));
BufferedSink bufferedSink = Okio.buffer(Okio.sink(fileDst));
bufferedSource.readAll(bufferedSink);
bufferedSink.close();
bufferedSource.close();
} catch (Exception e) {
e.printStackTrace();
}
}
这段代码中,前两行上一章都分析过了,关键是中间的一行,牵涉的细节稍微多一点,我们重点关注一下 bufferedSource.readAll(bufferedSink) 方法
RealBufferedSource:
@Override
public long readAll(Sink sink) throws IOException {
if (sink == null) throw new IllegalArgumentException("sink == null");
long totalBytesWritten = 0;
while (source.read(buffer, Segment.SIZE) != -1) {
long emitByteCount = buffer.completeSegmentByteCount();
if (emitByteCount > 0) {
totalBytesWritten += emitByteCount;
sink.write(buffer, emitByteCount);
}
}
if (buffer.size() > 0) {
totalBytesWritten += buffer.size();
sink.write(buffer, buffer.size());
}
return totalBytesWritten;
}
方法形参 sink 的类型实际上是 RealBufferedSink;while 循环中的 source 是 RealBufferedSource 构造方法中传进来的,它对应的是 Okio.source(file),也就是说是
private static Source source(final InputStream in, final Timeout timeout) {
return new Source() {
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (byteCount == 0) return 0;
try {
timeout.throwIfReached();
Segment tail = sink.writableSegment(1);
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
if (bytesRead == -1) return -1;
tail.limit += bytesRead;
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
...
};
}
所以此时 source.read(buffer, Segment.SIZE) 调用的是上面的方法,读取内容时,会把读取到的数据放到 Segment 对象的 data 数组中,writableSegment() 方法是用来创建 Segment 对象并且把他们用链表串联起来的; sink.size += bytesRead 的意思是用 size 来记录一共读取数据的长度,这里 sink 对应的就是 readAll() 方法 while 循环中的 buffer,Segment tail 也是通过 buffer 创建的,并且用它的属性 head 地址指向 tail 对象,此时, buffer 中的两个属性记录了读取的内容和长度;继续往下看,调用了 buffer 的 completeSegmentByteCount() 方法
public long completeSegmentByteCount() {
long result = size;
if (result == 0) return 0;
Segment tail = head.prev;
if (tail.limit < Segment.SIZE && tail.owner) {
result -= tail.limit - tail.pos;
}
return result;
}
这个方法比较有意思,按照目前的逻辑,由于读取内容时一次最大的长度为 Segment.SIZE,即 8192,此时 Segment 链表中只有一个 Segment 对象,此时该方法返回的值就有趣了,如果小于临界点 8192,由于if判断中把它给减去了,此时返回值为0;如果长度是 8192,则返回 8192。重新回到 readAll() 方法中,if (emitByteCount > 0) 判断如果成立,则执行里面的方法;如果文件体积比较大,while循环中会不停的执行,如果文件很小,则 emitByteCount 值为0,跳出while循环,下面还有个if判断,此时buffer.size() 大于0,则执行它内部的方法,while循环内部调用的方法和外部调用的方法是一致的,totalBytesWritten 是记录字节总长度,sink.write(buffer, emitByteCount) 是数据赋值的
RealBufferedSink:
@Override
public void write(Buffer source, long byteCount)
throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.write(source, byteCount);
emitCompleteSegments();
}
重点关注 buffer.write(source, byteCount) 方法,这个方法设计的比较巧妙,堪称是性能优化的核心
@Override
public void write(Buffer source, long byteCount) {
while (byteCount > 0) {
// 重点一
if (byteCount < (source.head.limit - source.head.pos)) {
Segment tail = head != null ? head.prev : null;
if (tail != null && tail.owner && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
// 重点1.1
source.head.writeTo(tail, (int) byteCount);
source.size -= byteCount;
size += byteCount;
return;
} else {
// 重点1.2
source.head = source.head.split((int) byteCount);
}
}
// 重点二
Segment segmentToMove = source.head;
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
source.head = segmentToMove.pop();
if (head == null) {
// 重点2.1
head = segmentToMove;
head.next = head.prev = head;
} else {
// 重点2.2
Segment tail = head.prev;
tail = tail.push(segmentToMove);
tail.compact();
}
// 重点2.3
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
}
}
这个是 Buffer 中的方法,我们假设有 Buffer A 和 Buffer B,此时 A.write(B, 10000);我们来分析一下,这是个while循环,我们假设 重点一 不符合要求,先看 重点二,segmentToMove 是 Buffer B 的头部 head 对象,movedByteCount 是其中有效的数据长度,pop() 方法会把 Segment 的前后链接切断并返回它的下一个 Segment 值,source.head = segmentToMove.pop() 意思就很明显了,B 中的 head 指针位置变了,并且把原头部Segment的链接给切断了,成为了独立的一个 Segment;重点2.1 中,如果 Buffer A 的 head 为null,则直接把 head 的地址指向 segmentToMove 对象,并且形成闭环,next 和 prev 都指向自己;重点2.2 中,head 已经有值,head.prev 其实就是 Segment 链表中的最后一个 Segment,此时通过 tail.push(segmentToMove) 把 segmentToMove 添加到当前链表中,同时调用 compact() 来优化数组内存,这些上一章讲过,可以温故一下;重点2.3 则是把 Buffer B 中的 size 减去 A 中增加的长度,同时更新要赋值的长度 byteCount,看看是否还要继续while循环。
重点二 是关于 Buffer A 和 Buffer B 的数据赋值,这里体现了一点,就是把 Buffer B 中的 Segment 对象的链接切断,直接添加到 Buffer A 的链表,没有说对 Segment 中 data 的数组进行拷贝或复制,这样就节省了CPU的消耗。
重点一,这里判断的是 Buffer B 中head中可复制的内容长度是否大于byteCount,如果满足条件,说明这是while循环中最后一次操作,则判断 Buffer A 中的 head不会null,并获取它的前面一个对象 tail,如果 tail 中的剩余空间够 byteCount 大小的内容,则把 Buffer B 中head中内容写入 Buffer A 中,writeTo() 这个方法上一章也分析过,此时更新 size 的值,然后写入操作到此为止;如果上述条件不满足,走到了 重点1.2,split(int byteCount) 方法上一章也分析过了,意思是在当前的 Segment 基础上在创建一个 Segment,把 byteCount 长度的数据也填充进去,然后把它添加到原 Segment 的链表后面,split(int byteCount) 返回的对象也就创建出来的这个 Segment,此时 Buffer B 中head 指向新创建的这个Segment,如果继续往下看,看到了 重点二,source.head = segmentToMove.pop() 这行代码的意思就是把它链表后面的对象重新赋值给head,它后面的就是原head对应的 Segment。
bufferedSource.readAll(bufferedSink) 方法就分析完了,它下面的两个 close() 上章分析过了,这里略过。所谓封装,并不是简单的将一堆代码写在一个单利类或工具类里,把代码摞起来,而是在明白原先代码的基础上,进行优化和分层,Okio就是个对IO封装的很好的例子。