Android 对象池 Pools 优化

01.什么时对象池

  • 对象池作用
    • 在某些时候,我们需要频繁使用一些临时对象,如果每次使用的时候都申请新的资源,很有可能会引发频繁的 gc 而影响应用的流畅性。这个时候如果对象有明确的生命周期,那么就可以通过定义一个对象池来高效的完成复用对象。
  • 对象池使用场景
    • glide中对加载图片时频繁创建对象使用到了对象池。

02.glide使用对象池

  • glide频繁请求图片
    • 比如Glide中,每个图片请求任务,都需要用到类。若每次都需要重新new这些类,并不是很合适。而且在大量图片请求时,频繁创建和销毁这些类,可能会导致内存抖动,影响性能。
    • Glide使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能。

03.多条件key缓存bitmap

3.1 多条件key创建

  • 首先看一个简单的缓存bitmap代码,代码如下所示

    • 就简单的通过 HashMap 缓存了Bitmap资源,只有在缓存不存在时才会执行加载这个耗时操作。但是上面的缓存条件十分简单,是通过图片的名字决定的,这很大程度上满足不了实际的需求。可能会出现意想不到的问题……
    1
    2
    3
    4
    5
    6
    7
    8
    9
    private final Map<String, Bitmap> cache = new HashMap<>()
    private void setImage(ImageView iv, String name){
    Bitmap b = cache.get(name);
    if(b == null){
    b = loadBitmap(name);
    cache.put(name, b);
    }
    iv.setImageBitmap(b);
    }
  • 多条件 Key

    • 所以我们就需要定义一个Key对象来包含各种缓存的条件,例如我们除了图片名字作为条件,还有图片的宽度,高度也决定了是否是同一个资源,那么代码将变成如下:
    • 注意多条件key需要重写equals和hashCode方法。equals注意是比较两个对象是否相同,而hashCode主要作用是当数据量很大的时候,使用equals一一比较比较会大大降低效率。hashcode实际上是返回对象的存储地址,如果这个位置上没有元素,就把元素直接存储在上面,如果这个位置上已经存在元素,这个时候才去调用equal方法与新元素进行比较就可以提高效率呢!
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    private final Map<Key, Bitmap> cache = new HashMap<>();
    private void setImage(ImageView iv, String name, int width, int height){
    Key key = new Key(name, width, height);
    Bitmap b = cache.get(key);
    if(b == null){
    b = loadBitmap(name, width, height);
    cache.put(key, b);
    }
    iv.setImageBitmap(b);
    }

    public class Key {

    private final String name;
    private final int width;
    private final int heifht;

    public Key(String name, int width, int heifht) {
    this.name = name;
    this.width = width;
    this.heifht = heifht;
    }

    public String getName() {
    return name;
    }

    public int getWidth() {
    return width;
    }

    public int getHeifht() {
    return heifht;
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) {
    return true;
    }
    if (o == null || getClass() != o.getClass()) {
    return false;
    }
    Key key = (Key) o;
    if (width != key.width) {
    return false;
    }
    if (heifht != key.heifht) {
    return false;
    }
    return name != null ? name.equals(key.name) : key.name == null;
    }

    @Override
    public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    final int prime = 31;
    result = prime * result + width;
    result = prime * result + heifht;
    return result;
    }
    }

3.2 key值的复用

  • key值的复用是如何操作的

    • 虽然可以支持多条件的缓存键值了,但是每次查找缓存前都需要创建一个新的 Key 对象,虽然这个 Key 对象很轻量,但是终归觉得不优雅。gilde源码中会提供一个 BitmapPool 来获取 Bitmap 以避免 Bitmap 的频繁申请。而 BitmapPool 中 get 方法的签名是这样的:
    • image
    • Bitmap 需要同时满足三个条件(高度、宽度、颜色编码)都相同时才能算是同一个 Bitmap,那么内部是如何进行查找的呢?需要知道的是,BitmapPool 只是一个接口,内部的默认实现是 LruBitmapPool
  • 看LruBitmapPool中get方法

    • 注意重点看这行代码:final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
    • strategy 是 LruPoolStrategy 接口类型,查看其中一个继承该接口类的 get 方法的实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    @Override
    @NonNull
    public Bitmap get(int width, int height, Bitmap.Config config) {
    Bitmap result = getDirtyOrNull(width, height, config);
    if (result != null) {
    // Bitmaps in the pool contain random data that in some cases must be cleared for an image
    // to be rendered correctly. we shouldn't force all consumers to independently erase the
    // contents individually, so we do so here. See issue #131.
    result.eraseColor(Color.TRANSPARENT);
    } else {
    result = createBitmap(width, height, config);
    }

    return result;
    }


    @Nullable
    private synchronized Bitmap getDirtyOrNull(
    int width, int height, @Nullable Bitmap.Config config) {
    assertNotHardwareConfig(config);
    // 对于非公共配置类型,配置为NULL,这可能导致转换以此处请求的配置方式天真地传入NULL。
    final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
    if (result == null) {
    if (Log.isLoggable(TAG, Log.DEBUG)) {
    Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
    }
    misses++;
    } else {
    hits++;
    currentSize -= strategy.getSize(result);
    tracker.remove(result);
    normalize(result);
    }
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
    Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
    }
    dump();

    return result;
    }
  • 然后看一下SizeConfigStrategy类中的get方法

    • 看一下下面注释的两行重点代码。同样也需要一个专门的类型用来描述键,但是键result居然是也是从一个对象池keyPool中获取的。
    • 可以看到 Key 是一个可变对象,每次先获取一个Key对象(可能是池中的,也可能是新创建的),然后把变量初始化。但是大家知道,HashMap 中的 Key 不应该是可变对象,因为如果 Key的 hashCode 发生变化将会导致查找失效,那么这里是如何做到 Key 是可变对象的同时保证能正确的作为 HashMap 中的键使用呢?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    @Override
    @Nullable
    public Bitmap get(int width, int height, Bitmap.Config config) {
    int size = Util.getBitmapByteSize(width, height, config);
    Key bestKey = findBestKey(size, config);
    //第一处代码
    Bitmap result = groupedMap.get(bestKey);
    if (result != null) {
    decrementBitmapOfSize(bestKey.size, result);
    result.reconfigure(width, height,
    result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
    }
    return result;
    }

    private Key findBestKey(int size, Bitmap.Config config) {
    //第二处代码
    Key result = keyPool.get(size, config);
    for (Bitmap.Config possibleConfig : getInConfigs(config)) {
    NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
    Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
    if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
    if (possibleSize != size
    || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
    keyPool.offer(result);
    result = keyPool.get(possibleSize, possibleConfig);
    }
    break;
    }
    }
    return result;
    }

    @VisibleForTesting
    static class KeyPool extends BaseKeyPool<Key> {
    Key get(int width, int height, Bitmap.Config config) {
    Key result = get();
    result.init(width, height, config);
    return result;
    }

    @Override
    protected Key create() {
    return new Key(this);
    }
    }
  • 然后看一下groupedMap的代码

    • 在查找时,如果没有发现命中的值,那么就会创建新的值,并将其连同 Key 保存在 HashMap 中,不会对 Key 进行复用。而如果发现了命中的值,也就是说 HashMap 中已经有一个和当前 Key 相同的 Key 对象了,那么 Key 就可以通过 offer 方法回收到了 KeyPool 中,以待下一次查找时复用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Nullable
    public V get(K key) {
    LinkedEntry<K, V> entry = keyToEntry.get(key);
    if (entry == null) {
    entry = new LinkedEntry<>(key);
    keyToEntry.put(key, entry);
    } else {
    key.offer();
    }

    makeHead(entry);

    return entry.removeLast();
    }

04.glide对象池总结

  • 优化点
    • 对开销较大的 Bitmap 进行了复用,就连为了复用Bitmap时重复申请的Key对象都进行了复用,尽可能的减少了对象的创建开销,保证了应用的流畅性。
  • 为何要多条件key
    • 针对bitmap,加载图片特别频繁且多,不建议只是简单通过一个name图片名称作为键,因为可能图片名称是一样的,比如有时候接口返回同样名称的图片有大图,正常图,缩略图等,那样可能会存储重复或者碰撞。但是通过name,还有图片宽高字段,就可以大大减小这种问题呢。
  • HashMap中键存储问题
    • 为了正确使用HashMap,选择恰当的Key是非常重要的。Key在HashMap里是不可重复的。也就是说这个key对象的hashcode是不能改变的。那么多条件key是如何保证唯一了,如果要以可变对象作为key的话,那就必须要重写hashcode和equals方法来达到这个目的,除此之外,别无他法。同时这个时候可以利用keyPool对key对象进行缓存。
    • 那么有人会问,要是key值变化了,怎么办?如果HashMap的Key的哈希值在存储键值对后发生改变,Map可能再也查找不到这个Entry了。如果Key对象是可变的,那么Key的哈希值就可能改变。在HashMap中可变对象作为Key会造成数据丢失。这也就是为何key一般要用string或者int值的缘由呢。

05.学以致用对象池

5.1 使用场景

  • 在写图片缩放控件的时候,当双手指滑动时,会频繁操作让图片缩放和移动。这就会频繁用到变化矩阵Matrix,还有RectF绘画相关的工具类。为了防止内存抖动,所以可以使用对象池顺利解决问题。
  • 内存抖动是由于在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在循环里创建对象对内存影响不大,不会造成严重内存抖动这样可以接受也不可避免,频繁的话就很内存抖动很严重),它伴随着频繁的GC。而我们知道GC太频繁会大量占用ui线程和cpu资源,会导致app整体卡顿。

5.2 实现步骤

  • 创建抽象ObjectsPool类,由于缓存的对象可能是不同的类型,这里使用泛型T。主要操作是从对象池请求对象的函数,还有释放对象回对象池的函数。同时可以自己设置对象池的大小,可以使用队列来实现存储功能。

    • 代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    /**
    * 对象池抽象类
    */
    public abstract class ObjectsPool<T> {


    /*
    * 防止频繁new对象产生内存抖动.
    * 由于对象池最大长度限制,如果吞度量超过对象池容量,仍然会发生抖动.
    * 此时需要增大对象池容量,但是会占用更多内存.
    * <T> 对象池容纳的对象类型
    */

    /**
    * 对象池的最大容量
    */
    private int mSize;

    /**
    * 对象池队列
    */
    private Queue<T> mQueue;

    /**
    * 创建一个对象池
    *
    * @param size 对象池最大容量
    */
    public ObjectsPool(int size) {
    mSize = size;
    mQueue = new LinkedList<>();
    }

    /**
    * 获取一个空闲的对象
    *
    * 如果对象池为空,则对象池自己会new一个返回.
    * 如果对象池内有对象,则取一个已存在的返回.
    * take出来的对象用完要记得调用given归还.
    * 如果不归还,让然会发生内存抖动,但不会引起泄漏.
    *
    * @return 可用的对象
    *
    * @see #given(Object)
    */
    public T take() {
    //如果池内为空就创建一个
    if (mQueue.size() == 0) {
    return newInstance();
    } else {
    //对象池里有就从顶端拿出来一个返回
    return resetInstance(mQueue.poll());
    }
    }

    /**
    * 归还对象池内申请的对象
    * 如果归还的对象数量超过对象池容量,那么归还的对象就会被丢弃
    *
    * @param obj 归还的对象
    *
    * @see #take()
    */
    public void given(T obj) {
    //如果对象池还有空位子就归还对象
    if (obj != null && mQueue.size() < mSize) {
    mQueue.offer(obj);
    }
    }

    /**
    * 实例化对象
    *
    * @return 创建的对象
    */
    abstract protected T newInstance();

    /**
    * 重置对象
    *
    * 把对象数据清空到就像刚创建的一样.
    *
    * @param obj 需要被重置的对象
    * @return 被重置之后的对象
    */
    abstract protected T resetInstance(T obj);

    }
  • 然后,可以定义一个矩阵对象池,需要实现上面的抽象方法。如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class MatrixPool extends ObjectsPool<Matrix>{

    /**
    * 矩阵对象池
    */
    public MatrixPool(int size) {
    super(size);
    }

    @Override
    protected Matrix newInstance() {
    return new Matrix();
    }

    @Override
    protected Matrix resetInstance(Matrix obj) {
    obj.reset();
    return obj;
    }
    }

5.3 对象池使用

  • 至于使用,一般是获取矩阵对象,还有归还矩阵对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
    * 矩阵对象池
    */
    private static MatrixPool mMatrixPool = new MatrixPool(16);

    /**
    * 获取矩阵对象
    */
    public static Matrix matrixTake() {
    return mMatrixPool.take();
    }

    /**
    * 获取某个矩阵的copy
    */
    public static Matrix matrixTake(Matrix matrix) {
    Matrix result = mMatrixPool.take();
    if (matrix != null) {
    result.set(matrix);
    }
    return result;
    }

    /**
    * 归还矩阵对象
    */
    public static void matrixGiven(Matrix matrix) {
    mMatrixPool.given(matrix);
    }
  • 注意事项

    • 如果对象池为空,则对象池自己会new一个返回。如果对象池内有对象,则取一个已存在的返回。take出来的对象用完要记得调用given归还,如果不归还,仍然会发生内存抖动,但不会引起泄漏。

5.4 项目实践分享

  • 避免发生内存抖动的几点建议:
    • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
    • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
    • 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
    • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。
  • 大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码的时候显式的在程序里面去创建对象池,然后处理好复用的实现逻辑,要么就是利用系统框架既有的某些复用特性达到减少对象的重复创建,从而减少内存的分配与回收。

06.对象池的容量

  • 通常情况下,我们需要控制对象池的大小
    • 如果对象池没有限制,可能导致对象池持有过多的闲置对象,增加内存的占用
    • 如果对象池闲置过小,没有可用的对象时,会造成之前对象池无可用的对象时,再次请求出现的问题
    • 对象池的大小选取应该结合具体的使用场景,结合数据(触发池中无可用对象的频率)分析来确定。
  • 使用对象池也是要有一定代价的:短时间内生成了大量的对象占满了池子,那么后续的对象是不能复用的。