Okio 的使用及源码分析(二)

上一章重点讲了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封装的很好的例子。


 

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/102710773