Volley源代码分析 – 3: 缓存之ByteArrayPool

在Android的开发当中,需要特别小心内存的时候,如果不去控制内存的使用,任意新建或者删除对象,虽然GC可以及时的会收到大量的内存空间,但由于很多情况下,我们的细节也不会处理的特别到位,因此就会出现内存泄漏,最典型的就是,你这部分堆中的内存永远都不会被使用到了,但仍然在栈中有对象指向它。

在Volley的实际中,也特别注意到了这一点。其实很容易想象到,大量的网络访问,接收到的数据结果一般都会保存在一个byte[]的数组当中,然后再调用其他的处理类进行数据分析。我们也知道,Volley的设计初衷是满足那些大量的频繁的网络访问,因此如果直接简单的通过new byte[length]来新建一个数组,必然会大量的频繁的向系统申请内存,如果这些内存处理得到,那么也会消耗大量的GC时间,更别说处理不当带来的内存泄漏,必然导致我们的APP的堆空间的使用量不断上升。此外还可能引入大量的新建对象的资源消耗,当然这个微乎其微。综上,为了解决这些问题,而且又能够保证Volley作为library的轻量,Volley的设计者设计了一个简单的byte[]的池,即ByteArrayPool类,先看源代码,然后再分析一下其原理和作用。

package com.android.volley.toolbox;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

/**
 * ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to
 * supply those buffers to consumers who need to use them for a short period of time and then
 * dispose of them. Simply creating and disposing such buffers in the conventional manner can
 * considerable heap churn and garbage collection delays on Android, which lacks good management of
 * short-lived heap objects. It may be advantageous to trade off some memory in the form of a
 * permanently allocated pool of buffers in order to gain heap performance improvements; that is
 * what this class does.
 * 
 * ByteArrayPool是一个byte[]对象的源和仓库。其目的在于支持那些用户所需要的使用一段时间,然后丢弃的buffer。
 * 在一般的情况下,直接创建,然后丢弃这样的buffer堆空间的大量消耗以及在Android设备的垃圾回收延时,这些都是缺少
 * 一个良好的对于短生命周期的堆的管理。申请一块永久的空间来获取堆性能上的提升是有价值的,这也是这个类的作用。
 * 
 * <p>
 * A good candidate user for this class is something like an I/O system that uses large temporary
 * <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants
 * the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks
 * off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into
 * account and also to maximize the odds of being able to reuse a recycled buffer, this class is
 * free to return buffers larger than the requested size. The caller needs to be able to gracefully
 * deal with getting buffers any size over the minimum.
 * 一个比较好的使用环境是比如像I/O系统中大量的临时的byte[]缓冲的数据拷贝。在这些使用情况中,大多数情况下,用户想要
 * 一个比较小的特定的空间来保证一个好的性能(比如从一个数据流中拷贝一个数据块),但并不会关心这个buffer是否大于他们
 * 所需要的最小的块。考虑到这些,也是尽可能的重用这个可回收的buffer,这个类返回大于请求大小的buffer。调用者需要优雅
 * 的处理任何比最小的所需要的buffer大的空间。
 * <p>
 * If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
 * class will allocate a new buffer and return it.
 * 如果一个请求提交的时候,在回收池当中没有合适的大小的buffer,那么这个类就会申请一块新的空间并且返回。
 * <p>
 * This class has no special ownership of buffers it creates; the caller is free to take a buffer
 * it receives from this pool, use it permanently, and never return it to the pool; additionally,
 * it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there
 * are no other lingering references to it.
 * 该类对于其创建的buffer没有拥有者的概念;调用者可以自由的从pool中获取buffer,永久的使用这些buffer,不再归还。
 * 除此之外,如果从其他的地方申请了buffer,归还到这个位置,也是没有任何坏处的。
 * <p>
 * This class ensures that the total size of the buffers in its recycling pool never exceeds a
 * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
 * least-recently-used buffers are disposed.
 * 这个类保证了回收池中总共的buffer的大小用于不会超过其限制。当返回一个buffer会导致buffer池接近其极限的时候,
 * least-recently-used的buffer就会被丢弃。
 */
/**
 * 我的理解:该buffer的作用就在于通过保存一些buffer来减少申请空间的次数,从而更加优化堆的使用。
 * 其方法是,当需要buffer的时候,从这里面取,如果没有合适的,那么就申请,如果申请到了的话,那么就将这块buffer从
 * 记录的List中删除。
 * 当buffer不用的时候,就将buffer归还,方法是,首先根据buffer的长度将buffer放入到mBuffersBySize的合适的位置上,
 * 然后判断新的buffer的大小是否超过了预设的大小限制,如果超过了的话,那么就从中删除那个占用空间最小的。
 *
 */
public class ByteArrayPool {
    /** The buffer pool, arranged both by last use and by buffer size */
    /** buffer池,通过LastUse和大小管理 */
    private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
    private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);

    /** The total size of the buffers in the pool */
    /** 池中所有的buffer大小的总和 */
    private int mCurrentSize = 0;

    /**
     * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
     * under this limit.
     * 最大的buffers的大小的数量。当接近最大值的时候,就的buffer就会被丢弃。
     */
    private final int mSizeLimit;

    /** Compares buffers by size */
    /** 通过buffer的大小进行比较  */
    protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
        @Override
        public int compare(byte[] lhs, byte[] rhs) {
            return lhs.length - rhs.length;
        }
    };

    /**
     * @param sizeLimit the maximum size of the pool, in bytes
     */
    public ByteArrayPool(int sizeLimit) {
        mSizeLimit = sizeLimit;
    }

    /**
     * Returns a buffer from the pool if one is available in the requested size, or allocates a new
     * one if a pooled one is not available.
     * 从池总获取一个buffer,如果大小合适,则直接读取,如果不合适,则申请一块空间。
     *
     * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
     *        larger.
     * @return a byte[] buffer is always returned.
     */
    public synchronized byte[] getBuf(int len) {
        for (int i = 0; i < mBuffersBySize.size(); i++) {
            byte[] buf = mBuffersBySize.get(i);
            if (buf.length >= len) {
                //此块buffer大小满足长度要求
              
                //当前buffer池可用大小减去buf的长度
                mCurrentSize -= buf.length;
                //该buffer刚刚被使用,所以从mBuffersBySize中删掉
                mBuffersBySize.remove(i);
                //该buffer刚刚被使用,所以从mBuufersByLastUse中删掉
                mBuffersByLastUse.remove(buf);
                //返回该buffer
                return buf;
            }
        }
        return new byte[len];
    }

    /**
     * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
     * size.
     * 将一块buffer归还给池,如果pool达到其分配的大小,则丢弃任何的旧的buffer。
     * @param buf the buffer to return to the pool.
     */
    public synchronized void returnBuf(byte[] buf) {
        if (buf == null || buf.length > mSizeLimit) {
            return;
        }
        //将该buffer放入到mBuufersByLastUse当中。
        mBuffersByLastUse.add(buf);
        //找到在mBuffersBySize中合适的位置。
        int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
        if (pos < 0) {
            pos = -pos - 1;
        }
        //将buffer添加到合适的位置上。
        mBuffersBySize.add(pos, buf);
        //将可用的buffer长度进行修改。
        mCurrentSize += buf.length;
        trim();
    }

    /**
     * Removes buffers from the pool until it is under its size limit.
     * 如果当前系统中的可用buffer过多的话,则删除其中最小的buffer。
     */
    private synchronized void trim() {
        while (mCurrentSize > mSizeLimit) {
            byte[] buf = mBuffersByLastUse.remove(0);
            mBuffersBySize.remove(buf);
            mCurrentSize -= buf.length;
        }
    }

}

其实整个ByteArrayPool的程序比较简单,我的理解就是

  • 该buffer的作用就在于通过保存一些buffer来减少申请空间的次数,从而更加优化堆的使用(可能在使用率不高的时候,会浪费一定的内存空间,但代价不高)。
  • 其方法是
    1. 当需要buffer的时候,从这里面取,如果没有合适的,那么就申请一个新的大小合适的buffer。
    2. 如果申请到了的话,那么就将这块buffer从记录的List中删除。即意味着再次有其他程序获取buffer的时候,不会使用到这块buffer。
    3. 当buffer使用完毕时候,就将buffer归还。方法是:
      1. 根据buffer的长度将buffer放入到mBuffersBySize的合适的位置上
      2. 判断现在ByteArrayPool所占用的空间是否超过了预设的大小,如果超过了的话,那么就从中删除那个占用空间最小的。

基于ByteArrayPool的PoolingByteArrayOutputStream

/**
 * A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead
 * of always allocating them fresh, saving on heap churn.
 */
public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
    /**
     * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
     * the default size to which the underlying byte array is initialized.
     */
    private static final int DEFAULT_SIZE = 256;

    private final ByteArrayPool mPool;

    /**
     * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
     * to this instance, the underlying byte array will expand.
     */
    public PoolingByteArrayOutputStream(ByteArrayPool pool) {
        this(pool, DEFAULT_SIZE);
    }

    /**
     * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
     * more than {@code size} bytes are written to this instance, the underlying byte array will
     * expand.
     *
     * @param size initial size for the underlying byte array. The value will be pinned to a default
     *        minimum size.
     */
    public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
        mPool = pool;
        buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
    }

    @Override
    public void close() throws IOException {
        mPool.returnBuf(buf);
        buf = null;
        super.close();
    }

    @Override
    public void finalize() {
        mPool.returnBuf(buf);
    }

    /**
     * Ensures there is enough space in the buffer for the given number of additional bytes.
     */
    private void expand(int i) {
        /* Can the buffer handle @i more bytes, if not expand it */
        if (count + i <= buf.length) {
            return;
        }
        byte[] newbuf = mPool.getBuf((count + i) * 2);
        System.arraycopy(buf, 0, newbuf, 0, count);
        mPool.returnBuf(buf);
        buf = newbuf;
    }

    @Override
    public synchronized void write(byte[] buffer, int offset, int len) {
        expand(len);
        super.write(buffer, offset, len);
    }

    @Override
    public synchronized void write(int oneByte) {
        expand(1);
        super.write(oneByte);
    }
}

需要注意的几点:

  • 继承自ByteArrayOutputStream,所有的写入操作都是同步的
  • 在写入之前首先会检查是否需要扩展buffer的空间。
  • 覆写了finalize()方法,当垃圾回收的时候,会尝试归还所申请的空间,但注意,JVM并不保证这段代码一定会执行,而且在此实例的声明周期中,只会执行一次。所以我们尽量要调用close()方法显示的归还。
    • 当然,如果不归还也没太大的问题,空间还是会被垃圾回收的,但就违背了我们使用这个输出流的初衷了。

 

性能?打算跑个测试看一下,应该还是不错,基本的测试思路是,开N个线程,在N个线程中申请随机大小的空间,如此重复,观察内存的占用情况。

About: happyhls


5 thoughts on “Volley源代码分析 – 3: 缓存之ByteArrayPool”

  1. 判断现在ByteArrayPool所占用的空间是否超过了预设的大小,如果超过了的话,那么就从中删除那个占用空间最小的。应该是依次删除最长时间未使用的那些(mBuffersByLastUse列表头部开始删除),直到所总缓存空间在限制内,不是占用空间最小的。

  2. 楼主你好。查看到volley源码中 /**
    * @param httpStack HTTP stack to be used
    */
    public BasicNetwork(HttpStack httpStack) {
    // If a pool isn’t passed in, then build a small default pool that will give us a lot of
    // benefit and not use too much memory.
    this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
    }
    BasicNetwork初始化时,private static int DEFAULT_POOL_SIZE = 4096;也就是
    /**
    * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
    * under this limit.
    */
    private final int mSizeLimit; 它被复制成了4096,个人觉得太小了,并且对于所有Length大于4096的情况就没起到作用。

    不知道为什么DEFAULT_POOL_SIZE为这么小?

    1. Hi,其实DEFAULT_POOL_SIZE为4096的时候并不小,因为这代表着缓存池里面最多可能同时存在4096份数组实例。在BaseNetwork中,数组缓存池主要用来接收数据上,但实际的应用场景中,并不会有这么高的并发,所以够用;而且如果设置的太高的话,也会造成内存占用始终无法释放的问题。所以这个是各取舍的问题,一般默认的就好,如果实在应用场景非常特殊,再修改也没有问题。

      1. /**
        * Removes buffers from the pool until it is under its size limit.
        */
        private synchronized void trim() {
        while (mCurrentSize > mSizeLimit) {
        byte[] buf = mBuffersByLastUse.remove(0);
        mBuffersBySize.remove(buf);
        mCurrentSize -= buf.length;
        }
        }

        hi,您看这个方法,变量mSizeLimit如果是多少份数组实例的话,这里怎么理解呢[思考]

        1. Hi,mSizeLimit就是这个byte池子所限制的最大的容量,如果超过容量,就依次把最不常用的Buffer给除去,知道满足容量的要求为止。

发表评论

电子邮件地址不会被公开。 必填项已用*标注