高效易用的okio(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/f409031mn/article/details/85563393

在上篇 高效易用的 okio(二) 的结尾提到了 Segment ,这是一个内存缓冲区, IO 读写操作能如此高效,都是通过这个 Segement

Segement 本质是一个字节数组,同时也是一个循环双向链表,同时为了提高效率,okio 还提供了一个 SegmentPool 用于存储空闲状态的 Segment 用于复用,它们构成了 okio 高效的内存使用政策,它们的关系如下图:

在这里插入图片描述

SegmentPool

SegmentPool 代码量不多,它负责管理处于空闲状态的 Segment (循环双向链表),SegmentPool 最大容量是64KB,而一个 Segment 占用 8KB,因此最多保存 8 个空闲的 Segment,它的核心只有两个,用于添加和回收 Segment

/**
 * 当池子里面有空闲的 Segment 就直接复用,否则就创建一个新的 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(); 
  }

 /**
  * 回收 segment 进行复用,提高效率
  */
  static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    // 共享的 segment 不能被回收  
    if (segment.shared) return; 
    synchronized (SegmentPool.class) {
      // 池子满了,无法不能回收新的 segment  
      if (byteCount + Segment.SIZE > MAX_SIZE) return;
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }

从这里就可以看出 okio 的一个内存策略,就是在不影响内存效率的情况下,尽可能去重复使用现有的资源,这个策略用的好,带来的收益是很可观的

Segment

现在来看这个 Segment,可以说这个是最核心的底层结构了,我们先来看下它的一些参数和构造方法:

    /**
     * segment 中字节数组的大小
     */
    static final int SIZE = 8192;

    /**
     * segment 拆分时,当数据量少于这个值时不做数据共享
     */
    static final int SHARE_MINIMUM = 1024;

    /**
     * segment 中实际存放数据的地方
     */
    final byte[] data;

    /**
     * data数组中下一个读取的数据的位置,也就是一个数据读取的位置index
     */
    int pos;

    /**
     * data数组中下一个写入的数据的位置,也就是一个数据写入的位置index
     */
    int limit;

    /**
     * 为 true 表示有其他 segment 和当前 segment 共享 data 数组
     */
    boolean shared;

    /**
     * 为 true 时表示当前 segment 是 data 数组的实际拥有者,并能够进行数据的写入
     */
    boolean owner;

    /**
     * 当前节点在双向链表中的指向的下一个 Segment
     */
    Segment next;

    /**
     * 当前节点在双向链表中的指向的前一个 Segment
     */
    Segment prev;

    /**
     * 创建一个新的 Segment
     */
    Segment() {
        this.data = new byte[SIZE];
        this.owner = true;
        this.shared = false;
    }

    /**
     * 创建一个共享 data数组 的 Segment 或克隆一个 Segment
     */
    Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {
        this.data = data;
        this.pos = pos;
        this.limit = limit;
        this.shared = shared;
        this.owner = owner;
    }

上面有几个重要的概念:

  • SIZE: 这个说明了一个 Segment 占据的字节大小

  • data :显而易见,这个就是 Segment 存储的数据

  • shared :它用于标明当前 Segment 是否一个共享 data 数组 的 Segment

  • owner:它用于区分当前 Segment 是否它拥有的 data 数组 的实际拥有者 ,一份 data 数组只能有一个拥有者,其他的都是使用者 ;只有 data 数组 的实际拥有者才可以对 data 数组 进行写入操作,其他 Segment 仅可以对这份 data 数组进行读取操作而已

    扫描二维码关注公众号,回复: 4793253 查看本文章
  • next :当前节点在双向链表中的指向的下一个 Segment

  • prev : 当前节点在双向链表中的指向的前一个 Segment

既然 Segment 是链表,那么它自然是有相关的链表操作的:

 /**
  * 当前节点的位置插入一个新的 Segment
  */
public Segment push(Segment segment) {
        segment.prev = this;
        segment.next = next;
        next.prev = segment;
        next = segment;
        return segment;
}

   /**
     * 在双向循环链表中删除当前节点并返回当前节点的next节点
     * 操作完成后,next节点去到了当前节点的位置
     */
    public @Nullable
    Segment pop() {
        Segment result = next != this ? next : null;
        prev.next = next;
        next.prev = prev;
        next = null;
        prev = null;
        return result;
    }

这里都是较为简单的数据结构的操作,下面我们来写 Segment 的核心操作:合并与拆分

Segment 的合并

现在我们知道每个 Segment 占据着 8KB 的空间,为了达到高效利用的目标, okio 会合并一些邻近的 Segment ,具体怎么做,我们看下图:

在这里插入图片描述

上面简单说明了 okio 的合并操作,也就是邻近压缩的方法:当邻近的数据合并起来并没有超出大小(8192字节)的限制,就把它们合并起来,并回收一个空闲的 Segment ,供下次使用

具体操作的方法,我们还是看代码吧:

   /**
     * 合并邻近的 Segment 并回收
     */
    public void compact() {
        // 前一个 segment 不能跟当前 segment 是同一个 segment
        if (prev == this) throw new IllegalStateException();
        // 前一个 segment 必须是data字节数组的拥有者,这样才能写入数据
        if (!prev.owner) return;
        // 计算当前 segment 还未读取的数据字节数
        int byteCount = limit - pos;
        // 计算前一个 segment 剩余可用空间
        int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
        // 只有当前一个 segment 剩余空间大于等于当前 segment 未读取的数据大小时,才能写入
        if (byteCount > availableByteCount) return; 
        // 将当前segment的数据合并到前一个segment中
        writeTo(prev, byteCount);
        // 将当前 segment 从链表中移除
        pop();
        // 将当前 segment 在 segment 池中进行资源回收
        SegmentPool.recycle(this);
    }

这里可以看出,合并的核心代码在于 writeTo 方法,前面都是一些判断操作,接着来看下 writeTo 方法好了:

   /**
     * 进行数据合并的写入操作
     *
     * @param sink      前一个 Segment , 也就是数据写入的目标
     * @param byteCount 写入的数据的字节数
     */
    public void writeTo(Segment sink, int byteCount) {
        // 前一个 Segment 必须是data字节数组的拥有者
        if (!sink.owner) throw new IllegalArgumentException();
        // 大块的空间不够,进行腾挪操作,也就是覆盖部分用于读取的数据
        if (sink.limit + byteCount > SIZE) {
            //共享 Segment 无法进行写入操作
            if (sink.shared) throw new IllegalArgumentException();
            //如果当前写入位置和写入数据的字节数减去当前可读的字节数还是超出了尺寸大小限制
            if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
            System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
            sink.limit -= sink.pos;
            sink.pos = 0;
        }
        // 有足够的空间,直接进行内存拷贝操作
        System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
        sink.limit += byteCount;
        pos += byteCount;
    }

代码并不复杂,具体看注释就好了,上面重新提到了一个概念:共享

要理解什么是 Segment 的共享,那么就要知道 Segment 的拆分了

Segment 的拆分

为了高效的利用内存, okio 不仅会合并压缩邻近的 Segment ,还会拆分 Segment

什么情况下会拆分 Segment 呢?具体看下下面的代码:

	@Override
	public void write(Buffer source, long byteCount) {
        ........
        while (byteCount > 0) { 
          //判断是否调整 Segment   
          if (byteCount < (source.head.limit - source.head.pos)) {
             if (tail != null && tail.owner
                 && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
                       //Segment 空间充足,直接写入数据
                        source.head.writeTo(tail, (int) byteCount);
                        source.size -= byteCount;
                        size += byteCount;
                        return;
                    } else {
                        //Segment 空间不足,拆分为两个 Segment,再写入数据
                        source.head = source.head.split((int) byteCount);
                    }
               }     
        ........
        }     
	}

okio 会在单个 Segment 空间不足以存储写入的数据时,就会尝试拆分为 两个 Segment

拆分的代码如下:

public Segment split(int byteCount) {
        // 参数范围校验
        if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
        // 拆分出来的新 Segment
        Segment prefix;

        //只有数据大于SHARE_MINIMUM才考虑使用共享
        if (byteCount >= SHARE_MINIMUM) {
            // 共享 Segment
            prefix = sharedCopy();
        } else {
            // 非共享 Segment 的逻辑
            prefix = SegmentPool.take();
            // 直接分为两个数据一样的 Segment,但是是两个 data 数组了
            //和直接得到一个全新的 Segment 对象没区别
            System.arraycopy(data, pos, prefix.data, 0, byteCount);
        }
        //设置新 Segment 写入数据的 index
        prefix.limit = prefix.pos + byteCount;
        //设置当前节点已经读取到的 index
        pos += byteCount;
        //插入新的 Segment
        prev.push(prefix);
        return prefix;
    }

    /**
     * 返回一个与此共享 data数组 的 Segment
     * 用于共享数据的 Segment 无法写入数据和合并
     */
    Segment sharedCopy() {
        shared = true;
        return new Segment(data, pos, limit, true, false);
    }

可以看到,拆分走了两个逻辑,如果走的是共享 Segment 的逻辑,那么结果如下图:
在这里插入图片描述

最终目标还是多出一个用于写入数据的空间,以便达到更好的内存使用效果

如果走的不是共享 Segment 的逻辑,那么结果如下图:

在这里插入图片描述

结语

到此相大家已经充分理解了 okioSegment 了,可以看到为了达到最好的内存优化效果, okio 下了非常大的力气, 希望大家在日常开发中,能把 okio 用起来,它远比 Java 的原生 IO 更加好用

下一篇 okio 的文章就轮到 okiotimeOut 机制了,敬请期待

猜你喜欢

转载自blog.csdn.net/f409031mn/article/details/85563393
今日推荐