Android下的IO库-Okio源码解析(贰)

来源:互联网 发布:java 语句块 执行顺序 编辑:程序博客网 时间:2024/06/07 02:09

上一章,我们简单使用了一下Okio来读取了一个png文件,本章将以上一章的例子,以读取为入口来剖析一下Okio在读取的时候,是如何管理流和内存。

BufferedSource pngSource = Okio.buffer(Okio.source(in));ByteString header = pngSource.readByteString(PNG_HEADER.size());

上一篇的例子中,我们先生成一个Source接口对象,然后再用一个BufferedSource对象。我们来看下Okio的实现:

 /** Returns a source that reads from {@code in}. */  public static Source source(InputStream in) {    return source(in, new Timeout());  }  private static Source source(final InputStream in, final Timeout timeout) {    if (in == null) throw new IllegalArgumentException("in == null");    if (timeout == null) throw new IllegalArgumentException("timeout == null");    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;//设置Buffer的size数据          return bytesRead;        } catch (AssertionError e) {          if (isAndroidGetsocknameError(e)) throw new IOException(e);          throw e;        }      }      @Override public void close() throws IOException {        in.close();//inputstream.close()      }      @Override public Timeout timeout() {        return timeout;      }    };  }

可以看到,实际上source函数会生成一个匿名的Source接口对象,而这个接口匿名实现类,主要实现了read(Buffer buffer,int byteCount)函数。而这个函数作为核心函数实际上做了以下几件事:
1.向Buffer对象sink申请一个可写的内存片段”Segment”对象
2.根据最大可写入数目写入到这个内存片段中。
3.重新记录内存片段中的数据和Buffer对象(sink)中的数据
(不知道是否有看官不明白,明明是一个Buffer对象为什么要声明成为一sink?非墨窃以为:由于你的Buffer相对于你的source是一个输出操作,因此声明成为sink)

那么Buffer是如何申请一个内存页呢?

Segment writableSegment(int minimumCapacity) {    if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();    if (head == null) {      head = SegmentPool.take(); // Acquire a first segment.      return head.next = head.prev = head;    }    Segment tail = head.prev;    if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {//发生越界后将存入一个新的内存片段      tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.    }    return tail;  }

我们可以通过上面的read代码得到以下信息:
1.SegmentPool是一个享元池,所有内存片段都需要从这个静态的享元对象中生成并且存放
2.SegmentPool.take方法可以简单理解为构造一个内存片段Segment
3.Buffer在管理这个内存片段的时候,用的是链表的方式
4.tail.owner代表的是,这个Segment里面的byte[]数组对象,并不是由自己生成的,而是由外面传入的,这个时候,内存偏移的计算方法不由既定的计算方法来计算。而是由外部来自己计算。由于我们使用的是默认情况下的Segment,因此owner参数永远为true。

好了,现在有了byte[]缓存(Segment对象),又有了最大读取数,Source对象就可以通过这两个参数调用InputStream的read方法来读取数据:

int bytesRead = in.read(tail.data, tail.limit, maxToCopy);

但是我们也发现了一些问题:
1.在Okio的Buffer中在你读取的时候,内存占用是没有上限的,也就是在内存允许的情况下,Buffer对象里的内存片是可以无限增长
2.内存片的回收,可以通过往SegmentPool中配置参数来设置,缓存的目的主要是为了避免频繁的内存抖动
3.和操作系统里的知识类似,采用页式管理的内存,实际上会产生一定的碎片化问题
4.在高并发的情况下,比如同时产生多个大型图片请求的情况下,如果使用Buffer对象来管理内存,内存可能会非常的吃紧。

那么如何避免Buffer的这些问题呢?
1.为了避免Buffer的无限增长,需要在必要的时候clear掉Buffer,如果你担心Buffer调用clear后重新分配内存时候的开销,其实大可不必,因为Buffer被clear掉的时候,你的那部分内存页被上述的SegmentPool给缓存住了,因此在低并发情况下,并不会产生很大的内存抖动
2.在高并发情况下,必要时需要引入锁的机制去顺序地占用内存资源
3.为了降低碎片化带来的消耗,内存页不宜太大。页式管理中的内存碎片不可避免,只能降低。

0 0
原创粉丝点击