微信号:MobileDevTips

介绍:专注与原创或者分享Android,iOS,ReactNative,Web前端移动开发领域高质量文章,主要包括业界最新动态,前沿技术趋势,开源函数库与工具等

12 | Android 高级进阶(源码剖析篇) Square 高效易用的&n

2018-05-22 07:45 ASCE1885

作者简介:ASCE1885, 《Android 高级进阶》作者。

本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!

本文分析的源码版本已经 fork 到我的 Github。

在本系列第二篇文章中,我们专门介绍了 okio 中的 Source,但其中 InflaterSource 和 GzipSource 由于涉及 segment 的相关知识,因此没有作介绍,本文就来补充一下。由于压缩和解压算法是配套的,因此,本文同时也会介绍 DeflaterSink 和 GzipSink。顾名思义,

  • DeflaterSink 是一个具备压缩功能的输出流,将数据写入这个 sink 时,它会使用 DEFLATE算法对数据进行压缩操作

  • InflaterSource 是一个具备解压功能的输入流,从这个 source 中读取数据时,它会使用 DEFLATE 算法对自身的数据进行解压操作

  • GzipSink 是一个具备压缩功能的输出流,将数据写入这个 sink 时,它会使用 GZIP 算法对数据进行压缩操作

  • GzipSource 是一个具备解压功能的输入流,从这个 source 中读取数据时,它会使用 GZIP 算法对数据进行解压操作

下面我们分别进行介绍。

PS:okio 中输入流 Source 和输出流 Sink 的字节数据都是存放在自身 segment 链表的元素中,因此,在下面代码中我们可以看到大量的对 segment 的操作,大家请自觉复习本系列第三篇文章。

DEFLATE 算法

DEFLATE 是一个同时运用了 LZ77 算法和哈夫曼编码的无损数据压缩算法,在 Java 中,提供了 Deflater 工具类来实现数据压缩,该 API 使用的示例如下所示:

 
           
  1. String inputString = "blahblahblah";

  2. byte[] input = inputString.getBytes("UTF-8"); // 未压缩的数据

  3. byte[] output = new byte[100]; // 存放压缩后的数据

  4. Deflater compresser = new Deflater();

  5. compresser.setInput(input); // 设置需要压缩的数据

  6. compresser.finish(); // 表示需要压缩的数据设置完成

  7. int compressedDataLength = compresser.deflate(output); // 执行压缩操作

  8. compresser.end(); // 关闭压缩器,并丢弃任何还未压缩的数据

提供 Inflater 工具类来实现数据的解压,该 API 使用的示例如下所示,为了演示方便,承接上面的压缩代码,使用其中的某些变量:

 
           
  1. Inflater decompresser = new Inflater();

  2. decompresser.setInput(output, 0, compressedDataLength); // 设置需要解压的数据

  3. byte[] result = new byte[100];

  4. int resultLength = decompresser.inflate(result); // 执行实际的解压操作

  5. decompresser.end(); // 关闭解压器,并丢弃任何还未解压的数据

可见,API 的使用还是挺简单的,接下来看看怎么将 DEFLATE 算法和 okio 相结合实现数据流的压缩和解压,首先来看压缩操作,也就是 DeflaterSink 这个类。

DeflaterSink

DeflaterSink 类从某个输入流的 segment 链表中读取数据,然后使用 DEFLATE 对数据进行压缩后存放到自身的 segment 链表中,核心算法在 write 方法中,该方法首先进行参数校验,然后每次以一个 segment 为单位,循环从输入流中读取数据进行压缩,直到达到 write 方法指定的压缩字节数,具体的逻辑我们直接看代码和注释:

 
           
  1. @Override public void write(Buffer source, long byteCount) throws IOException {

  2.    // 参数校验

  3.    checkOffsetAndCount(source.size, 0, byteCount);

  4.    // byteCount表示剩余的需要压缩的字节数

  5.    while (byteCount > 0) {

  6.      // 每次从输入流source中取出segment链表的第一个元素

  7.      Segment head = source.head;

  8.      // 根据segment中的未读的数据大小和byteCount来决定要压缩的数据字节数

  9.      int toDeflate = (int) Math.min(byteCount, head.limit - head.pos);

  10.      // 设置需要压缩的数据

  11.      deflater.setInput(head.data, head.pos, toDeflate);

  12.      // 将原始数据压缩后存入当前sink中

  13.      deflate(false);

  14.      // 移动数据指针

  15.      source.size -= toDeflate;

  16.      head.pos += toDeflate;

  17.      if (head.pos == head.limit) {

  18.        // 输入流已经没有未压缩数据,释放资源

  19.        source.head = head.pop();

  20.        SegmentPool.recycle(head);

  21.      }

  22.      // 重新计算未压缩数据字节数

  23.      byteCount -= toDeflate;

  24.    }

  25. }

可以看到,上面代码操作的主要是输入流的 segment,而实际的压缩操作和对输出流的 segment 的申请等操作则在 deflate 私有方法中,如下所示:

 
           
  1. private void deflate(boolean syncFlush) throws IOException {

  2.    Buffer buffer = sink.buffer();

  3.    while (true) {

  4.      // 在当前sink中申请一个位于segment链表尾部的segment,用来存放压缩后的数据

  5.      Segment s = buffer.writableSegment(1);

  6.      // 调用Deflater类的压缩方法执行实际的压缩操作,压缩后的数据存放在s.data中

  7.      int deflated = syncFlush

  8.          ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH)

  9.          : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit);

  10.      if (deflated > 0) {

  11.        // 有数据被压缩了

  12.        s.limit += deflated;

  13.        buffer.size += deflated;

  14.        sink.emitCompleteSegments();

  15.      } else if (deflater.needsInput()) {

  16.        // 没有数据被压缩,同时也不存在需要压缩的数据,则释放申请的segment

  17.        if (s.pos == s.limit) {

  18.          buffer.head = s.pop();

  19.          SegmentPool.recycle(s);

  20.        }

  21.        return;

  22.      }

  23.    }

  24. }

总结起来,DeflaterSink 的核心算法就是将输入流中需要压缩的数据,以 segment 为单位循环进行压缩后存放到输出流的 segment 中,仅此而已,当然细节是魔鬼,一切的细节都在上面的代码中,大家可以细细品味。

InflaterSource

和 DeflaterSink 相对应,InflaterSource 的核心算法就是将自身需要解压的数据,以 segment 为单位进行解压后存放到输出流中。我们来看下核心的 read 方法,该方法首先进行参数校验,然后从当前输入流 source 中读取需要解压的数据填充给解压器 inflater,然后从输出流 sink 中申请 segment,用来存放解压后的数据,代码和注释如下所示:

 
           
  1. @Override public long read(

  2.      Buffer sink, long byteCount) throws IOException {

  3.    // 参数校验

  4.    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);

  5.    if (closed) throw new IllegalStateException("closed");

  6.    if (byteCount == 0) return 0;

  7.    while (true) {

  8.      // 从当前source中获取需要解压的数据并设置给解压器inflater

  9.      boolean sourceExhausted = refill();

  10.      try {

  11.        // 从输出流中申请segment

  12.        Segment tail = sink.writableSegment(1);

  13.        // 调用Inflater类的解压方法执行实际的解压操作,并将解压后的数据存放到输出流sink的tail.data中

  14.        int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit);

  15.        if (bytesInflated > 0) {

  16.          // 有数据被解压,返回被解压的字节数

  17.          tail.limit += bytesInflated;

  18.          sink.size += bytesInflated;

  19.          return bytesInflated;

  20.        }

  21.        // 没有需要解压的数据

  22.        if (inflater.finished() || inflater.needsDictionary()) {

  23.          // 清理未解压的数据

  24.          releaseInflatedBytes();

  25.          if (tail.pos == tail.limit) {

  26.            // 回收前面申请的segment

  27.            sink.head = tail.pop();

  28.            SegmentPool.recycle(tail);

  29.          }

  30.          return -1;

  31.        }

  32.        if (sourceExhausted) throw new EOFException("source exhausted prematurely");

  33.      } catch (DataFormatException e) {

  34.        throw new IOException(e);

  35.      }

  36.    }

  37. }

需要注意的一点是,上面代码中每次解压都是把当前 segment 中剩余未读的所有数据(大小是 Segment.SIZE - tail.limit)进行解压,而入参 byteCount 没有使用到,这是有问题的,这部分代码如下:

 
           
  1. Segment tail = sink.writableSegment(1);

  2. int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit);

正确的做法是把入参 byteCount 考虑进来,取亮着的最小值作为实际解压的数据大小,如下所示:

 
           
  1. Segment tail = sink.writableSegment(1);

  2. int toRead = (int) Math.min(byteCount, Segment.SIZE - tail.limit);

  3. int bytesInflated = inflater.inflate(tail.data, tail.limit, toRead);

通过查看 Github 上最新的 okio 的代码,我们可以发现这个问题已经被解决了,具体可以参见这次提交。

好,我们继续分析,在上面 read 方法中调用了另外两个私有方法,其中 refill 是重新给解压器设置需要解压的数据,也就是调用 Inflater 的 setInput 方法,代码如下所示:

 
           
  1. public boolean refill() throws IOException {

  2.    // 判断inflater中是否已经有需要解压的数据

  3.    if (!inflater.needsInput()) return false;

  4.    // 清理inflater中未解压的数据

  5.    releaseInflatedBytes();

  6.    if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible?

  7.    // source 中没有数据了

  8.    if (source.exhausted()) return true;

  9.    // 每次从输入流source中取出segment链表的第一个元素

  10.    Segment head = source.buffer().head;

  11.    // 记录inflater当前持有(setInput)的未解压数据字节数,默认是segment中所有的未读数据

  12.    bufferBytesHeldByInflater = head.limit - head.pos;

  13.    // 设置需要解压的数据

  14.    inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater);

  15.    return false;

  16. }

releaseInflatedBytes 是根据解压过的数据大小来移动 source 中第一个 segment 的数据读取指针,代码如下所示:

... 更多内容请点击阅读原文继续阅读。

 
ASCE1885 更多文章 11 | Android 高级进阶(源码剖析篇) Square 高效易用的&n 10 | Android 高级进阶(源码剖析篇) Square 高效易用的&n 09 | Android 高级进阶(源码剖析篇) Square 高效易用的&n 08 | Android 高级进阶(源码剖析篇) Square 高效易用的&n 07 | Android 高级进阶(源码剖析篇) Twitter 的高性能序列
猜您喜欢 再回眸,丽影如初 - 腾讯ISUX 携程无线APM平台 - 如何实现全球端到端性能监控 WEB测试 酒后谈IT,那些术语大妈都能秒懂! LinkedME,用深度链接打破App信息孤岛