学习基于OkHttp的网络框架(一)Okio详解 - Go语言中文社区

学习基于OkHttp的网络框架(一)Okio详解


继承的缺点


如果要给一个类扩展功能应该怎么做?

继承与组合:
复用代码是进行程序设计的一个重要原因,组合和继承被委以重任,其中继承更是面向对象的基石之一,但相比于组合,继承其实有诸多缺点。组合只要持有另一个类的对象,就可以使用它暴露的所有功能,同时也隐藏了具体的实现(黑盒复用);组合之间的关系是动态的,在运行才确定;组合有助于保持每个类被封装,并被集中在单个任务上(单一原则)。而然,类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的(白盒复用);继承是在编译时刻静态定义的,即是静态复用,在编译后子类已经确定了;继承中父类定义了子类的部分实现,而子类中又会重写这些实现,修改父类的实现,这是一种破坏了父类的封装性的表现。总之组合相比继承更具灵活性。即便如此,我们有不得不使用继承的理由:

向上转型,复用接口

如果用继承来扩展功能会遇到上面所说的诸多问题,对父类的方法做了修改的话,则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,并且因为子类是静态的,当扩展的功能是多种情况的组合的话,你必须枚举出所有的情况为它们定义子类。比如咖啡店里有四种咖啡:


现在还可以给咖啡添加额外的四种调料
Milk,Chocolate,Icecream,Whip如果为每一种咖啡和调料的组合编写子类将有64种情况,显然这种类型体系臃肿是无法接受的!
那么有什么方法可以即保留向上转型的继承结构,又避免继承带来的问题呢 ?

装饰者优化继承结构


Decorator Pattern

ConcreteComponentComponent是原有的继承结构,相比于直接在ConcreteComponent上开刀来扩展功能,我们重新定义了一个Decorator类,Decorator用组合的方式持有一个Component对象,同时继承Component这样就实现了保留向上转型的继承结构的同时,拥有组合的优点:

  1. 通过动态的方式来扩展一个对象的功能
  1. 通过装饰类的排列组合,可以创造恒多不同行为的组合
  2. 装饰类Decorator和构建类ConcreteComponent可以独立变化

OKio原理分析


好了,终于进入正题了。和Java的io流相同,Okio的整体设计也是装饰者模式,一层层的拼接流(Stream)正是在使用使用装饰者在装饰的过程。

  • Okio封装了java.io,java.nio的功能使用起来更方便
  • Okio优化了缓存,使io操作更高效

Source和Sink流程

SourceSink类似于InputStreamOutputStream,是io操作的顶级接口类,SourceSink中只定义了三个方法:

  public interface Source extends Closeable {
  /**
   * 定义基础的read操作,该方法将字节写入Buffer
   */
  long read(Buffer sink, long byteCount) throws IOException;

  /** Returns the timeout for this source. */
  Timeout timeout();

  /**
   * Closes this source and releases the resources held by this source. It is an
   * error to read a closed source. It is safe to close a source more than once.
   */
  @Override void close() throws IOException;
}

Sink的结构是相同的,就不废话了。那么Source和Sink的具体实现在哪里呢?Okio类提供了静态的方法生产SinkSource,这个方法也比较简单,将InputStream中的数据写入到BufferSegment中,BufferSegment是Okio对io流操作进行优化的关键类,后面在详细讨论,先把读写操作的流程走完。

  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);
          //写入Segment 
          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;
        }
      }

      //.....
}

不同的读取操作定义在BufferedSource中,它同样也是个接口:

BufferedSource

BufferedSource的具体实现是RealBufferedSource,可以看到RealBufferedSource其实是个装饰类,内部管理Source对象来扩展Source的功能,同时拥有Source读取数据时用到的Buffer对象。

final class RealBufferedSource implements BufferedSource {
  public final Buffer buffer = new Buffer();
  public final Source source;
  boolean closed;

    //....
    @Override public long read(Buffer sink, long byteCount) throws IOException {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");

    //先将数据读到buffer中
    if (buffer.size == 0) {
       //source是被装饰的对象
      long read = source.read(buffer, Segment.SIZE);
      if (read == -1) return -1;
    }

    long toRead = Math.min(byteCount, buffer.size);
    return buffer.read(sink, toRead);
  }
  //...
}

小结一下:
Source对象每次read,Sink对象每次write都需要一个Buffer对象,Buffer管理者循环双向链表Segment,每次读写数据都先保存在segment中进行缓冲,BufferedSourceBufferedSink进行读写操作时都是间接调用BufferSegment的操作来完成的,整个过程层层嵌套还是有点绕的。
InputStream--Source--BufferedSource--Buffer--segment--Buffer--Sink--BufferedSink--OutputStream

为什么Okio更高效

buffer注释中说明了Okio的高效性:

  1. 采用了segment的机制进行内存共享和复用,避免了copy数组;
  2. 根据需要动态分配内存大小;
  3. 避免了数组创建时的zero-fill,同时降低GC的频率。

Segment和SegmentPool:
Segment是一个循环双向列表,内部维护者固定长度的byte[]数组:

  static final int SIZE = 8192;
  /** Segments 用分享的方式避免复制数组 */
  static final int SHARE_MINIMUM = 1024;
  final byte[] data;
  /** data[]中第一个可读的位置*/
  int pos;
  /** data[]中第一个可写的位置 */
  int limit;
  /**与其它Segment共享  */
  boolean shared;
  boolean owner;

  Segment next;
  Segment prev;
  /**
   * 将当前segment从链表中移除
   */
  public Segment pop() {
     //....
  }
  /**
   * 将一个segment插入到当前segment后
   */
  public Segment push(Segment segment) {
    //....
  }

SegmentPool是一个Segment池,由一个单向链表构成。该池负责Segment的回收和闲置Segment的管理,也就是说Buffer使用的Segment是从Segment单向链表中取出的,这样有效的避免了GC频率。

  /** 总容量 */
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /**用Segment实现的单向链表,next是表头*/
  static Segment next;

  /** Total bytes in this pool. */
  static long byteCount;
  
  
  //回收闲置的segment,插在链表头部
  static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    if (segment.shared) return; // This segment cannot be recycled.
    synchronized (SegmentPool.class) {
      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
  //从链表头部取出一个
    static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {
        Segment result = next;
        next = result.next;
        result.next = null;
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
  }

Segment中还有两个特殊的方法split()compact()split()根据当前的Segment产生一个新的Segment,新的Segment与原来的Segment共用同一个data[]数组,但是改变了读写的标记位poslimit,从原来的
[pos..limit]拆分为[pos..pos+byteCount]和[pos+byteCount..limit],从而避免了复制数组带来的性能消耗。前一个和自身的数据量都不足一半时,compact()会对segement进行压缩,把自身的数据写入到前一Segment中,然后将自身进行回收,使Segment的利用更高效!

    public Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    // We have two competing performance goals:
    //  - Avoid copying data. We accomplish this by sharing segments.
    //  - Avoid short shared segments. These are bad for performance because they are readonly and
    //    may lead to long chains of short segments.
    // To balance these goals we only share segments when the copy will be large.
    if (byteCount >= SHARE_MINIMUM) {
      prefix = new Segment(this);
    } else {
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
  }

Okio实战

Okio封装了io操作底层操纵字节的细节,使用起来更简单了。但一般来说高度的封装意味着无法定制,比如说在网络应用中经常要监听文件的上传下载进度,显然Okio默认是没有这个功能的,应该怎么扩展呢?别忘了,装饰者模式。实际上Okio已经提供了Decorator类:ForwardingSinkForwardingSource,只要继承这两个类就可以自己定制功能了。

class CountingSink extends ForwardingSink{
        private long bytesWritten = 0;
        private Listen listen;
        private File file;
        private long totalLength;
        public CountingSink(Sink delegate,File file,Listen listen) {
            super(delegate);
            this.listen = listen;
            this.file = file;
            totalLength = contentLength();
        }
        public long contentLength(){
            if (file != null) {
                long length = file.length();
                Log.d("abc : length :", length + "");
                return length;
            }else {
                return 0;
            }

        }

        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);
            bytesWritten += byteCount;
            listen.onProgress(bytesWritten, totalLength);
        }

        interface Listen{
            void onProgress(long bytesWritten, long contentLength);
        }
    }

我们复制一首歌做测试:

 File fileSrc = new File(Environment.getExternalStorageDirectory() + "/000szh", "pain.mp3");
 File fileCopy = new File(Environment.getExternalStorageDirectory() + "/000szh","pain3.mp3");

      CountingSink.Listen listen = new CountingSink.Listen() {
            @Override
            public void onProgress(long bytesWritten, long contentLength) {
                long total = contentLength;
                float pos = bytesWritten *1.0f / total;
            }
        };
        BufferedSink bufferedSink = null;
        Source source = null;
        try {
            //包装sink
            Sink sink= Okio.sink(fileCopy);
            CountingSink countingSink = new CountingSink(sink, fileSrc,listen);
            bufferedSink = Okio.buffer(countingSink);
            source = Okio.source(fileSrc);
            bufferedSink.writeAll(source);
            bufferedSink.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                closeAll(bufferedSink, source);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/a42170233a32
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-08 08:41:00
  • 阅读 ( 1172 )
  • 分类:Go Web框架

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢